Design Pattern

[Design Pattern] 변하는 코드 분리(2) Strategy Pattern

유자맛바나나 2022. 11. 21. 00:09

❑ 패턴 소개

1) GoF 패턴 분류

행위 패턴(Behavioral Pattern)

 

[참고] GoF Patterns

더보기
생성 패턴 구조 패턴 행위 패턴
- 객체를 생성하는 것과 관련된 패턴
- 객체의 생성과 변경이 전체 시스템에 미치는 영향을 최소화하고 코드의 유연성을 높임
- 큰 구조를 형성하기 위해 클래스, 객체들을 어떻게 구성하고 합성할지 정하는데 활용할수 있는 패턴화한 것
- 복잡한 구조의 개발과 유지보수를 쉽게 만듦
- 반복적으로 사용되는 객체들의 상호작용을 패턴화한 것
❑ Factory Method
❑ Abstract Factory
❑ Builder
❑ Prototype
❑ Singleton
❑ Adapter
❑ Bridge
❑ Composite
❑ Decorator
❑ Facade
❑ Flyweight
❑ Proxy
❑ Chain of Responsibility
❑ Command
❑ Iterator
❑ Interpreter
❑ Mediator
❑ Memento
❑ Observer
❑ State
❑ Strategy
❑ Template Method
❑ Visitor

 

2) 패턴의 의도

  • 로직에서 변하는 코드를 찾아 '외부 클래스'로 분리한다. 변하는 코드를 찾아 분리 하는 것이 핵심이다.
  • 변하는 코드를 분리하지 않은 채 개발한다면 새로운 조건이 추가될 때 마다 기존의 코드를 수정해야하기 때문에 OCP(Open-Closed Pricipal)를 위배하게 된다.

 

 예제

  • 자동차의 동력 장치(PowerUnit)에 시동을 거는 기능 startEngine()을 만들고자 한다.
  • 자동차의 동력 장치는 가솔린 엔진이 될 수도 있고, 디젤 엔진이 될 수도 있다. 따라서 가솔린과 디젤 별로 시동될 수 있는 조건을 만족하는지 확인하는 로직(checkConditionForStart)이 필요하다.

 

[Bad Design] 조건문 이용

1) 구현

  • Bad Case 예제는 템플릿 메서드 패턴의 것과 완전히 동일하다
  • "가솔린 엔진이냐, 디젤 엔진이냐"를 구분할 때 가장 직관적이고 쉽게 떠올리는 방법은 if, else 문이다.
  • 조건문을 이용해 코드를 구현해보고 무엇이 문제인지 파악해본다

 

PowerUnitType.java

public enum PowerUnitType {
    GASOLINE, DIESEL
}
  • 동력 장치는 가솔린과 디젤 두 가지가 있다. enum을 이용해 타입을 구분해준다.

 

PowerUnit.java

public class PowerUnit {

    protected PowerUnitType powerType;
    private static final String ENGINE_ON = "ON";
    private static final String ENGINE_OFF = "OFF";
    private String engineStatus = ENGINE_OFF;

    public PowerUnit(PowerUnitType powerType) {
        this.powerType = powerType;
    }

    public void startEngine(PowerUnitType powerType) {
        if (checkConditionForStart(powerType)) {
            engineStatus = ENGINE_ON;
        }
    }

    public boolean checkConditionForStart(PowerUnitType powerType) {
        boolean result = false;
        if (powerType == PowerUnitType.GASOLINE) {
            System.out.println("Check condition for stating GASOLINE Engine");
            // ..
            result = true;
        } else if (powerType == PowerUnitType.DIESEL) {
            System.out.println("Check condition for stating DIESEL Engine");
            // ..
            result = true;
        }
        return result;
    }
}
  • 살펴볼 부분은 checkConditionForStart() 이다. if, else if 를 이용해 powerType을 구분하고, 각 powerType 별 엔진 시동을 위한 컨디션을 체크하게 된다.

 

