[디자인패턴] 11. 템플릿 메서트 패턴 ( Template Method Pattern )

1. 템플릿 메서드 패턴이란?

템플릿 메서드 패턴(Template Method Pattern)은 전체적으로 동일하면서 부분적으로 다른 구문으로 구성된 메서드의 중복을 최소화할 때 유용한 패턴이다. 동일한 기능을 상위 클래스에서 정의하면서 확장/변화가 필요한 부분만 서브 클래스에서 구현할 수 있도록 한다.

전체적인 알고리즘 코드를 재사용하는데 유용한 패턴이다.

1.1 템플릿 메서드 패턴 콜라보레이션

template-method-pattern-collaboration

템플릿 메서드 패턴에서 나타나는 역할이 수행하는 작업은 다음과 같다.

1.2 템플릿 메서드 패턴 시퀀스 다이어그램

template-method-pattern-sequence-diagram

Client는 ConcreteClass객체의 templateMethod()를 호출한다. 실제로 templateMethod()는 AbstractClass에서 정의되었지만 ConcreteClass는 AbstractClass의 하위 클래스이기 때문에 Client가 호출할 수 있다. AbstractClass :: templateMethod()에서는 primitiveOperation1()과 primitiveOperation2()를 호출한다. 이 2개의 메서드는 ConcreteClass에서 오버라이드 된 것이다.

2. 템플릿 메서드 패턴 예제 : 엘리베이터 모터 구동 기능

2.1 현대 모터를 구동

template-method-pattern-class

엘리베이터 제어 시스템에서 현대 모터를 이용하여 구동시키는 기능을 구현한다면 위와 같이 클래스를 설계할 수 있다. move() 메서드를 실행할 때 안전을 위해 문(Door클래스)이 닫혀 있는지 조사할 필요가 있기 때문에 HyundaiMotor클래스에서 Door클래스로의 연관관계를 정의한다.

template-method-pattern-enum-class

위의 Enumeration는 각각의 상태를 나타내는 인터페이스들이다.

template-method-pattern-move-method

위는 HyundaiMotor 클래스의 move()메서드를 시퀀스 다이어그램으로 표현한 것이다.

  1. Client클래스에서 move()메서드 호출
  2. HyundaiMotor클래스는 getMotorStatus()메서드를 호출, 모터의 상태 조회하고, 이미 작동 중이면 move()메서드 실행 종료
  3. Door클래스의 getDoorStatus()메서드를 호출, 문의 상태 조회
  4. 문이 열려있는 상태면 close()메서드를 호출하여 문을 닫음
  5. moveHyundaiMotor()메서드를 호출, 모터 구동
  6. setMotorStatus()메서드 호출, 모터의 상태를 MOVING으로 변경

아래의 코드는 위의 설계를 바탕으로 작성한 것이다.

// 모터 상태
public enum MotorStatus {
    MOVING, STOPPED
}
// 문 상태
public enum  DoorStatus {
    CLOSED, OPENED
}
// 모터 구동 방향
public enum Direction {
    UP, DOWN
}
// 문 클래스
public class Door {

    // 문 상태 변수
    private DoorStatus doorStatus;

    // 생성자
    public Door() {
        this.doorStatus = DoorStatus.CLOSED;
    }
    
    public DoorStatus getDoorStatus() {
        return doorStatus;
    }

    // 닫기
    public void close() {
        doorStatus = DoorStatus.CLOSED;
    }

    // 열기
    public void open() {
        doorStatus = DoorStatus.OPENED;
    }

}
// 현대 모터 클래스
public class HyundaiMotor {

    private Door door;                  // 문
    private MotorStatus motorStatus;    // 모터 상태 변수

    // 생성자
    public HyundaiMotor(Door door) {
        this.door = door;
        this.motorStatus = MotorStatus.STOPPED;
    }

    // 현대 모터 구동
    private void moveHyundaiMotor(Direction direction) {
        // 현대 모터 구동
        System.out.println("현대 모터 구동 방향 : " + direction);
    }
    
    public MotorStatus getMotorStatus() {
        return motorStatus;
    }

    public void setMotorStatus(MotorStatus motorStatus) {
        this.motorStatus = motorStatus;
    }

