JUINTINATION

템플릿 메서드(Template Method) 패턴 본문

JAVA객체지향디자인패턴

템플릿 메서드(Template Method) 패턴

DEOKJAE KWON 2023. 7. 16. 02:34
반응형

탬플릿 메서드 패턴이란?

전체적으로 동일하면서 부분적으로 상이한 문장을 가지는 메소드의 코드 중복을 최소화하기 위해 상이한 부분은 하위 클래스에서 구현할 수 있도록 해주는 디자인패턴이다.

다음과 같이 엘레베이터 제어 시스템에서의 모터 관련 설계가 있다고 하자.

enum DoorStatus {
    CLOSED,
    OPENED
}

enum MotorStatus {
    MOVING,
    STOPPED
}

enum Direction {
    UP,
    DOWN
}

class Door {
    private DoorStatus doorStatus;
    public Door() {
        doorStatus = DoorStatus.CLOSED;
    }
    public DoorStatus getDoorStatus() {
        return doorStatus;
    }
    public void open() {
        doorStatus = DoorStatus.OPENED;
    }
    public void close() {
        doorStatus = DoorStatus.CLOSED;
    }
}

class HyundaiMotor {
    private Door door;
    private MotorStatus motorStatus;
    public HyundaiMotor(Door door) {
        this.door = door;
        motorStatus = MotorStatus.STOPPED;
    }
    public void move(Direction direction) {
        if (getMotorStatus() == MotorStatus.MOVING) return;
        if (door.getDoorStatus() == DoorStatus.OPENED) door.close();
        moveHyundaiMotor(direction);
        setMotorStatus(MotorStatus.MOVING);
    }
    public void moveHyundaiMotor(Direction direction) {
        // HyundaiMotor 구동
    }
    public MotorStatus getMotorStatus() {
        return motorStatus;
    }
    private void setMotorStatus(MotorStatus motorStatus) {
        this.motorStatus = motorStatus;
    }
}
  • 위 코드의 문제점
    • 현대가 아닌 다른 회사의 모터를 제어해야 한다면 중복되는 기능을 똑같이 중복되게 작성하여 각각의 클래스를 만들어야 한다.
    • 예를 들어 LG 모터를 사용했을 때 작성할 코드는 다음과 같다.
      class LGMotor {
          private Door door;
          private MotorStatus motorStatus;
          public LGMotor(Door door) {
              this.door = door;
              motorStatus = MotorStatus.STOPPED;
          }
          public void move(Direction direction) {
              if (getMotorStatus() == MotorStatus.MOVING) return;
              if (door.getDoorStatus() == DoorStatus.OPENED) door.close();
              moveLGMotor(direction); // 이 부분을 제외하면 HyundaiMotor와 동일하다
              setMotorStatus(MotorStatus.MOVING);
          }
          public void moveLGMotor(Direction direction) {
              // LGMotor 구동
          }
          public MotorStatus getMotorStatus() {
              return motorStatus;
          }
          private void setMotorStatus(MotorStatus motorStatus) {
              this.motorStatus = motorStatus;
          }
      }

하지만 마찬가지로 move 메서드에 공통된 기능을 중복되게 작성하는 문제가 발생한다. 이 문제를 HyundaiMotor와 LGMotor 클래스의 상위 클래스 Motor를 정의하도록 설계하면 다음과 같다.

class Door {
    private DoorStatus doorStatus;
    public Door() {
        doorStatus = DoorStatus.CLOSED;
    }
    public DoorStatus getDoorStatus() {
        return doorStatus;
    }
    public void open() {
        doorStatus = DoorStatus.OPENED;
    }
    public void close() {
        doorStatus = DoorStatus.CLOSED;
    }
}

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

class HyundaiMotor extends Motor {
    public HyundaiMotor(Door door) {
        super(door);
    }
    public void move(Direction direction) {
        if (getMotorStatus() == MotorStatus.MOVING) return;
        if (door.getDoorStatus() == DoorStatus.OPENED) door.close();
        moveHyundaiMotor(direction); // 마찬가지로 해당 코드를 제외하고 LGMotor 클래스의 move 메서드와 동일하다.
        setMotorStatus(MotorStatus.MOVING);
    }
    public void moveHyundaiMotor(Direction direction) {
        // HyundaiMotor 구동
    }
}

class LGMotor extends Motor {
    public LGMotor(Door door) {
        super(door);
    }
    public void move(Direction direction) {
        if (getMotorStatus() == MotorStatus.MOVING) return;
        if (door.getDoorStatus() == DoorStatus.OPENED) door.close();
        moveLGMotor(direction); // 마찬가지로 해당 코드를 제외하고 HyundaiMotor 클래스의 move 메서드와 동일하다.
        setMotorStatus(MotorStatus.MOVING);
    }
    public void moveLGMotor(Direction direction) {
        // LGMotor 구동
    }
}

이를 해결하기 위해 다음과 같이 공통적인 부분을 상위 클래스인 Motor로 이동했다.

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;
    }
    public void move(Direction direction) {
        if (getMotorStatus() == MotorStatus.MOVING) return;
        if (door.getDoorStatus() == DoorStatus.OPENED) door.close();
        moveMotor(direction);
        setMotorStatus(MotorStatus.MOVING);
    }
    protected abstract void moveMotor(Direction direction);
}

class HyundaiMotor extends Motor {
    public HyundaiMotor(Door door) {
        super(door);
    }
    @Override
    protected void moveMotor(Direction direction) {
        // HyundaiMotor 구동
    }
}

