JaeWon's Devlog
article thumbnail
반응형

최근에 NextStep(넥스트스탭)에서의 TDD, 클린코드 With Java 를 수강하면서, 첫번째 미션인 자동차 경주를 구현하던 도중, 리뷰어님께서 아래와 같은 말씀을 해주셨다.

처음에 작성한 코드는 아래와 같았다.

public class Car {
    private static final int MOVE_CONDITION = 4;
    
	...
    
    private boolean isMove(int randomNum) {
        return randomNum >= MOVE_CONDITION;
    }
}

위 코드대로라면 만약 Car의 객체가 조금 다르게 4보다 큰 것이 아닌 5, 7보다 크다면 매번 Car 라는 클래스를 새로 만들어야 한다.

이번 글에서는 위 로직을 전략패턴을 사용하여 위 이슈를 리팩토링을 하면서 정리하고자 한다.


1. 전략 패턴(Strategy Pattern)이란?

- 전략(Strategy) 패턴은 행위들의 인터페이스를 정의하고, 각 인터페이스를 별도의 클래스에 넣은 후 그들의 객체들을 상호교환할 수 있도록 하는 디자인 패턴이다.

- 객체의 행위를 동적으로 바꾸고 싶은 경우 직접 행위를 수정하지 않고 전략을 바꿈으로써 행위를 유연하게 확장하는 방법이다.

- 좀 더 쉽게 말해보자면, 비슷한 동작을 하지만 다르게 구현이 되어야 하는 행위(전략)들을 공통의 인터페이스를 구현하고, 이를 각각의 클래스에서 실제 행위를 구현하여 동적으로 바꾸도록 하는 패턴이다.

 

2. 전략 패턴의 사용 이유

- 위에서와 같이 자동차(Car)버스(Bus) 클래스가 있고, 이 두 클래스는 서로 isMove() 라는 메소드를 가지게 될 것 입니다.

public class Car {
    private static final int MOVE_CONDITION = 4;
    
    	...
    
    public void move() {
        if(isMove(randomNum)) {
            ...
        }
    }
    
    private boolean isMove(int randomNum) {
        return randomNum >= MOVE_CONDITION;
    }
}
public class Bus {
    
    private static final int MOVE_CONDITION = 4;
    
    	...
    
    public void move() {
        if(isMove(randomNum)) {
            ...
        }
    }
    
    private boolean isMove(int randomNum) {
        return randomNum < MOVE_CONDITION;
    }
}

- 이를 isMove()를 가지는 MoveStrategy 라는 전략 인터페이스를 구현하고, 해당 객체를 사용하는 Client 도 있다고 가정하겠습니다.

- 위 그림의 구조로 다시 코드를 작성하면 아래와 같습니다.

public interface MoveStrategy {
    boolean isMove(int number);
}
public class Car implements MoveStrategy {

	...

    @Override
    public boolean isMove(int number) {
        return number >= MOVE_CONDITION;
    }
}
public class Bus implements MoveStrategy {

	...

    @Override
    public boolean isMove(int number) {
        return number < MOVE_CONDITION;
    }
}
public class Client {

    public static void main(String args[]) {
        MoveStrategy car = new Car();
        MoveStrategy bus = new Bus();

        car.move();
        bus.move();
    }
}

- 자동차와 버스 둘 다 이동한다는 동일한 동작을 하지만, 자동차는 전달되는 숫자가 더 커야하고, 버스는 작아야 이동합니다.

- 그러다가 버스도 숫자가 커야 이동하도록 개발이 되어야 한다고 가정을 해보겠습니다.

- BusisMove() 메소드를 아래와 같이 변경해주기만 하면 됩니다.

public class Bus implements MoveStrategy {

	...
    
    @Override
    public boolean isMove(int number) {
        return number >= MOVE_CONDITION; 
    }
}

- 하지만 이렇게 수정을 하게 된다면, 객체지향 설계 5원칙인 SOLID 중 OCP(Open-Closed Principle) 에 위배 됩니다.

- OCP(개방-폐쇄 원칙)에 따르면 기존의 Bus 객체의 isMove() 메소드를 수정하지 않고 행위가 수정되어야만 합니다.

- 이를 전략 패턴을 사용하여 구조를 개선해보겠습니다.

3. 전략 패턴 구현

- 전략을 생성해보도록 하겠습니다.

