[코드리뷰 스터디 - 🚗 자동차 경주] 전략 패턴 (Strategy Pattern)

Hyunjoon Choi·2023년 10월 1일
0

코드리뷰 스터디

목록 보기
4/5
post-thumbnail

🎲 랜덤 값에 대한 테스트

특정한 값을 랜덤하게 제공해주는 Dice 클래스가 있다고 해 보자.

public class Dice {

    private static final int DICE_MAX_VALUE = 10;
    private static final int RANDOM_RANGE_EXPAND = 10000;
    
    public static int getRandomValue() {
        return (int) ((Math.random) * RANDOM_RANGE_EXPAND) % DICE_MAX_VALUE);
    }
}

만약, 이러한 Dice 클래스에 대해서 테스트를 하려고 하면 어떻게 해야 할까? 특정한 값을 받았을 때 특정 로직이 실행되거나 다른 객체의 상태가 변화되었는지 등을 체크할 때 말이다!

실제로 자동차 경주 미션에서는, Dice로부터 랜덤한 값을 받았을 때 이 값의 기준에 따라 전진할 수도 있고, 무시될 수도 있다. 따라서 기존에는 아래와 같이 작성했었다.

public void racing(int dice) {
    if (isValueSatisfiedToAccelerate(dice)) {
        this.accelerate();
    }
}

바로 이렇게 랜덤 값에 대해서 테스트를 하려고 할 때, 전략 패턴을 활용하면 좋다.

전략 패턴 (Strategy Pattern)

전략 패턴은 디자인 패턴 (Design Pattern)의 일종으로서, 위키백과에 따르면 다음과 같이 정의되어 있다.

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

전략 패턴은 특정한 계열의 알고리즘들을 정의하고 각 알고리즘을 캡슐화하며 이 알고리즘들을 해당 계열 안에서 상호 교체가 가능하게 만든다.
전략은 알고리즘을 사용하는 클라이언트와는 독립적으로 다양하게 만든다. 전략은 유연하고 재사용 가능한 객체 지향 소프트웨어를 어떻게 설계하는지 기술하기 위해 디자인 패턴의 개념을 보급시킨 디자인 패턴(Gamma 등)이라는 영향력 있는 책에 포함된 패턴들 가운데 하나이다.

정의를 봐도 잘 와닿지 않을 수도 있을 것이다. 기술된 UML 그림과 예시 코드를 보자.

위의 UML 클래스 구조를 보면, 전략 패턴의 핵심 요소를 확인할 수 있다. 그것은 바로 전략 (Strategy)가 인터페이스 형태라는 점이다.

왜 전략 패턴에 인터페이스 개념이 있을까? 그것은 객체지향에서의 인터페이스가 사용되는 목적을 보면 이해할 수 있다.

객체지향에서 인터페이스는 같은 기능을 하는 구현체들의 메서드들을 공통되게 선언함으로써 메서드 선언 중복을 줄일 수 있고, 클라이언트에서 정확한 구현체를 모르게 한다는 점에서 추상화에 기여한다는 특징이 있다.

전략 패턴 적용하기

다시 본론으로 돌아와서, 왜 이것이 전략 패턴을 적용하기 좋은지 보자.

자동차 경주 미션에서 제공된 차의 전진 규칙은 다음과 같이 정의되어 있다.

전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다.

그렇다면, 4 이상의 값을 제공하는 Dice 구현체, 3 이하의 값을 제공하는 Dice 구현체, 랜덤 값을 제공하는 Dice 구현체로 만들면 테스트와 클라이언트의 사용을 분리할 수 있을 것이다.

Dice 인터페이스 생성

기존의 Dice를 위에서 말한 인터페이스 형태로 만들자.

public interface Dice {

    int random();
}

클라이언트에서 사용할 구현체 생성

실제 클라이언트에서 사용되는 구현체를 만든다.

public class DiceImpl implements Dice {

    private static final int DICE_MAX_VALUE = 10;

    @Override
    public int random() {
        return (int) (Math.random() * DICE_MAX_VALUE);
    }
}

클라이언트 코드 수정

기존의 racing 메서드는 이제 특정 수를 받는 게 아니라, Dice 클래스를 의존하도록 변경한다.

public void racing(final Dice dice) {
    final int randomNumber = dice.random();
    if (isValueSatisfiedToAccelerate(randomNumber)) {
        this.accelerate();
    }
}

테스트 시 사용될 구현체 생성 1

4 이상의 값을 제공하는 구현체를 만든다.

public class DiceMovableImpl implements Dice {

    private static final int NUMBER = 4;

    @Override
    public int random() {
        return NUMBER;
    }
}

테스트 시 사용될 구현체 생성 2

3 이하의 값을 제공하는 구현체를 만든다.

public class LowerDiceImpl implements Dice {

    private static final int NUMBER = 0;

    @Override
    public int random() {
        return NUMBER;
    }
}

테스트 시 사용될 구현체 생성 3

9를 초과하는 값을 제공하는 구현체를 만든다.

public class HigherDiceImpl implements Dice {

    private static final int NUMBER = 10;

    @Override
    public int random() {
        return NUMBER;
    }
}

실제 테스트 적용

이제, 실제 테스트 시 위의 구현체들을 적절히 활용하면 각 케이스에 대한 테스트를 진행할 수 있다.

@Test
public void 숫자가_조건값보다_크거나_같으면_위치를_1_증가() {
    // given
    Name name = Name.from("hello");
    Car car = Car.createDefault(name);
    Dice movableDice = new DiceMovableImpl();

    // when
    car.racing(movableDice);

    // then
    assertThat(car.isDistanceEqualTo(1)).isTrue();
}

@Test
public void 숫자가_조건값보다_작으면_위치는_그대로여야_한다() {
    // given
    Name name = Name.from("hello");
    Car car = Car.createDefault(name);
    Dice movableDice = new LowerDiceImpl();

    // when
    car.racing(movableDice);

    // then
    assertThat(car.isDistanceEqualTo(0)).isTrue();
}

@Test
public void 숫자가_조건값보다_커도_위치는_그대로여야_한다() {
    // given
    Name name = Name.from("hello");
    Car car = Car.createDefault(name);
    Dice movableDice = new HigherDiceImpl();

    // when
    car.racing(movableDice);

    // then
    assertThat(car.isDistanceEqualTo(0)).isTrue();
}

부족하거나 보완할 점이 있다면 댓글 부탁드립니다 😃

profile
개발을 좋아하는 워커홀릭

0개의 댓글