최근에 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();
}
}
- 자동차와 버스 둘 다 이동한다는 동일한 동작을 하지만, 자동차는 전달되는 숫자가 더 커야하고, 버스는 작아야 이동합니다.
- 그러다가 버스도 숫자가 커야 이동하도록 개발이 되어야 한다고 가정을 해보겠습니다.
- Bus의 isMove() 메소드를 아래와 같이 변경해주기만 하면 됩니다.
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. 언제 전략 패턴을 사용할 것인가?
- 객체 내에서 한 알고리즘의 다양한 변형들을 사용하고 싶을 때
- 일부 행동을 실행하는 방식에서만 차이가 있는 유사한 클래스들이 많은 경우
- 클래스의 비즈니스 로직을 해당 로직의 콘텍스트에서 그리고 중요하지 않을지도 모르는 알고림즏ㄹ의 구현 세부 사항들로 부터 고립할 때
- 같은 알고리즘의 다른 변형들 사이를 전환하는 거대한 조건문이 있는 경우
5. 전략 패턴의 장단점
5-1. 장점
- 런타임에 한 객체 내부에서 사용되는 알고리즘들을 교환할 수 있다.
- 알고리즘의 구현 세부 정보들을 고립할 수 있다.
- 상속을 합성으로 대체 가능하다.
- OCP(개방-폐쇄 원칙) 콘텍스트를 변경하지 않고도 새로운 전략을 도입하여 구현할 수 있다.
5-2. 단점
- 알고리즘이 많지 않고 거의 변하지 않는다면, 새로운 클래스와 인터페이스로 인해 프로그램이 더욱 복잡해질 수 있다.
- 클라이언트들은 적절한 전략을 선택해야하고, 전략 간의 차이점들을 알아야 한다.
참고
- https://victorydntmd.tistory.com/292
'디자인패턴' 카테고리의 다른 글
[디자인패턴] 생성패턴 - Builder 패턴 (0) | 2022.07.09 |
---|---|
[디자인패턴] 싱글톤(Singleton) 패턴 (0) | 2022.01.16 |