    // 이동
    public void move(Direction direction) {

        MotorStatus motorStatus = getMotorStatus();

        // 모터가 구동 중이면 메서드 종료
        if (motorStatus == MotorStatus.MOVING) {
            return;
        }

        DoorStatus doorStatus = this.door.getDoorStatus();

        // 문이 열려있으면 문을 닫음
        if (doorStatus == DoorStatus.OPENED) {
            door.close();
        }

        moveHyundaiMotor(direction);        // 모터를 주어진 방향으로 작동
        setMotorStatus(MotorStatus.MOVING); // 모터 상태를 이동중으로 변경
    }
    
}
// 클라이언트 클래스
public class Client {
    public static void main(String[] args) {

        Door door = new Door(); // 문 객체 생성
        HyundaiMotor hyundaiMotor = new HyundaiMotor(door); // 현대 모터 객체 생성
        hyundaiMotor.move(Direction.UP);    // 현대 모터를 위로 구동

    }
}
현대 모터 구동 방향 : UP

2.2 문제점 : LG 모터를 추가할 경우

만약 다른 회사의 모터를 이용해 엘리베이터를 제어해야되는 기능을 추가한다면 아래와 같이 LG모터 클래스를 추가할 수 있다.

// LG 모터 클래스
public class LGMotor {

    private Door door;                  // 문
    private MotorStatus motorStatus;    // 모터 상태 변수

    // 생성자
    public LGMotor(Door door) {
        this.door = door;
        this.motorStatus = MotorStatus.STOPPED;
    }

    // LG 모터 구동
    private void moveLGMotor(Direction direction) {
        // LG 모터 구동
        System.out.println("LG 모터 구동 방향 : " +  direction);
    }

    public MotorStatus getMotorStatus() {
        return motorStatus;
    }

    public void setMotorStatus(MotorStatus motorStatus) {
        this.motorStatus = motorStatus;
    }

    // 이동
    public void move(Direction direction) {

        MotorStatus motorStatus = getMotorStatus();

        // 모터가 구동 중이면 메서드 종료
        if (motorStatus == MotorStatus.MOVING) {
            return;
        }

        DoorStatus doorStatus = this.door.getDoorStatus();

        // 문이 열려있으면 문을 닫음
        if (doorStatus == DoorStatus.OPENED) {
            door.close();
        }

        moveLGMotor(direction);             // 모터를 주어진 방향으로 작동
        setMotorStatus(MotorStatus.MOVING); // 모터 상태를 이동중으로 변경

    }
}
// LG모터를 추가한 클라이언트 클래스
public class Client {
    public static void main(String[] args) {

        Door door = new Door(); // 문 객체 생성
        HyundaiMotor hyundaiMotor = new HyundaiMotor(door); // 현대 모터 객체 생성
        hyundaiMotor.move(Direction.UP);    // 현대 모터를 위로 구동
        
        LGMotor lgMotor = new LGMotor(door); // LG 모터 객체 생성
        lgMotor.move(Direction.DOWN);   // LG 모터를 아래로 구동
    }
}
현대 모터 구동 방향 : UP
LG 모터 구동 방향 : DOWN

LGMotor클래스와 HyundaiMotor클래스를 비교해보면 많은 중복코드를 가지고 있다. 일반적으로 코드의 중복은 유지보수성을 악화시키므로 바람직하지 않다. 이러한 코드의 중복 문제는 다른 회사의 모터를 추가하여 사용할 때마다 발생하게 된다.

2개 이상의 클래스가 유사한 기능을 제공하면서 중복된 코드가 있는 경우 아래와 같이 상속을 이용해 중복 문제를 해결할 수 있다.

template-method-pattern-motor-class-diagram3

아래의 코드는 위의 설계를 바탕으로 구현한 코드이다.

// HyundaiMotor, LGMotor 공통적인 기능을 구현하는 클래스
public abstract class Motor {

    protected Door door;                // 문
    private MotorStatus motorStatus;    // 모터 상태 변수

    // 생성자
    public Motor(Door door) {
        this.door = door;
        this.motorStatus = MotorStatus.STOPPED;
    }

    public MotorStatus getMotorStatus() {
        return motorStatus;
    }

    public void setMotorStatus(MotorStatus motorStatus) {
        this.motorStatus = motorStatus;
    }

}
// 모터 추상 클래스를 상속받은 현대 모터 클래스
public class HyundaiMotor extends Motor {

    public HyundaiMotor(Door door) {
        super(door);
    }