2) 요구사항의 변경: 전기차가 추가된다면?

  • 조건문을 이용해 구현했을 때 요구사항이 변경되어 전기차가 추가될 경우 문제점이 무엇인지 파악해본다

PowerUnit.java

public class PowerUnit {

    protected PowerUnitType powerType;
    private static final String ENGINE_ON = "ON";
    private static final String ENGINE_OFF = "OFF";
    private String engineStatus = ENGINE_OFF;

    public PowerUnit(PowerUnitType powerType) {
        this.powerType = powerType;
    }

    public void startEngine(PowerUnitType powerType) {
        if (checkConditionForStart(powerType)) {
            engineStatus = ENGINE_ON;
        }
    }

    public boolean checkConditionForStart(PowerUnitType engineType) {
        boolean result = false;
        if (engineType == PowerUnitType.GASOLINE) {
            System.out.println("Check condition for stating GASOLINE Engine");
            // ..
            result = true;
        } else if (engineType == PowerUnitType.DIESEL) {
            System.out.println("Check condition for stating DIESEL Engine");
            // ..
            result = true;
        } else if (engineType == PowerUnitType.ELECTRIC) { // 전기차 조건 추가
            System.out.println("Check condition for stating DIESEL Engine");
            // ..
            result = true;
        }
        return result;
    }
}
  • checkConditionForStart()에서 else if(engineType == PowerUnitType.) { } 추가해 전기차 로직을 처리해야 한다.
  • 즉, 기존 클래스인 PowerUnit 를 수정한 것이다. 기존 클래스가 수정된 것은 "기능 추가에는 열려있고, 수정에 대해서는 닫혀 있어야 한다"는 OCP를 위반한 것이다.

 

그럼 어떻게 작성하는게 좋은 디자인일까?
변하는 코드를 분리하자!

어디로? 인터페이스를 활용해 외부 클래스로!

 

 

 

 

 

[Good Design] 인터페이스를 활용해 '외부클래스'로 분리(Strategy)

1) 구현

  • 변하는 코드인 checkConditionForStart() 메서드를 제공하는 인터페이스 PowerConditionChecker를 만들고 GasolineConditionChecker, DieselConditionChecker에서 구현하도록 한다.
  • PowerUnit의 생성자를 수정해 PowerConditionChecker를 외부에서 주입받을 수 있도록 한다.
  • startEngine에서는 외부에서 주입받은 checker를 이용하도록 변경한다.

 

PowerUnit.java

public class PowerUnit {

    private PowerUnitType powerType;
    private PowerConditionChecker checker; // 외부 클래스인 PowerConditionChecker 도입
    private static final String ENGINE_ON = "ON";
    private static final String ENGINE_OFF = "OFF";
    private String engineStatus = ENGINE_OFF;

    public PowerUnit(PowerUnitType powerType, PowerUnitConditionChecker checker) {
        this.powerType = powerType;
        this.checker = checker; // 외부로부터 PowerConditionChecker를 주입 받음
    }

    public void startEngine(PowerUnitType powerType) {
        // 외부에서 주입받은 checker를 이용한다.
        if (checker.checkConditionForStart(powerType)) {
            engineStatus = ENGINE_ON;
        }
    }
}

 

PowerConditionChecker.java (추가)

public interface PowerConditionChecker {

    boolean checkConditionForStart(PowerUnitType powerType);
}

 

GasolineConditionChecker.java (추가)

public class GasolineConditionChecker implements PowerConditionChecker {

    @Override
    public boolean checkConditionForStart(PowerUnitType powerType) {
        boolean result = false;
        if (powerType == PowerUnitType.GASOLINE) {
            System.out.println("Check condition for stating GASOLINE Engine");
            // ..
            result = true;
        }
        return result;
    }
}

 

DieselConditionChecker.java (추가)

public class DieselConditionChecker implements PowerConditionChecker {

    @Override
    public boolean checkConditionForStart(PowerUnitType powerType) {
        boolean result = false;
        if (powerType == PowerUnitType.GASOLINE) {
            System.out.println("Check condition for stating GASOLINE Engine");
            // ..
            result = true;
        }
        return result;
    }
}

 

