본문 바로가기
Java/Java Story

[Java] Strategy Pattern(전략패턴)[feat. Interface]

by 잭피 2020. 12. 6.

안녕하세요~ 잭코딩입니다!

이번에는 인터페이스의 활용에 대해 글을 써보려고 합니다!

요즘 글쓴이는 우아한테크캠프 Pro라는 교육과정을 듣고 있습니다

이번 미션에서 Strategy Pattern을 적용해보라는 코드리뷰를 받아서 수정해보았습니다

Interface를 이용해 전략 패턴을 사용하였는데 어떤 점에서 코드가 좋아졌을까요?


Strategy Pattern(전략 패턴) ?

전략 패턴이란 무엇일까요?

같은 기능이지만 서로 다른 전략을 가진 클래스들을 각각 캡슐화하여 상호교환할 수 있도록 하는 패턴입니다

 

Strategy Pattern 예시 (feat. CoffeeMachine)

예를 들어서 설명해보겠습니다

커피머신에는 각자가 커피를 내리는 전략이 있습니다 

'커피를 내린다'라는 같은 기능이지만 서로 다른 전략으로 커피를 내립니다

 

 

커피머신

 

커피를 내리는 전략을 코드로 표현해볼까요?

public interface CoffeeStrategy {
    void brew();
}

 

먼저 '커피를 내린다'라는 공통적인 기능을 CoffeeStrategy 인터페이스에 정의해줍니다

그리고 이를 구현해서 아메리카노, 카페라떼 전략 클래스를 정의해줍니다

public class AmericanoStrategy implements CoffeeStrategy {

    private static final String AMERICANO = "아메리카노";
    
    @Override
    public String brew() {
        // 아메리카노를 내리는 기능
        return AMERICANO;
    }
}
public class CafeLatteStrategy implements CoffeeStrategy {

    private static final String CAFE_LATTE = "카페라떼";

    @Override
    public String brew() {
        // 카페라떼를 내리는 기능
        return CAFE_LATTE;
    }
}

이제 커피머신을 보겠습니다

public class CoffeeMachine {
    
    public String brew(CoffeeStrategy coffeeStrategy) {
        return coffeeStrategy.brew();
    }
    
}

전략패턴을 사용하면 커피머신을 아주 간결하게 구현할 수 있습니다

커피전략 인터페이스를 받으면 클라이언트쪽에서 주입하는 구현체에 따라서 전략이 결정되기 때문입니다

 

한번 클라이언트 입장에서 커피머신을 실행시켜볼까요?

길거리를 걸어가던 잭코딩이 커피머신을 보고 아메리카노, 카페라떼를 눌렀습니다

 

 

 

코드를 통해 보겠습니다

public class Road {

    public static void main(String[] args) {
    	// 도로 위에 커피머신을 설치합니다
        CoffeeMachine coffeeMachine = new CoffeeMachine();
        
        // 아메리카노 버튼을 누르면 아메리카노 전략을 넣어 아메리카노를 추출해줍니다
        String americano = coffeeMachine.brew(americanoButton());
        System.out.println(americano);

        // 카페라떼 버튼을 누르면 카페라떼 전략을 넣어 카페라떼를 추출해줍니다
        String cafelatte = coffeeMachine.brew(cafeLatteButton());
        System.out.println(cafelatte);
    }

    public static CoffeeStrategy americanoButton() {
        return new AmericanoStrategy();
    }

    public static CoffeeStrategy cafeLatteButton() {
        return new CafeLatteStrategy();
    }
}

클라이언트가 누르는 버튼에만 전략을 다르게 주면 됩니다

이렇게 코드를 구성하면 무엇이 좋을까요?

 

Strategy Pattern 장점

1. 유지보수하기 좋다 (feat. 핫초코)

글쓴이가 생각하기에 첫번째 장점은 유지보수하기 좋습니다

만약 기계에 핫초코 내리는 방식이 추가되었다고 합니다

한번 코드를 통해 구현해볼까요?

public class HotChocolateStrategy implements CoffeeStrategy {

    private static final String HOTCHOCOLATE = "핫초코";

    @Override
    public String brew() {
        // 핫초코를 내리는 기능
        return HOTCHOCOLATE;
    }
}

먼저 커피전략을 구현하여 핫초코를 내리는 전략 클래스를 만듭니다

 

CoffeeMachine 클래스는 수정할 필요가 없습니다

커피머신은 커피전략의 brew() 메소드를 호출하기만 합니다