class LGMotor extends Motor {
    public LGMotor(Door door) {
        super(door);
    }
    @Override
    protected void moveMotor(Direction direction) {
        // LGMotor 구동
    }
}

위 코드에서 move 메서드를 Template 메서드라고 하며 moveMotor 메서드를 Primitive 메서드 또는 Hook 메서드라고 한다. Template 메서드는 공통 코드와 하위 클래스에서 구현될 추상 메서드인 Primitive 메서드를 포함한다.

템플릿 메서드 패턴 컬레보레이션

 

AbstractClass : 템플릿 메서드를 정의하는 클래스, 하위 클래스의 공통 알고리즘과 하위 클래스에서 구현될 기능을 primitiveMethod로 정의한다.

ConcreteClass : 상위 클래스에 구현된 템플릿 메서드의 일반적인 알고리즘에서 하위 클래스에 적합하게 primitiveMethod를 재정의하는 클래스


데커레이터 패턴을 이용한 간단한 예제

다음은 자동 세차기 시스템의 요구사항이다. 요구사항을 참고해 템플릿 메서드 패턴을 사용하는 클래스 다이어그램과 소스코드를 작성하라.

  • 자동 세차기는 대형차, 소형차, 스포츠카 옵션을 고를 수 있다.
  • 세차 방법은 대체적으로 유사하나, 차종에 따라 세차 방법이 조금씩 다르다.
  • 세 옵션의 공통적인 세차 방법으로는 차 이동시키기, 브러쉬 세척하기, 말리기가 있다.
  • 대형차 세차 방법의 특징으로는 브러쉬 간격 매우 넓게 벌리기, 세제 많이 분사하기, 물 많이 분사하기가 있다.
  • 소형차 세차 방법의 특징으로는 브러쉬 간격 일반 넓이로 벌리기, 세제 일반 분사하기, 물 일반 분사하기가 있다.
  • 스포츠카 세차 방법의 특징으로는 브러쉬 간격 좁게 벌리기, 세제 일반 분사하기, 물 일반 분사하기가 있다.
  • 세차 옵션은 추후 다른 옵션이 추가될 가능성이 있다.
  • 세차 순서는 다음과 같다.
세차 순서
 1. 차 이동시키기
 2. 브러쉬 간격 벌리기
 3. 세제 분사하기
 4. 브러쉬 세척하기
 5. 물 분사하기
 6. 말리기

 

  • 구현 예시
옵션을 선택하세요(1:대형차, 2:소형차, 3:스포츠카) : 1
대형차 세차를 진행합니다.
차를 이동시킵니다.
브러쉬 간격을 매우 넓게 벌립니다.
세제를 많이 분사합니다.
브러쉬 세척을 진행합니다.
물을 많이 분사합니다.
말리기 과정을 진행합니다.
세차가 완료되었습니다.

 

클래스 다이어그램

소스코드

import java.util.Scanner;

abstract class CarWash {
    public void washCar() {
        System.out.println("차를 이동시킵니다.");
        spreadBrush();
        sprayDispensing();
        System.out.println("브러쉬 세척을 진행합니다.");
        sprayWater();
        System.out.println("말리기 과정을 진행합니다.");
    }

    protected abstract void spreadBrush();
    protected abstract void sprayDispensing();
    protected abstract void sprayWater();
}

class LargeCarWash extends CarWash {

    @Override
    protected void spreadBrush() {
        System.out.println("브러쉬 간격을 매우 넓게 벌립니다.");
    }

    @Override
    protected void sprayDispensing() {
        System.out.println("세제를 많이 분사합니다.");
    }

    @Override
    protected void sprayWater() {
        System.out.println("물을 많이 분사합니다.");
    }
}

class SmallCarWash extends CarWash {

    @Override
    protected void spreadBrush() {
        System.out.println("브러쉬 간격을 중간 넓이로 벌립니다.");
    }

    @Override
    protected void sprayDispensing() {
        System.out.println("세베를 중간 양으로 분사합니다.");
    }

    @Override
    protected void sprayWater() {
        System.out.println("물을 중간 양으로 분사합니다.");
    }
}

class SportsCarWash extends CarWash {

    @Override
    protected void spreadBrush() {
        System.out.println("브러쉬 간격을 좁게 벌립니다.");
    }

    @Override
    protected void sprayDispensing() {
        System.out.println("세제를 중간 양으로 분사합니다.");
    }

    @Override
    protected void sprayWater() {
        System.out.println("물을 중간 양으로 분사합니다.");
    }
}

public class Client {
    public static void main(String[] args) {
        CarWash carWash = null;
        boolean isValid;
        int carSize;
        Scanner sc = new Scanner(System.in);
        do {
            System.out.print("옵션을 선택하세요(1:대형차, 2:소형차, 3:스포츠카) : ");
            carSize = sc.nextInt();
            sc.nextLine();
            switch (carSize) {
                case 1:
                    System.out.println("대형차 세차를 진행합니다.");
                    isValid = true;
                    carWash = new LargeCarWash();
                    break;
                case 2:
                    System.out.println("소형차 세차를 진행합니다.");
                    isValid = true;
                    carWash = new SmallCarWash();
                    break;
                case 3:
                    System.out.println("스포츠카 세차를 진행합니다.");
                    isValid = true;
                    carWash = new SportsCarWash();
                    break;
                default:
                    isValid = false;
            }
        } while (!isValid);
        carWash.washCar();
        System.out.println("세차가 완료되었습니다.");
        sc.close();
    }
}
728x90
Comments