NEXT STEP > 자바 플레이그라운드 with TDD, 클린 코드
자동차 경주 미션 중
테스트하기 힘든 코드
자동차 경주 미션을 기능 중 전진에 대한 기능을 구현할 때 “0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상” 경우 전진한다는 조건이 있다.
전진조건을 테스트하기 위해서는 난수를 생성해야 되는데 난수의 값은 어떤 값이 나올지 판단할 수 없으므로 테스트하기가 힘들다.
public class Car {
public static final int BOUND = 9;
public static final int FORWARD_NUM = 4;
....
//자동차 전진 메서드
public void move() {
if(makeRandomNum() >= FORWARD_NUM){
this.posoion++
}
}
//0~9 난수 생성 메서드
private int makeRandomNum() {
Random random = new Random();
//n 미만의 랜덤 정수 리턴 (1~9)
return random.nextInt(BOUND) + 1;
}
}
위 코드에서 Car 클래스의 move 메서드가 자동차 전진 메서드이다. 이 메서드는 makeRandomNum 메서드를 호출하여 0~9 값의 난수를 만들어 4 이상이면 전진하는 로직이다.
테스트 코드를 작성해 보자.
public class CarTest {
@Test
void 이동(){
Car car = new Car("Pobi");
car.move();
assertThat(car.getPosition()).isEqualTo(1);
}
@Test
void 정지(){
Car car = new Car("Pobi");
car.move();
assertThat(car.getPosition()).isEqualTo(0);
}
}
테스트 코드를 실행시켜 보면 통과될 때도 있고 실패할 때도 있는 것을 확인할 수 있다. 그런 이유는 난수 생성메서드가 move 메서드 안에 포함되어 있는 난수생성 메서드의 값을 특정할 수 없어 테스트가 힘들게 된다는 것을 확인할 수 있다.
이런 코드는 테스트할 수 없으니 안 좋은 코드라고 볼 수 있다. 설계를 변경하여 테스트 가능하게 변경해 보자.
테스트 가능한 코드로 변경하기
여기서 문제는 랜던값을 외부에서 지정할 수 없다는 것이다.
1. 접근지정자를 변경한다.
private int makeRandomNum() {
Random random = new Random();
//n 미만의 랜덤 정수 리턴 (1~9)
return random.nextInt(BOUND) + 1;
}
//아래와 같이 변경
protected int makeRandomNum() {
Random random = new Random();
//n 미만의 랜덤 정수 리턴 (1~9)
return random.nextInt(BOUND) + 1;
}
위 코드와 같이 private를 protected로 변경하여 외부에서 메서드를 재정의 할 수 있게 변경한다. 이렇게 변경함으로써 외부에서 해당값을 변경할 수 있게 할 수 있다.
테스트 코드를 통해 확인해보자.
@Test
void 이동(){
Car car = new Car("Pobi"){
@Override
protected int makeRandomNum() {
//테스트 값은 범위의 경계값으로 진행한다.
return 4;
}
};
car.move();
assertThat(car.getPosition()).isEqualTo(1);
}
@Test
void 정지(){
Car car = new Car("Pobi"){
@Override
protected int makeRandomNum() {
return 3;
}
};
car.move();
assertThat(car.getPosition()).isEqualTo(0);
}
접근지정자를 변경함에 따라 직접 값을 지정할 수 있게 되어 테스트가 가능해졌다.
이 방법은 메서드의 시그니처를 변경하지 않아도 되는 장점이 있다. 메서드 시그니처를 변경하지 못하는 상황에서 사용하면 된다.
또한, 테스트 코드가 없는 상태에서 테스트코드를 만들어서 리팩토링을 진행할 때 첫 단계로 접근하기가 쉽다. 일단 테스트 가능하게 만들어두고 앞으로 설명할 2,3번째 단계로 점진적으로 적용하면 된다.
2. 메서드 시그니처를 변경한다.
public void move(int randomNo) {
if(randomNo >= FORWARD_NUM){
this.posoion++
}
}
위 코드는 기존코드에서 랜덤을 호출하는 메서드의 클래스 위치를 변경하여 move 메서드에서 매개변수 randomNo를 받도록 수정한 코드이다.
아래는 위 코드로 변경 시 테스트 코드이다.
public class CarTest {
@Test
void 이동(){
Car car = new Car("Pobi");
car.move(4);
assertThat(car.getPosition()).isEqualTo(1);
}
@Test
void 정지(){
Car car = new Car("Pobi");
car.move(3);
assertThat(car.getPosition()).isEqualTo(0);
}
}
이러한 메서드 시그니처를 변경은 테스트 어려운 의존을 매개변수를 통해서 전달받도록 수정하여
메서드가 직접적인 의존을 가지지 않도록 하는 방법이다.
물론 상위 클래스나 다른 클래스에서 해당 move 메서드를 호출하게 된다면 또다시 랜덤에 대한 의존 관계가 생기므로 완전한 분리하고 볼 수는 없다. 그렇다고 모든 클래스에서 의존 관계를 계속 이동시키면 실제 사용하는 위치와 코드가 너무 동떨어지게 되며, 그로 인한 코드 응집도가 떨어지게 된다.
3. 인터페이스를 활용
메서드 시그니처의 문제를 개선하기 위해 활용되는 방법이다. 독립된 인터페이스를 활용해 분리하여 독립적인 구조로 변경한다.
이러한 방식은 처음부터 예측하여 설계하기보단 서비스 유지보수 중 지속적인 변경사항이 발생할 때 적용하는 것도 좋은 방식 중 하나이다.
public void move(MovingStrategy movingStrategy) {
if(movingStrategy.movable()){
this.posoion++
}
}
아까 변경한 매개변수를 위 코드처럼 인터페이스로 선언해준다. 위와 같이 인터페이스의 기능으로 로직을 변경할 수 있다.
위 코드 처럼 변경하기 위해선 우선 인터페이스를 구성해야 한다.
MovingStrategy 생성하고 조건절의 들어갈 movable 메서드를 선언해 준다.
public interface MovingStrategy {
boolean movable();
}
이런 다음 인테페이스를 구현할 구현체를 정의하면 된다.
public class RandomMovingStrategy implements MovingStrategy {
public static final int BOUND = 9;
public static final int FORWARD_NUM = 4;
@Override
public boolean movable() {
return makeRandomNum() >= FORWARD_NUM;
}
private int makeRandomNum() {
Random random = new Random();
//n 미만의 랜덤 정수 리턴 (1~9)
return random.nextInt(BOUND) + 1;
}
}
랜덤과 관련된 로직들이 위 구현체 코드로 이동한 것을 볼 수 있다. 랜덤에 대한 의존을 해당 구현체에서 담당하니 코드 응집도를 떨어뜨리지 않고도 로직을 수행할 수 있다.
아래는 인터페이스로 구현한 테스트 코드이다.
@Test
void 이동(){
Car car = new Car("Pobi");
car.move(new MovingStrategy){
@Override
public boolean movable() {
return true;
}
};
assertThat(car.getPosition()).isEqualTo(1);
}
@Test
void 정지(){
Car car = new Car("Pobi");
car.move(new MovingStrategy){
@Override
public boolean movable() {
return false;
}
};
assertThat(car.getPosition()).isEqualTo(0);
}
물론 메서드 시니그처로 변경하고 바로 적용이 아니라 후에 적용하려면 변경에 대한 이슈가 발생할 수 있다. 해당 메서드가 많은 곳에서 사용된다면 변경에 대한 리소스가 많이 사용될 수 있어 큰 부담으로 느낄 수 있다.
정리하면
위 세 가지 방법 모두 각자의 장점이 존재한다. 프로젝트 설계에 맞추어 적절히 적용하도록 하자.
지금은 리펙토링 방식에 익숙하지 않으니 위 단계를 순차적으로 진행하며 방식을 익히도록 하자.
Reference
해당 강좌 피드백 강의
https://tecoble.techcourse.co.kr/post/2020-05-07-appropriate_method_for_test_by_parameter/
https://tecoble.techcourse.co.kr/post/2020-04-28-test-without-method-change/
https://tecoble.techcourse.co.kr/post/2020-05-17-appropriate_method_for_test_by_interface/
'Language > TDD, 클린코드, 리펙토링' 카테고리의 다른 글
[클린코드] 4장. 주석 (0) | 2023.12.14 |
---|---|
[클린코드] 3장. 함수 (with SOLID) (2) | 2023.12.11 |
[클린코드] 1장 깨끗한 코드, 2장 의미있는 이름을 읽고 정리한 내용 (0) | 2023.12.07 |
[TDD] getter 대신 메시지를 던져라 (0) | 2023.09.03 |
NEXT STEP > 자바 플레이그라운드 with TDD, 클린 코드
자동차 경주 미션 중
테스트하기 힘든 코드
자동차 경주 미션을 기능 중 전진에 대한 기능을 구현할 때 “0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상” 경우 전진한다는 조건이 있다.
전진조건을 테스트하기 위해서는 난수를 생성해야 되는데 난수의 값은 어떤 값이 나올지 판단할 수 없으므로 테스트하기가 힘들다.
public class Car {
public static final int BOUND = 9;
public static final int FORWARD_NUM = 4;
....
//자동차 전진 메서드
public void move() {
if(makeRandomNum() >= FORWARD_NUM){
this.posoion++
}
}
//0~9 난수 생성 메서드
private int makeRandomNum() {
Random random = new Random();
//n 미만의 랜덤 정수 리턴 (1~9)
return random.nextInt(BOUND) + 1;
}
}
위 코드에서 Car 클래스의 move 메서드가 자동차 전진 메서드이다. 이 메서드는 makeRandomNum 메서드를 호출하여 0~9 값의 난수를 만들어 4 이상이면 전진하는 로직이다.
테스트 코드를 작성해 보자.
public class CarTest {
@Test
void 이동(){
Car car = new Car("Pobi");
car.move();
assertThat(car.getPosition()).isEqualTo(1);
}
@Test
void 정지(){
Car car = new Car("Pobi");
car.move();
assertThat(car.getPosition()).isEqualTo(0);
}
}
테스트 코드를 실행시켜 보면 통과될 때도 있고 실패할 때도 있는 것을 확인할 수 있다. 그런 이유는 난수 생성메서드가 move 메서드 안에 포함되어 있는 난수생성 메서드의 값을 특정할 수 없어 테스트가 힘들게 된다는 것을 확인할 수 있다.
이런 코드는 테스트할 수 없으니 안 좋은 코드라고 볼 수 있다. 설계를 변경하여 테스트 가능하게 변경해 보자.
테스트 가능한 코드로 변경하기
여기서 문제는 랜던값을 외부에서 지정할 수 없다는 것이다.
1. 접근지정자를 변경한다.
private int makeRandomNum() {
Random random = new Random();
//n 미만의 랜덤 정수 리턴 (1~9)
return random.nextInt(BOUND) + 1;
}
//아래와 같이 변경
protected int makeRandomNum() {
Random random = new Random();
//n 미만의 랜덤 정수 리턴 (1~9)
return random.nextInt(BOUND) + 1;
}
위 코드와 같이 private를 protected로 변경하여 외부에서 메서드를 재정의 할 수 있게 변경한다. 이렇게 변경함으로써 외부에서 해당값을 변경할 수 있게 할 수 있다.
테스트 코드를 통해 확인해보자.
@Test
void 이동(){
Car car = new Car("Pobi"){
@Override
protected int makeRandomNum() {
//테스트 값은 범위의 경계값으로 진행한다.
return 4;
}
};
car.move();
assertThat(car.getPosition()).isEqualTo(1);
}
@Test
void 정지(){
Car car = new Car("Pobi"){
@Override
protected int makeRandomNum() {
return 3;
}
};
car.move();
assertThat(car.getPosition()).isEqualTo(0);
}
접근지정자를 변경함에 따라 직접 값을 지정할 수 있게 되어 테스트가 가능해졌다.
이 방법은 메서드의 시그니처를 변경하지 않아도 되는 장점이 있다. 메서드 시그니처를 변경하지 못하는 상황에서 사용하면 된다.
또한, 테스트 코드가 없는 상태에서 테스트코드를 만들어서 리팩토링을 진행할 때 첫 단계로 접근하기가 쉽다. 일단 테스트 가능하게 만들어두고 앞으로 설명할 2,3번째 단계로 점진적으로 적용하면 된다.
2. 메서드 시그니처를 변경한다.
public void move(int randomNo) {
if(randomNo >= FORWARD_NUM){
this.posoion++
}
}
위 코드는 기존코드에서 랜덤을 호출하는 메서드의 클래스 위치를 변경하여 move 메서드에서 매개변수 randomNo를 받도록 수정한 코드이다.
아래는 위 코드로 변경 시 테스트 코드이다.
public class CarTest {
@Test
void 이동(){
Car car = new Car("Pobi");
car.move(4);
assertThat(car.getPosition()).isEqualTo(1);
}
@Test
void 정지(){
Car car = new Car("Pobi");
car.move(3);
assertThat(car.getPosition()).isEqualTo(0);
}
}
이러한 메서드 시그니처를 변경은 테스트 어려운 의존을 매개변수를 통해서 전달받도록 수정하여
메서드가 직접적인 의존을 가지지 않도록 하는 방법이다.
물론 상위 클래스나 다른 클래스에서 해당 move 메서드를 호출하게 된다면 또다시 랜덤에 대한 의존 관계가 생기므로 완전한 분리하고 볼 수는 없다. 그렇다고 모든 클래스에서 의존 관계를 계속 이동시키면 실제 사용하는 위치와 코드가 너무 동떨어지게 되며, 그로 인한 코드 응집도가 떨어지게 된다.
3. 인터페이스를 활용
메서드 시그니처의 문제를 개선하기 위해 활용되는 방법이다. 독립된 인터페이스를 활용해 분리하여 독립적인 구조로 변경한다.
이러한 방식은 처음부터 예측하여 설계하기보단 서비스 유지보수 중 지속적인 변경사항이 발생할 때 적용하는 것도 좋은 방식 중 하나이다.
public void move(MovingStrategy movingStrategy) {
if(movingStrategy.movable()){
this.posoion++
}
}
아까 변경한 매개변수를 위 코드처럼 인터페이스로 선언해준다. 위와 같이 인터페이스의 기능으로 로직을 변경할 수 있다.
위 코드 처럼 변경하기 위해선 우선 인터페이스를 구성해야 한다.
MovingStrategy 생성하고 조건절의 들어갈 movable 메서드를 선언해 준다.
public interface MovingStrategy {
boolean movable();
}
이런 다음 인테페이스를 구현할 구현체를 정의하면 된다.
public class RandomMovingStrategy implements MovingStrategy {
public static final int BOUND = 9;
public static final int FORWARD_NUM = 4;
@Override
public boolean movable() {
return makeRandomNum() >= FORWARD_NUM;
}
private int makeRandomNum() {
Random random = new Random();
//n 미만의 랜덤 정수 리턴 (1~9)
return random.nextInt(BOUND) + 1;
}
}
랜덤과 관련된 로직들이 위 구현체 코드로 이동한 것을 볼 수 있다. 랜덤에 대한 의존을 해당 구현체에서 담당하니 코드 응집도를 떨어뜨리지 않고도 로직을 수행할 수 있다.
아래는 인터페이스로 구현한 테스트 코드이다.
@Test
void 이동(){
Car car = new Car("Pobi");
car.move(new MovingStrategy){
@Override
public boolean movable() {
return true;
}
};
assertThat(car.getPosition()).isEqualTo(1);
}
@Test
void 정지(){
Car car = new Car("Pobi");
car.move(new MovingStrategy){
@Override
public boolean movable() {
return false;
}
};
assertThat(car.getPosition()).isEqualTo(0);
}
물론 메서드 시니그처로 변경하고 바로 적용이 아니라 후에 적용하려면 변경에 대한 이슈가 발생할 수 있다. 해당 메서드가 많은 곳에서 사용된다면 변경에 대한 리소스가 많이 사용될 수 있어 큰 부담으로 느낄 수 있다.
정리하면
위 세 가지 방법 모두 각자의 장점이 존재한다. 프로젝트 설계에 맞추어 적절히 적용하도록 하자.
지금은 리펙토링 방식에 익숙하지 않으니 위 단계를 순차적으로 진행하며 방식을 익히도록 하자.
Reference
해당 강좌 피드백 강의
https://tecoble.techcourse.co.kr/post/2020-05-07-appropriate_method_for_test_by_parameter/
https://tecoble.techcourse.co.kr/post/2020-04-28-test-without-method-change/
https://tecoble.techcourse.co.kr/post/2020-05-17-appropriate_method_for_test_by_interface/
'Language > TDD, 클린코드, 리펙토링' 카테고리의 다른 글
[클린코드] 4장. 주석 (0) | 2023.12.14 |
---|---|
[클린코드] 3장. 함수 (with SOLID) (2) | 2023.12.11 |
[클린코드] 1장 깨끗한 코드, 2장 의미있는 이름을 읽고 정리한 내용 (0) | 2023.12.07 |
[TDD] getter 대신 메시지를 던져라 (0) | 2023.09.03 |