    // 현대 모터 구동 메서드
    private void moveHyundaiMotor(Direction direction) {
        // 현대 모터 구동
        System.out.println("현대 모터 구동 방향 : " + direction);
    }

    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);

    }

}
// 모터 추상 클래스를 상속받은 LG 모터 클래스
public class LGMotor extends Motor {

    public LGMotor(Door door) {
        super(door);
    }
    
    // LG 모터 구동 클래스
    private void moveLGMotor(Direction direction) {
        // LG 모터 구동
        System.out.println("LG 모터 구동 방향 " + direction);
    }

    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);
    }

}
// 클라이언트
public class Client {
    public static void main(String[] args) {

        Door door = new Door();

        HyundaiMotor hyundaiMotor = new HyundaiMotor(door);
        hyundaiMotor.move(Direction.DOWN);

        LGMotor lgMotor = new LGMotor(door);
        lgMotor.move(Direction.UP);

    }
}
현대 모터 구동 방향 : DOWN
LG 모터 구동 방향 UP

Motor클래스를 HyundaiMotor, LGMotor클래스의 상위 클래스로 정의함으로써 이전의 코드 중복을 피할 수 있었다. 하지만 move()메서드는 여전히 부분적으로 코드 중복 문제가 있는데 탬플릿 메서드 패턴을 통해 해결해보자.

2.3 해결책 : 템플릿 메서드 패턴 적용

위에서 살펴본 move()메서드의 부분적인 중복문제는 상속을 활용하여 해결할 수 있다. moveHyundaiMotor()moveLGMotor() 메서드를 호출하는 부분을 제외하면 두 클래스의 메서드는 동일하다. 그리고 실제로 모터를 구동한다는 기능면에서 동일하다.

이러한 경우 move()메서드를 상위 Motor클래스로 이동시키고 moveHyundaiMotor()moveLGMotor()메서드의 호출 부분을 하위 클래스에서 오버라이드하는 방식으로 처리하면 move()메서드의 중복을 최소화 할 수 있다.

template-method-pattern4

위의 설계에서 볼 수 있듯이 두 클래스의 move()메서드에서 다른 부분은 moveMotor()메서드로 대체하였다. moveMotor()메서드의 구현이 HyundaiMotor클래스와 LGMotor클래스에 따라 달라야 하므로 moveMotor() 메서드는 Motor클래스에서 추상메서드로 정의한 후 각 하위 클래스에서 오버라이드되도록 한다.

아래는 위의 설계에 따라 코드의 중복을 최소한으로 줄여 작성된 내용이다.

// 모터 클래스
public abstract class Motor {

    private Door door;                  // 문
    private MotorStatus motorStatus;    // 모터 상태 변수

    // 생성자
    public Motor(Door door) {
        this.door = door;
        this.motorStatus = MotorStatus.STOPPED;
    }

    public MotorStatus getMotorStatus() {
        return motorStatus;
    }

    public 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);
}
// 현대 모터 클래스 : 모터 클래스 상속
public class HyundaiMotor extends Motor {

    public HyundaiMotor(Door door) {
        super(door);
    }
    
    // 현대 모터에 맞게 구동 메서드 오버라이드
    @Override
    protected void moveMotor(Direction direction) {
        // 현대 모터 구동
        System.out.println("현대 모터 구동 방향 : " + direction);
    }
}
// LG 모터 클래스 : 모터 클래스 상속
public class LGMotor extends Motor {

    public LGMotor(Door door) {
        super(door);
    }

    // LG 모터에 맞게 구동 메서드 오버라이드
    @Override
    protected void moveMotor(Direction direction) {
        // LG 모터 구동
        System.out.println("LG 모터 구동 방향 : " + direction);
    }
}
// 클라이언트
public class Client {

    public static void main(String[] args) {

        Door door = new Door();

        HyundaiMotor hyundaiMotor = new HyundaiMotor(door);
        hyundaiMotor.move(Direction.DOWN);

        LGMotor lgMotor = new LGMotor(door);
        lgMotor.move(Direction.UP);

    }

}
현대 모터 구동 방향 : DOWN
LG 모터 구동 방향 UP

이전의 코드에 비해 중복된 코드가 줄었고, 훨씬 더 간결해진 것을 알 수 있다. 최종적으로 위 예제를 템플릿 메서드 패턴을 적용해 각각의 역할을 정리해보면 아래와 같다.

template-method-pattern-motor-class-diagram5