바로 도로로 나가서 핫초코 버튼만 달아주고 그 버튼을 눌렀을 때, 핫초코 전략을 보내주면 됩니다

 

 

핫초코가 추가된 커피머신

 

public class Road {

    public static void main(String[] args) {
        CoffeeMachine coffeeMachine = new CoffeeMachine();
        String americano = coffeeMachine.brew(americanoButton());
        System.out.println(americano);

        String cafelatte = coffeeMachine.brew(cafeLatteButton());
        System.out.println(cafelatte);

        String hotchocolate = coffeeMachine.brew(hotChocolateButton());
        System.out.println(hotchocolate);
    }

    public static CoffeeStrategy americanoButton() {
        return new AmericanoStrategy();
    }

    public static CoffeeStrategy cafeLatteButton() {
        return new CafeLatteStrategy();
    }

    public static CoffeeStrategy hotChocolateButton() {
        return new HotChocolateStrategy();
    }
}

 

2. 테스트 코드를 작성하기 좋다 

사실 이건 전략 패턴의 장점보다도 인터페이스로 구현했을 때의 장점? 이라고 생각합니다

그리고 실제 적용해보니 테스트 코드를 작성할 때 아주 편하다는 생각을 했습니다

그러면 제가 수정했던 미션 코드를 가지고 예를 들어보겠습니다

 

글쓴이가 수행했던 미션을 간단히 설명드리겠습니다

1~9까지 랜덤의 숫자를 뽑아서 4이상일 때만 자동차는 전진하는 조건이였습니다

1~9까지 랜덤의 숫자를 뽑아서 4이상인 경우를 전략 클래스로 작성하였습니다

MovingStrategy 인터페이스를 구현하여 RandomMovingStrategy 클래스를 작성하였고, isMove() 메소드를 통해 움직일 수 있을지 없을지를 판단합니다

public interface MovingStrategy {
    boolean isMove();
}
public class RandomMovingStrategy implements MovingStrategy {

    private static final int ADVANCE_MIN_NUMBER = 4;
    private final RandomGenerator randomGenerator;


    public RandomMovingStrategy(RandomGenerator randomGenerator) {
        this.randomGenerator = randomGenerator;
    }

    @Override
    public boolean isMove() {
        return randomGenerator.generate() >= ADVANCE_MIN_NUMBER;
    }
}

이제 이 전략을 사용하는 도메인 클래스(Car)를 볼까요?

public class Car {

    public void move(MovingStrategy movingStrategy) {
        if (movingStrategy.isMove()) {
            position++;
        }
    }

이 자동차는 움직임 전략을 받아서 움직일 수 있으면 차를 움직입니다

이제 이 Car 도메인을 테스트해보죠

 

막상 테스트 코드를 짜려니 "MovingStrategy를 어떻게 넘겨줘야하지?" 라는 생각을 할 수 있습니다

특히나 RandomMovingStrategy의 경우는 랜덤값을 가지고 판단하기 때문에 매번 결과가 다릅니다

 

하지만 잘 생각해보면 Car 도메인 테스트에서는 움직임 전략에 따라 이동하는지 정지하는지에 대한 테스트가 필요합니다

 

움직임 전략이 실제로 동작하지 않아도 됩니다

즉, 움직임 전략이 예측 가능한 결과 값만 리턴해주면 됩니다

 

그러면 이렇게 코드를 작성해볼 수 있습니다

public class CarTest {

    Car car;

    @BeforeEach
    void setUp() {
        car = new Car("jack");
    }

    @Test
    @DisplayName("전진 조건에 만족하면 전진")
    void move() {
        car.move(()->true);
        assertThat(car.getPosition()).isEqualTo(1);
    }

    @Test
    @DisplayName("전진 조건에 만족하지 않으면 스탑")
    void stop() {
        car.move(()->false);
        assertThat(car.getPosition()).isEqualTo(0);
    }

}

car.move(MovingStrategy movingStrategy)에 ()->true 또는 ()->false를 통해 값을 넣어줍니다 

MovingStrategy 인터페이스는 메소드가 1개이므로 Functional Interface입니다

따라서 위처럼 람다형으로도 사용할 수 있습니다

 

이렇게 간단한 예제를 통해 전략패턴과 인터페이스에 관련된 내용을 살펴보았습니다
커피머신의 예제는 github.com/shk3029/DesignPattern.git 에서 받아볼 수 있습니다
잘못된 내용 또는 설명이 있다면 댓글로 말씀해주시면 감사하겠습니다

 

댓글