전략 패턴 (Strategy Pattern)
서로 다른 정책이 한 코드에 섞여있는 경우 (한 메서드에) 전략 패턴을 적용할 수 있다. 전략 패턴은 특정 콘텍스트에서 알고리즘(전략)을 별도로 분리하는 설계 방법이다. 여기서 콘텍스트와 전략에 대해서 알아보자.
먼저, 콘텍스트는 핵심 기능 자체의 책임을 갖고 있는 부분을 의미한다.
예를 들어 계산 정책에 있어 첫 손님에게는 50% 할인, 남은 과일을 가져가는 손님에게는 30% 할인이 들어간다고 할 때, 여기서 핵심적인 부분은 어쨌든 계산이다. 어떤 할인 정책을 사용하는냐는 방법의 문제이고, 우리는 그 방법을 적용해 계산을 해야한다. 따라서 위 예시에서 콘텍스트는 계산 기능이 될 것이다.
다음으로 전략이란 공통된 부분을 추상화하고 있는 부분이다. 위 예시에서 우리는 DiscountStrategy라는 인터페이스를 만들고, 이 인터페이스를 확장한 EarlyCustomerDiscountStrategy와 LastProductDiscountStrategy를 만들 수 있다. (명명은 셀프로 했더니 구린 듯 하다.) 즉, 여기서 DiscountStrategy는 첫 손님 할인 정책과 남은 과일 가져가는 손님에게 적용되는 할인 정책의 공통 부분이라 볼 수 있고, 이를 우리는 전략이라고 표현할 수 있다.
설계를 하다보면, 콘텍스트의 클라이언트가 전략의 인터페이스가 아닌 상세 구현을 알게 되는 문제가 발생할 수 있다. 예를 들어, 첫 손님 할인 관련 메서드 내부에서 첫 손님 할인 클래스를 특정하여 생성할 수 있기 때문이다. 하지만 이는 전략의 Concrete class와 클라이언트의 코드가 쌍을 이룬다면 유지보수 문제가 발생할 일이 적고, 코드의 이해를 높일 수 있으며 코드의 응집도를 높일 수 있기 때문에 지양해야하는 방식은 아니다.
우리는 전략패턴을 이용하여 정책(추가 기능)에는 열려있고 변경에는 닫혀있도록 설계할 수 있다.
템플릿 메서드 패턴 (Template Method Pattern)
코드를 작성하다 보면, 실행 과정/단계는 동일한데 각 단계 중 일부의 구현이 서로 다른 경우가 발생하게 된다. 이 경우 우리는 템플릿 메서드 패턴을 적용할 수 있다. 템플릿 메서드 패턴은 다음과 같은 두 가지로 구성된다.
1. 실행 과정을 구현한 상위 클래스
2. 실행 과정의 일부 단계를 구현한 하위 클래스
우선 상위 클래스는 실행 과정을 구현한 메서드를 제공한다. 이 메서드는 기능을 구현하는 데 필요한 각 단계를 정의하며, 이 중 일부 단계는 추상 메서드를 호출하는 방식으로 구현한다. 이 때의 추상 메서드는 위에서 말한 구현이 다른 단계를 의미한다.
템플릿 메서드 패턴을 적용할 추상화된 상위 클래스를 만든다. 이 때 사용될 템플릿 메서드인 코드에서 공통된 부분은 상위 클래스에서 미리 구현한다. 그리고 하위 클래스에서는 상황에 맞게 필요한 메서드를 알맞게 정의해주어 사용하면 된다.
템플릿 메서드 패턴을 사용하게 되면 동일한 실행 과정의 구현을 제공하면서 동시에 하위 타입에서 일부 단계를 구현하도록 할 수 있는데, 이는 코드의 중복을 막을 수 있다. 또한 상위 클래스가 흐름을 제어하게 된다. 상위 클래스에서 제공될 템플릿 메서드의 경우에는 외부에 제공될 기능이기에 public으로 선언하여 구현하고, 템플릿 메서드 내에서 사용될 메서드들은 하위 타입에서 재정의하여 사용할 수 있도록 private이 아닌 protected로 제공한다. 하지만 상위 클래스에서의 템플릿 메서드를 무조건 구현해야하는 것은 아니다. 추상 메서드로 정의한 뒤 하위 클래스에서 알맞게 재정의하도록 구현할 수도 있다.
템플릿 메서드는 상속을 기반으로 설계되지만, 앞선 상속과 조합을 비교했을 때 상속을 사용하는 경우 불필요한 클래스가 증가할 수 있는 문제점이 발생한다. 따라서 우리는 템플릿 메서드와 전략 패턴을 조합하여 메서드의 인자로서 주입(조립/위임)하여 사용할 수 있다.
상태 패턴 (State Pattern)
만약, 상태에 따라 동일한 기능 요청의 처리를 다르게 한다면 상태 패턴을 적용하여 분리할 수 있다. 상태 패턴에서는 상태를 별도 타입으로 분리하고 각 상태별로 알맞은 하위 타입을 구현하도록 설계한다. 상태 패턴에서의 중요한 점은 상태 객체가 기능을 제공한다는 점이다. 우리는 State라는 상위 인터페이스를 두고, State 내부에 각 상태마다 동일하게 적용되는 메서드를 포함시키도록 할 수 있다.
별도 타입으로 분리한 상태는 콘텍스트에서 필드로서 사용할 수 있다.
public class StatePattern {
private State state;
}
이후, 각 상태에 알맞은 메서드를 실행하기 위해서는 state.select(); 와 같이 상태 내부에 구현된 메서드를 실행시키므로서 구현할 수 있다. 이번 미션이었던 블랙잭에서도 Blackjack, Hit, Stay, Bust 모두 earningRate() 라는 메서드를 포함하고 있기 때문에, state.earningRate()를 호출하면 각 상태에 알맞은 earningRate를 불러올 수 있다.
상태 패턴을 적용한다면 상태 별 동작 코드를 상태에게 위임할 수 있다. (즉, 상태별로 다르게 동작하도록 상태 내부에서 구현할 수 있다. 예를 들어 블랙잭은 earningRate가 1.5고, Stay는 1이라면 내부 earningRate() 함수 리턴 값을 다르게 설정해줌으로서 구현할 수 있다.)
데코레이터 패턴 (Decorater Pattern)
데코레이터 패턴은 상속이 아닌 위임을 통해 클래스를 확장해나갈 수 있도록 한다. 특정 상위 클래스의 하위 클래스로서 구현체를 설계하고, Decorater라는 추상 클래스를 설계한 뒤 데코레이터의 하위 클래스를 만들어나가며 확장할 수 있다. 이 말만 들었을 때엔 상속과 어떤 점이 다른지 이해가 잘 안되어 책의 예제를 발췌한다.
public abstract class Decorator implements FileOut {
private final FileOut delegate; //위임 대상
public Decorater(FileOut delegate) {
this.delegate = delegate;
}
protected void doDelegate(byte[] data) {
delegate.write(data); // delegate에 쓰기를 위임한다.
}
}
위와 같이 구현된 데코레이터 클래스를 상속받는 하위 클래스들은 자신의 기능을 수행한 뒤에 상위 클래스의 doDelegate() 메서드를 이용해 파일 쓰기를 위임하도록 구현한다. 예를 들어 데코레이터 클래스의 하위 클래스인 EncryptionOut 클래스는 다음과 같이 구현할 수 있다.
public class EncryptionOut extends Decorator {
public EncryptionOut(FileOut delegate) {
super(delegate);
}
public void write(Byte[] data) {
byte[] encryptedData = encrypt(data);
super.doDelegate(encryptedData);
}
...
}
EncryptionOut 클래스의 write() 메서드는 파일에 쓸 데이터를 암호화해서 delegate로 전달한다.
이제 파일에 데이터를 암호화해서 쓰는 기능이 필요한 곳의 코드는 다음과 같이 작성될 수 있다.
FileOut delegate = new FileOutImpl(); // 구현체
FileOut fileOut = new EncryptionOut(delegate);
fileOut.write(data);
데코레이터 패턴을 사용하면 각 확장 기능들의 구현이 별도의 클래스로 분리되기에 각 확장 기능 및 원래 기능을 서로 영향 없이 변경할 수 있도록 만들어준다. 따라서 단일 책임 원칙을 지킬 수 있도록 만들어준다.