👨🏼💻 템플릿 메서드 패턴은 전체적으로는 동일하면서 부분적으로는 다른 구문으로 구성된
메서드의 코드 중복을 최소화할 때 유용하다. 다른 관점에서 보면 동일한 기능을 상위
클래스에서 정의하면서 확장/변화가 필요한 부분만 서브 클래스에서 구현할 수 있도록
한다.
primitive 메서드는 hook 메서드라고 부르기도 한다.
엘리베이터 제어 시스템에서 모터를 구동시키는 기능을 생각해보자
HyundaiMotor
클래스 : 모터를 제어하여 엘리베이터를 이동시키는 클래스Door
클래스 : 문을 열거나 닫는 기능을 제공하는 클래스DoorStatus & MotorStatus
public enum DoorStatus {
CLOSED, OPENED
}
public enum MotorStatus {
MOVING, STOPPED
}
Door
public class Door {
private DoorStatus doorStatus;
public Door() {
doorStatus = DoorStatus.CLOSED;
}
public DoorStatus getDoorStatus() {
return doorStatus;
}
public void close() {
doorStatus = DoorStatus.CLOSED;
}
public void open() {
doorStatus = DoorStatus.OPENED;
}
}
HyundaiMotor
public class HyundaiMotor {
private Door door;
private MotorStatus motorStatus;
public HyundaiMotor(Door door) {
this.door = door;
motorStatus = MotorStatus.STOPPED;
}
private void moveHyundaiMotor(Direction direction) {
}
public MotorStatus getMotorStatus() {
return motorStatus;
}
private void setMotorStatus(MotorStatus motorStatus) {
this.motorStatus = motorStatus;
}
public void move(Direction direction) {
MotorStatus motorStatus = getMotorStatus();
if (motorStatus == MotorStatus.MOVING) {
return;
}
DoorStatus doorStatus = door.getDoorStatus();
if (doorStatus == DoorStatus.OPENED) {
door.close();
}
moveHyundaiMotor(direction);
setMotorStatus(MotorStatus.MOVING);
}
}
Client
public class Client {
public static void main(String[] args) {
Door door = new Door();
HyundaiMotor hyundaiMotor = new HyundaiMotor(door);
hyundaiMotor.move(Direction.UP);
}
}
Hyundai
클래스는 현대 모터를 구동시킨다. 만약 다른 회사의 모터를 제어해야 한다면? 예를 들어 LG 모터를 구동시키려면 어떻게 해야할까?LG 모터를 구동시키는 것은 현대 모터를 구동하는 것과 완전히 동일하지는 않다.
따라서 현대 모터의 코드를 복사해 일부분만 수정해야 한다.
LGMotor
public class LGMotor {
private Door door;
private MotorStatus motorStatus;
public LGMotor(Door door) {
this.door = door;
motorStatus = MotorStatus.STOPPED;
}
private void moveLGMotor(Direction direction) {
}
public MotorStatus getMotorStatus() {
return motorStatus;
}
private void setMotorStatus(MotorStatus motorStatus) {
this.motorStatus = motorStatus;
}
public void move(Direction direction) {
MotorStatus motorStatus = getMotorStatus();
if (motorStatus == MotorStatus.MOVING) {
return;
}
DoorStatus doorStatus = door.getDoorStatus();
if (doorStatus == DoorStatus.OPENED) {
door.close();
}
moveLGMotor(direction);
setMotorStatus(MotorStatus.MOVING);
}
}
그런데 현대 모터와 비교해보면 두 클래스는 많은 중복 코드를 가짐을 알 수 있다.
코드 중복은 유지보수성을 악화시키므로 바람직하지 않다. 이러한 코드 중복 문제는
다른 회사의 모터를 사용할 때마다 발생한다.
상속을 이용하여 코드 중복 문제를 해결해보자. 상위 클래스 Motor
정의한 설계는
다음과 같다.
Motor
public abstract class Motor {
protected Door door;
private MotorStatus motorStatus;
public Motor(Door door) {
this.door = door;
motorStatus = MotorStatus.STOPPED;
}
public MotorStatus getMotorStatus() {
return motorStatus;
}
protected void setMotorStatus(MotorStatus motorStatus) {
this.motorStatus = motorStatus;
}
}
일부 필드와 메서드의 중복을 해결했으나 여전히 두 클래스의 move
메서드 대부분이
비슷하게 구성된다.
move
메서드에서 각 모터를 움직이는 내부 메서드를 호출하는 부분을 제외하면
모터 구동을 실제로 구현한다는 기능 면에서는 두 클래스에서 동일하게 작동한다.
이런 경우 move
메서드를 상위 Motor
클래스로 이동시키고 다른 구문을
moveMotor
라는 메서드로 추출하여 하위 클래스에서 오버라이드하는 방식으로
코드 중복을 최소화할 수 있다.
moveMotor
메서드의 구현이 모터 제조사에 따라 달라야 하므로 Motor
클래스에서
추상 메서드로 정의한 후 각 하위 클래스에서 적절히 오버라이드되도록 한다.
Motor
public abstract class Motor {
private Door door;
private MotorStatus motorStatus;
public Motor(Door door) {
this.door = door;
motorStatus = MotorStatus.STOPPED;
}
public MotorStatus getMotorStatus() {
return motorStatus;
}
private void setMotorStatus(MotorStatus motorStatus) {
this.motorStatus = motorStatus;
}
public void move(Direction direction) {
MotorStatus motorStatus = getMotorStatus();
if (motorStatus == MotorStatus.MOVING) {
return;
}
DoorStatus doorStatus = door.getDoorStatus();
if (doorStatus == DoorStatus.OPENED) {
door.close();
}
moveMotor(direction);
setMotorStatus(MotorStatus.MOVING);
}
protected abstract void moveMotor(Direction direction);
}
HyundaiMotor
public class HyundaiMotor extends Motor {
public HyundaiMotor(Door door){
super(door);
}
protected void moveMotor(Direction direction) {
// HyundaiMotor 구동
}
}
LGMotor
public class LGMotor extends Motor {
public LGMotor(Door door) {
super(door);
}
protected void moveMotor(Direction direction) {
// LGMotor 구동
}
}
AbstractMap<K, V>
클래스에 정의되어 있는 get()
메서드를 이를 상속하는
HashMap, TreeMap
등 서브클래스에서 오버라이드하여 자신만의 구현 방법으로
재정의하고 있는 것을 볼 수 있다.
꼭 추상 메서드를 재정의하는 것이 아닌 일반 메서드도 템플릿에 고정되어 실행되는
것이라면 오버라이딩하여 알고리즘을 변경할 수 있다.
AbstractMap.get()
public V get(Object key) {
Iterator<Entry<K,V>> i = entrySet().iterator();
if (key==null) {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (e.getKey()==null)
return e.getValue();
}
} else {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (key.equals(e.getKey()))
return e.getValue();
}
}
return null;
}
HashMap.get()
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
TreeMap.get()
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
이외에 AbstractList
, AbstractSet
의 일반 메서드를 하위 클래스가 재정의하고 있다.
java.io.InputStream
, java.io.OutputStream
, java.io.Reader
의 일반 메서드를 하위 클래스가 재정의
javax.servlet.http.HttpServlet
의 모든 do...()
메서드는 기본적으로
응답에 HTTP 405 Not Allowed
리턴 코드를 보내기 때문에 이들을 상속하여
재정의하여 사용
알고리즘을 때에 따라 적용한다는 컨셉으로써, 둘이 공통점을 가지고 있다.
전략 및 템플릿 메서드 패턴을 OCP를 충족하고 코드를 변경하지 않고 쉽게 확장할 수
있도록 하는 데 사용할 수 있다.
전략 패턴은 합성을 통해 해결책을 강구하며, 템플릿 메서드 패턴은 상속을 통해
해결책을 제시한다.
따라서 템플릿 메서드 패턴이 전략 패턴에 비해 결합도가 더 높다.
전략 패턴에서는 대부분 인터페이스를 사용하지만, 템플릿 메서드 패턴서는 주로 추상
클래스나 구체적인 클래스를 사용한다.
전략 패턴에서는 전체 전략 알고리즘을 변경할 수 있지만, 템플릿 메서드 패턴에서는
알고리즘의 일부만 변경되고 나머지는 변경되지 않는 상태로 유지된다. (템플릿에 종속)
따라서 단일 상속만이 가능한 자바에서 상속 제한이 있는 템플릿 메서드 패턴보다는,
다양하게 많은 전략을 구현할 수 있는 전략 패턴이 협업에서 많이 사용되는 편이다.