전략 패턴

June·2022년 2월 19일
1

우테코

목록 보기
10/84

학습 동기

public void race() {
    for(Car car : cars) {
        car.move(movable());
    }
}

여기에 남기는 코멘트는 아니고 Cars#race에 남기는 코멘트입니다.

이 메서드는 도메인 객체인 Cars의 메서드임에도 불구하고 테스트가 불가능한 구조인 것 같아요.
Cars에서 랜덤을 분리한다면 테스트가 가능한 구조를 만들 수 있을 거 같은데 한 번 도전해보시는 것도 좋을 거 같네요 :)

이 부분 이해가 잘 안됐습니다! 테스트 하기 불가능한 구조인 것 까지는 이해를 했는데, Cars에서 랜덤을 분리하면 왜 테스트가 가능해지는지 이해가 잘 안되네요.

항상 일정한 값이 나올 수 있게 하는 외부 라이브러리를 사용하는 방식 역시 전략패턴에 기반하고 있습니다.
전략 패턴을 좀 더 편하게 사용하기 위한 라이브러리라고 생각하시면 좋을 것 같네요.
우테코 레벨 1 미션들에서는 아마 해당 라이브러리 사용의 필요성을 느끼시지 못할 거에요. 가급적 직접 구현해보시길 바랍니다.

지금 코드에서 race()를 호출하면 각 자동차들이 움직였을 수도, 움직이지 않았을 수도 있어서 테스트가 불가능하죠.
이 메서드가 테스트가 되지 않는 이유를 어디서 찾아야할까요?
바로 Cars객체 내부에서 랜덤한 값을 만드는 부분입니다.

그럼 어떻게 테스트 가능하게 구현할 수 있을까요?
실제 코드가 동작할 때는 랜덤으로 전진시키고, 테스트 상황에서는 항상 전진시키거나 전진시키지 않게 구현할 수 있으면 테스트가 가능할 것 같아요.

이 글을 읽어보고 전략패턴에 대해서 한 번 고민해보시면 방법을 찾을 수도 있을 것 같네요.

전략 패턴

전략 패턴(strategy pattern) 또는 정책 패턴(policy pattern)은 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다.

자바의 다형성을 이용하면 구현 가능할 것이라는 느낌이 온다.

적용

우선 인터페이스를 만들어 주자. 자동차 경주 게임에서는 NumberGenerator 인터페이스를 만든다.

NumberGenerator

public interface NumberGenerator {

    int generate();
}

이렇게 하고 나면 move() 메서드는 NumberGenerator를 인자로 받아서, numberGenerator.generate() 의 결과에 따라 판단을 한다.

즉, 실제 앱에서의 generate()와 테스트 환경에서 통제 가능한 generate()를 각각 구현해주면 되는 것이다.

RandomNumberGenerator

public class RandomNumberGenerator implements NumberGenerator {

    private static final int MAX_RANDOM_VALUE = 10;
    private static final Random random = new Random();

    @Override
    public int generate() {
        return random.nextInt(MAX_RANDOM_VALUE);
    }
}

이것은 실제 앱에서 사용될 generator이다.

MovableGenerator

public class MovableNumberGenerator implements NumberGenerator {

    @Override
    public int generate() {
        return 4;
    }
}

테스트용 generator다. 이 MovableNumberGenerator를 인자로 전달한다면 항상 move 가 가능할 것이다. 어디까지나 테스트용이므로 테스트 패키지에 넣어야 한다.

NonMovableGenerator

public class NonMovableGenerator implements NumberGenerator {

    @Override
    public int generate() {
        return 3;
    }
}

NonMovableNumberGenerator를 인자로 전달한다면 항상 move 가 불가능할 것이다.

이제 테스트가 더 이상 랜덤이 아니다. 뭐를 전달해주냐에 따라 직접 통제 가능하다.

CarTest

class CarsTest {

    @DisplayName("각 차들이 모두 움직인다")
    @Test
    public void race() {
        ...

        cars.race(new MovableNumberGenerator());

        assertThat(cars.getParticipantCars())
                .filteredOn(car -> car.isSamePosition(nonMoveCar))
                .isEmpty();
    }

남은 궁금증

이 패턴을 적용시키며 궁금한 점이 하나 생겼습니다. 지금 제 코드에서는 Carsrace()RandomNumberGenerator를 인수로 전달하게 구현하였는데요, 이 전달하는 부분은 RacingController입니다. 이러면 RacingController 부분에서 테스트 하기 어려워 졌다는 생각이 들었습니다 (RandomNumberGenerator 를 전달하니까). 뭔가 책임의 회피 같은 느낌이 들어서 조금 찜찜한 기분이 남아있습니다.

좋은 질문이에요. 이 코멘트에 대한 답변도 같이 드릴 수 있을 거 같아요. Cars에서 랜덤 객체의 생성을 상위 객체로 넘긴 거 처럼 여기서도 더 상위 객체로 옮기면 됩니다. RacingController의 생성자에서 해당 객체를 받아오면 되겠죠.

Application에서 객체를 생성하는게 이상하다고 느끼실 수도 있을텐데, 이 부분은 이번 단계에서는 넘어가도 좋을 거 같아요. 우테코 레벨2 쯤에 이 문제가 어떻게 해결되는지 확인할 수 있겠네요.

레벨2에서 이 문제를 만난다면 다시 보충해서 적도록 하겠다.

출처 및 참고

메서드 시그니처를 수정하여 테스트하기 좋은 메서드로 만들기

인터페이스를 분리하여 테스트하기 좋은 메서드로 만들기

전략 패턴

0개의 댓글