[레벨 1 회고] 전략패턴을 사용해보며
미션을 진행하다보면 객체들이 할 수 있는 다양한 행위가 설계해야 하는 경우가 생긴다. 행위를 변경해주기 위해서는 개발자가 직접 수정해주어야 하는데 이는 OCP를 위배하게 되는 요소중 하나이다. 이러한 문제들을 해결하는 방법 중 하나로 디자인 패턴 중 하나인 전략패턴을 제시해본다.
전략패턴을 사용하기 전 생기는 문제점들
자전거라는 예시를 통해서 한번 살펴보도록 하자.
자전거라는 클래스는 display 메서드를 통해서 이름을 말해주며, fuel 메서드를 통해서 움직이는데 필요한 에너지를 말해주는 역할을 수행한다.
public class Bicycle {
public void display() {
System.out.println("자전거 입니다.");
}
public void fuel() {
System.out.println("연료는 동력입니다.");
}
}
시간이 지나서, 전기 자전거가 발매되었다. 이렇게 되면 전기 자전거, 일반 자전거로 클래스를 분리하여서 관리해야 할 것이다.
public class ElectricBicycle {
public void display() {
System.out.println("전기 자전거 입니다.");
}
public void move() {
System.out.println("움직입니다.");
}
}
public class GenearlBicycle {
public void display() {
System.out.println("일반 자전거 입니다.");
}
public void move() {
System.out.println("움직입니다.");
}
}
시간이 더 지나서, 태양열로 움직이는 자전거를 발매했다고 가정하자. 그렇다면 또 SolorHeatBicycle를 만들어서 해당 객체를 관리해주어야 할 것이다. 결국 개발자가 신경써야 하는 관리 지점이 늘어나게 되는 것이다.
이러한 객체 관리 문제를 해결하기 위해서 상속을 사용해서 해결을 시도해 볼 수 있다.
public abstract class Bicycle {
public abstract void display();
public void move() {
System.out.println("움직입니다.");
}
}
public class ElectricBicycle extends Bicycle {
@Override
public void display() {
System.out.println("전기 자전거 입니다.");
}
}
public class GenearlBicycle extends Bicycle {
@Override
public void display() {
System.out.println("일반 자전거 입니다.");
}
}
이렇게 Bicycle을 기능들을 추상화하여, 각 Bicycle들을 좀 더 편하게 관리할 수 있게 된다. 하지만 상속을 사용한 방법 역시 몇가지 문제점이 존재한다.
1. 기능 확장의 어려움
만약 새로운 기능으로 `fule`라는 기능을 추가한다면 어떻게 될까?? fule라는 abstract 메서드를 정의하고 상속받는 모든 자식클래스에서 재정의를 해주어야 할 것이다.
public abstract class Bicycle {
public abstract void display();
public abstract void fuel();
public void move() {
System.out.println("움직입니다.");
}
}
public class ElectricBicycle extends Bicycle {
@Override
public void display() {
System.out.println("전기 자전거 입니다.");
}
@Override
public void fuel() {
System.out.println("연료는 전기입니다.");
}
}
public class GenearlBicycle extends Bicycle {
@Override
public void display() {
System.out.println("일반 자전거 입니다.");
}
@Override
public void fuel() {
System.out.println("연료는 동력입니다.");
}
}
2. OCP 위반
만약 move 메서드가 `빠르게 움직인다`는 기능이 업데이트 되면서 '움직인다'와 '빠르게 움직인다' 2개의 버전으로 관리된다면 어떻게 해야 할까? 기존에 사용하고 있는 모든 코드가 수정되야 할 것이다. 결국 이는 객체지향의 5대 원칙 중 OCP 원칙을 위반하게 된다.
전략패턴의 적용
전략 패턴을 이용하면 위에서 언급된 문제들을 제거할 수 있게 된다.
짧게 전략 패턴의 정의에 대해서 설명하고 넘어가자면, 전략 패턴은 디자인 패턴 중의 하나로 객체가 할 수 있는 행위를 각각의 전략으로 만들어 사용하고 이를 동적으로 수정이 가능하도록 설계된 패턴을 말한다.
코드를 통해서 한번 살펴보도록 하자.
public class Bicycle {
private MoveStrategy moveStrategy();
private FuleStrategy fuleStrategy();
public Bicycle(MoveStrategy moveStrategy, FuleStrategy fuleStrategy) {
this.moveStrategy = moveStrategy;
this.fuleStrategy = fuleStrategy;
}
public void move() {
moveStrategy.move();
}
public void fule() {
fuleStrategy.fule();
}
}
public interface MoveStrategy() {
void move();
}
public class General implements MoveStrategy {
@Override
public void move() {
System.out.println("움직입니다".);
}
}
public class Fast implements MoveStrategy {
@Override
public void move() {
System.out.println("더 빨리 움직입니다".);
}
}
public interface FuleStrategy() {
void fule();
}
public class General implements FuleStrategy {
@Override
public void fule() {
System.out.println("연료는 동력입니다.".);
}
}
public class Electric implements FuleStrategy {
@Override
public void fule() {
System.out.println("연료는 전기입니다.".);
}
}
public class SolarHeat implements FuleStrategy {
@Override
public void fule() {
System.out.println("연료는 태양열입니다.".);
}
}
이렇게 추후 상황에 따라 이동 정책이 변화 혹은 수정되거나, 자전거에 사용되는 연료가 추가되더라도 그에 대한 전략을 추상화하고 객체 컴파일 시점에 구체 클래스를 주입하여서 각 상황에 맞는 다양한 로직을 실행할 수 있게 된다.
public class Main {
public static void main(String[] args) {
Bicycle bicycle1 = new Bicycle(new General(), new Electric());
Bicycle bicycle2 = new Bicycle(new General(), new SolorHeat());
}
}
정리하자면, 전략 패턴은 동일한 계열의 역할(알고리즘)을 정의하고, 각 역할(알고리즘)을 캡슐화 한다고 할 수 있다.
이러한 위의 예시들은 상속 vs 조합의 관점에서도 접근 할 수 있는 내용이다. 상속 vs 조합에 대한 내용은 추후 포스팅을 통해서 다시 한번 자세히 정리할 생각이다.