2) 요구사항의 변경: 전기차가 추가된다면?

  • 전략 패턴을 활용해 구현했을 때 요구사항이 변경되어 전기차가 추가될 경우 장점이 무엇인지 파악해본다

ElectricConditionChecker.java (추가)

public class ElectricConditionChecker implements PowerConditionChecker {

    @Override
    public boolean checkConditionForStart(PowerUnitType powerType) {
        boolean result = false;
        if (powerType == PowerUnitType.ELECTRIC) {
            System.out.println("Check condition for stating ELECTRIC Motor");
            // ..
            result = true;
        }
        return result;
    }
}
  • 전기차 조건을 체크하기 위한 새로운 클래스 ElectricConditionChecker가 추가되었다. 기존 코드에는 아무런 변경사항도 없고, 새로운 기능이 추가되었으므로 OCP를 만족한다.

 

 

❑ 더 살펴보기

1) Class Diagram

Standard Example

 

2) 패턴의 특징

  • (Good) 정책의 분리: 재사용성
    PowerUnit과 정책(ConditionChecker)이 분리되어 있다. 따라서, 다른 클래스에서 ConditionChecker가 필요할 경우 사용 가능하므로 재사용성이란 장점이 있다.
  • (Good) 런타임(실행시간)에 정책 변경 가능
    외부에서 정책을 주입받기 때문에 런타임에 정책 변경이 가능하다. 본 포스팅의 예제에서 런타임에 ConditionChecker를 변경할 수 있다면 어떤게 좋을까? 예를 들어 자동차 게임을 만든다면, 게임 실행 중(=런타임에) 유저가 자동차 모델을 선택했을 때 가솔린과 디젤 별로 Checker를 주입할 수 있도록 변경할 수 있는것과 같다.

 

3) 다른 패턴과 비교하기

3-1) Template Method Pattern(템플릿 메서드 패턴)

  • '로직에서 변하는 코드를 찾아 분리한다'는 점에서 기본적으로 의도가 동일하다.
  • 하지만 변하는 코드를 다른 클래스로 분리 하는 전략 패턴과 달리, 템플릿 메서드 패턴은 변하는 코드를 상속을 이용해 추상 메서드로 분리하는 점에서 차이가 있다
  • 전략 패턴은 정책을 외부 클래스로 분리하고 의존성을 주입받는 것으로 설계하기 때문에 런타임 시 정책변경이 가능하다. 따라서 해당 정책을 다른 클래스에도 주입해서 사용할 수 있는 재사용성을 갖고 있다.
  Strategy Template Method
런타임 중 정책 변경 가능 불가능
재사용성

Strategy vs Template Method

 

  • 전략패턴은 템플릿 메서드 패턴보다 무조건 좋은가?
    • 런타임 중 정책 변경, 재사용성을 봤을 때 전략패턴이 유연한 설계가 가능해 무조건 좋아보인다. 그렇다면 언제나 전략패턴이 좋을까?
    • 거꾸로 생각해 런타임 정책 변경을 불가능하게 하고, 정책을 타 객체에서 재사용하지 못하지 못하게하고 싶을 때 템플릿 메서드 패턴이 좋다. 
    • 전략 패턴을 쓰면 해당 전략을 다른 객체가 사용할 수 있도록 Open하는 것과 마찬가지다. 하지만 템플릿 메서드 패턴을 사용하면 다른 객체가 사용하는 것을 막을 수 있다. 
    • 또한 개발을 하다보면 몇몇 정책들은 정책이 한번 정해지면 런타임 중 변경될 여지가 전혀 없는 경우가 있다. 예를 들어, 스타크래프트라는 게임을 생각했을 때 처음 종족을 선택(=객체의 생성)하고 나면 게임 중에 종족을 변경할 일이 없다. 이럴 때는 템플릿 메서드 패턴을 적용하는게 더 명확할 수 있다.