- 현재 이동은 자동차는 전달되는 숫자가 더 커야하고, 버스는 작아야 이동하는 두 가지 방식이 있습니다.

- 즉, 이동하는 두 방식에 따라 각 Strategy 클래스를 생성합니다.(CarMoveStrategy, BusMoveStrategy)

- 해당 클래스는 MoveStrategy 인터페이스를 상속 받습니다.(캡슐화)

- 위 그림의 코드는 아래와 같습니다.

public interface MoveStrategy {
    boolean isMove(int number);
}
public class CarMoveStrategy implements MoveStrategy{
    private static final int MOVE_CONDITION = 4;
    
    @Override
    public boolean isMove(int number) {
        return number >= MOVE_CONDITION;
    }
public class BusMoveStrategy implements MoveStrategy{
    private static final int MOVE_CONDITION = 4;
    
    @Override
    public boolean isMove(int number) {
        return number < MOVE_CONDITION;
    }
}

- 이렇게 전략을 만들었다면, Car 와 Bus 객체 클래스는 다음과 같이 변경을 할 수 있습니다.

- 각 객체가 생성될 때, 인자를 통해 외부로부터 전략을 주입(의존성 주입)받고 ,isMove() 메소드에서는 MoveStrategy 인터페이스가 제공하는 isMove() 메소드를 호출하여 수행하도록 합니다.

public class Car {

    MoveStrategy carMoveStrategy;

    public Car(MoveStrategy carMoveStrategy) {
        this.carMoveStrategy = carMoveStrategy;
    }
    
    public void move() {
        if(carMoveStrategy.isMove(randomNum)) {
            ...
        }
    }
}
public class Bus {

    MoveStrategy busMoveStrategy;

    public Bus(MoveStrategy carMoveStrategy) {
        this.busMoveStrategy = busMoveStrategy;
    }
    
    public void move() {
        if(busMoveStrategy.isMove(radnomNum)) {
            ...
        }
    }
}

- 다음으로는 위 변경된 객체를 통해서 Client 에서는 아래와 같이 변경이 됩니다.

- 각 객체를 생성할 때, 알맞은 전략을 전달합니다.

- 이렇게 전략을 사용하여 구조가 개선이 되면, 만약 기차라는 새로운 이동수단이 추가되어도 해당 객체와 아래 전략만 추가하면 됩니다.

public class Client {

    public static void main(String args[]) {
        Car car = new Car(new CarMoveStrategy());
        Bus bus = new Bus(new BusMoveStrategy());

        car.move();
        bus.move();
        
        //	이후 기차가 추가된다면
        Train train = new Train(new TrainMoveStrategy());
        train.move();
    }
}

- 최종적으로 개선된 구조를 다이어그램으로 표현하자면 아래와 같습니다.

4. 언제 전략 패턴을 사용할 것인가?

  1. 객체 내에서 한 알고리즘의 다양한 변형들을 사용하고 싶을 때
  2. 일부 행동을 실행하는 방식에서만 차이가 있는 유사한 클래스들이 많은 경우
  3. 클래스의 비즈니스 로직을 해당 로직의 콘텍스트에서 그리고 중요하지 않을지도 모르는 알고림즏ㄹ의 구현 세부 사항들로 부터 고립할 때
  4. 같은 알고리즘의 다른 변형들 사이를 전환하는 거대한 조건문이 있는 경우

5. 전략 패턴의 장단점

5-1. 장점

- 런타임에 한 객체 내부에서 사용되는 알고리즘들을 교환할 수 있다.

- 알고리즘의 구현 세부 정보들을 고립할 수 있다.

- 상속을 합성으로 대체 가능하다.

- OCP(개방-폐쇄 원칙) 콘텍스트를 변경하지 않고도 새로운 전략을 도입하여 구현할 수 있다.

5-2. 단점

- 알고리즘이 많지 않고 거의 변하지 않는다면, 새로운 클래스와 인터페이스로 인해 프로그램이 더욱 복잡해질 수 있다.

- 클라이언트들은 적절한 전략을 선택해야하고, 전략 간의 차이점들을 알아야 한다.


참고

- https://victorydntmd.tistory.com/292

- https://refactoring.guru/ko/design-patterns/strategy 

- https://hudi.blog/strategy-pattern/

반응형
profile

JaeWon's Devlog

@Wonol

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!