본문 바로가기

소프트웨어/디자인패턴

[소프트웨어/디자인패턴] 템플릿 메서드 패턴(Template Method Pattern)

템플릿 메서드 패턴은 구조적으로 동일한 기능들의 코드 중복의 최소화를 도와줍니다.

Context

엘리베이터는 모터와 문 등으로 이루어져 있다. 기존의 엘리베이터의 모터가 HyundaiMotor이고 Door가 있을 때 아래와 같이 나타낼 수 있는데

HyundaiMotorDoor을 가지고 있고 Door에 상태(DoorStatus)에 따라서 모터의 움직임이 달라진다.

모터

  1. 움직이고 있는 경우 명령을 무시한다.
  2. 문이 열려있을 경우 문을 닫는다.
  3. 모터를 움직인다.
  4. 모터의 상태를 바꾼다.

위와 같은 조건에 따라서 아래와 같이 구현할 수 있다.

class HyundaiMotor {
    private Door door;
    private MotorStatus motorStatus;

    public HyundaiMotor(Door door) {
        this.door = door;
        motorStatus = MotorStatus.STOPPED;
    }

    private void moveMotor(Direction direction) {
        // 현대 모터를 구동시키는 로직
    }

    public MotorStatus getMotorStatus() {
        return motorStatus;
    }

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

    public void move(Direction direction) {
        MotorStatus status = getMotorStatus();

        if(motorStatus == MotorStatus.MOVING) {
            return;
        }

        DoorStatus doorStatus = door.getDoorStatus();

        if(doorStatus == DoorStatus.OPENED) {
            door.close();
        }

        moveMotor(direction);
        setMotorStatus(MotorStatus.MOVING);
    }
}

하지만 유지보수를 하다보니 기존 HyundaiMotor에서 LGMotor로 바꾸게 되었다. 사실 클래스 하나를 추가하면 해결되는 사항이라 일단 클래스를 복사 붙혀넣기를 통해 생성한다.

class LGMotor {

    ...같음

    private void moveLGMoter(Direction direction) {
        // LG 모터를 구동시키는 로직
    }

    public void move(Direction direction) {
        MotorStatus status = getMotorStatus();

        if(motorStatus == MotorStatus.MOVING) {
            return;
        }

        DoorStatus doorStatus = door.getDoorStatus();

        if(doorStatus == DoorStatus.OPENED) {
            door.close();
        }

        moveLGMoter(direction);
        setMotorStatus(MotorStatus.MOVING);
    }
}

Problem

로직 수정

비슷한 내용의 클래스가 2개나 되며 기존 클래스를 복사하여 생성했기 떄문에 중복코드도 생겨났다, 일단 중복코드가 생긴다는 것은 수정사항에 대한 유지보수가 나빠진다는 것이다. 이는 상속을 통해 어느정도 해결이 가능하다.

공통적으로 들어가는 각 Door, Constructor, MotorStatus에 대해서 일반화한 후 아래와 같이 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;
    }
}

Motor은 필드를 가지지만 공통적인 부분의 대해서 일반화한 대상일 뿐, 특정한 인스턴스를 생성할 수 있는 클래스로 추상화된 것이 아니기 떄문에 abstract키워드를 붙혀 추상클래스로 생성한다. 해당 추상클래스를 기준으로 기존 HyundaiMotor을 구현한다. 이전과 비교해 클래스의 사이즈가 많이 줄었다.

class HyundaiMotor extends  Motor{
    public HyundaiMotor(Door door) {
        super(door);
    }

    private void moveHyundaiMotor(Direction direction) {
        // 현대 모터를 구동시키는 로직
    }

    public void move(Direction direction) {
        MotorStatus status = getMotorStatus();

        if(getMotorStatus() == MotorStatus.MOVING) {
            return;
        }

        DoorStatus doorStatus = door.getDoorStatus();

        if(doorStatus == DoorStatus.OPENED) {
            door.close();
        }

        moveHyundaiMotor(direction);
        setMotorStatus(MotorStatus.MOVING);
    }
}

따라서 LGMotor도 구현한다.

class LGMotor extends  Motor{
    public LGMotor(Door door) {
        super(door);
    }

    private void moveLGMotor(Direction direction) {
        // 현대 모터를 구동시키는 로직
    }

    public void move(Direction direction) {
        MotorStatus status = getMotorStatus();

        if(getMotorStatus() == MotorStatus.MOVING) {
            return;
        }

        DoorStatus doorStatus = door.getDoorStatus();

        if(doorStatus == DoorStatus.OPENED) {
            door.close();
        }

        moveLGMotor(direction); // 다른 부분
        setMotorStatus(MotorStatus.MOVING);
    }
}

하지만 근본적으로 로직자체는 동일해보인다, 하지만 위 LGMotorLGMotor::moveLGMotor메서드 때문에 상속을 통해 해결할 수 없는 코드 중복이 발생하였다. 이는 어떻게 해결할 수 있을까?

이 예제에서는 단순한 함수명 차이일 수 있지만 함수 내 로직 차이일 수 있다.

Solution

move메서드는 실제 구동하는 메서드를 호출하는 것을 제외하곤 로직자체는 거의 동일하다. 그렇다면 move메서드를 일반화하면 어느정도 해결이 된다. 하지만 메서드 명이 다르기 때문에 이를 하나의 메서드로 추상화시킨 후 일반화하여 만들어 놓은 뒤 구현 클래스에 해당 메서드를 구현하는 형태라면 코드 중복을 피할 수 있다.

구현 클래스에서 구현에 대한 책임을 가지는 메서드를 아래 두 가지 용어로 부른다.

  1. primitive method
  2. hook method

primitive method를 호출하는 메서드를 template method라고한다.

보통 웹이나 어플리케이션을 개발할 때 템플릿을 쓰게되는데 데이터를 바인딩할 때 특정한 구문(예를 들어서 {{message}})을 작성한다. template method가 템플릿에 해당되는 것이고 데이터를 바인딩하기 위한 구문이 여기서는 코드를 껴넣기(hooking) 위한 hook method, primitive method)가 되는 것이다.

여하튼 위의 내용을 일단 모델링 해보자면 아래와 같다.

이를 코드로 나타내자면..

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) {
        MotorStatus status = getMotorStatus();

        if(getMotorStatus() == MotorStatus.MOVING) {
            return;
        }

        DoorStatus doorStatus = door.getDoorStatus();

        if(doorStatus == 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) {
        // 현대 모터 구현
    }
}

class LGMotor extends Motor {
    public LGMotor(Door door) {
        super(door);
    }

    @Override
    protected void moveMotor(Direction direction) {
        // LG 모터 구현
    }
}

이를 구현하는 구현클래스에서는 변경 부분에 대해서만 책임을 진다.

Summary

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