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

사용 시기

  • 특정 메서드들이 순서대로 실행되어야만 하는 경우와 같이 제어 흐름을 통제해야 할 때
  • 프로그램을 구현하다 보면, 일부 과정의 구현만 다르고 완전히 동일한 절차를 가진 코드를 작성하게 될 때
  • 프레임워크에서 개발자가 정형화된/일관된 코딩을 하도록 유도해야 할 경우

    → 이 패턴은 ‘표준 매뉴얼’처럼 프레임워크 설계자가 원하는 방식으로 개발을 유도한다. 여러 개발자들과 협업할 때, 일관성 있는 개발을 끌어내기 좋은 패턴이다. 프레임워크에서 전체적인 알고리즘의 흐름은 언제나 프레임워크 설계자 의도대로 흘러간다. 대신 프레임워크에서 명시한 알고리즘의 세부 로직은 개발자 마음대로 구현할 수 있다. 결국 프레임워크 설계자도 좋고 개발자도 좋은 소프트웨어 개발 환경을 구축할 수 있다.

장점 및 특징

  • 부모 클래스에서 자식 클래스의 로직을 호출한다. 자식 클래스에서 부모 클래스를 직접 호출하진 않는다. ‘할리우드 원칙’이라고 부르기도 하는데, 부모 클래스만 자식 클래스를 호출해야 한다. 반대로 자식 클래스에서 부모 클래스를 호출하는 경우는 지양해야 한다. 예를 들어 부모 클래스가 자식 클래스에 의존하고 자식 클래스는 다시 부모 클래스에 의존하면, 의존 관계가 복잡하면서 소프트웨어를 개발하고 유지보수 하는 것이 어려워진다. 하지만 ‘할리우드 원칙’을 지키면 이런 잘못된 의존 상황을 개선할 수 있다. 템플릿 메서드를 잘 사용하면 자연스럽게 ‘할리우드 원칙’을 지킬 수 있다. (현장에서는 자식 클래스가 부모 클래스에 약하게 의존해야만 하는 경우는 종종 발생한다. 하지만 템플릿 메서드 패턴을 쓰게 되면 쓰기 전의 의존성보다 훨씬 약하게 의존이 된다.)
  • 일반적인 경우 하위 타입이 상위 타입의 기능을 재사용할 지 여부를 결정하기 때문에, 흐름 제어를 하위 타입이 하게 된다. 하지만 템플릿 메서드 패턴은 하위 클래스가 아닌 상위 클래스에서 모든 실행 흐름을 제어하고, 하위 타입의 메서드는 템플릿 메서드에서 호출되는 구조를 갖는다.

예제 1)

아쉬운 코드

  • 전체 절차가 거의 비슷하고 일부 과정의 구현만 다르다. 이로 인해 중복된 코드가 많이 발생했다.
  • Authenticator의 역할에 해당하는 또 다른 클래스를 생성하려 할 때, 전체 절차를 지키지 않고 구현할 위험성이 따른다. → 밑의 코드에서 볼 수 있다시피, 인증이 실패했을 경우 예외를 발생시켜야 하는데 이를 빠트리고 구현을 해버리게 될 수도 있는 것이다.
public class LocalAuthenticator {
    public Auth authenticate(String id, String pw) {
        // 사용자 정보로 인증 확인
        User user = userDao.selectById(id);
        boolean auth = user.equalPassword(pw);
        
        // 인증 실패 시 예외 발생
        if (!auth) {
            throw new AuthException();
        }

        // 인증 성공 시, 인증 정보 제공
        return new Auth(id, user.getName);
    }
}
public class SnsAuthenticator {
    public Auth authenticate(String id, String pw) {
        // 사용자 정보로 인증 확인
        boolean auth = snsClient.authenticate(id, pw);
        
        // 인증 실패 시 예외 발생
        if (!auth) {
            throw new AuthException();
        }
    
        // 인증 성공 시, 인증 정보 제공
        return new Auth(id, snsClient.find(id));
    }
}

개선된 코드

  1. 먼저 두 클래스에서 차이가 나는 부분의 로직을 별도의 추상 메서드로 분리해라.

    → ‘사용자 정보로 인증 확인’ 기능을 doAuthenticate()로 분리

    → ‘인증 성공 시, 인증 정보 제공’ 기능을 createAuth()로 분리

public abstract class Authenticator {
    // 템플릿 메서드 (흐름 제어를 갖고 있는 메서드)
    public Auth authenticate(String id, String pw) {
        // 사용자 정보로 인증 확인
        boolean auth = doAuthenticate(id, pw);
        
        // 인증 실패 시 예외 발생 (그대로)
        if (!auth) {
            throw new AuthException();
        }

        // 인증 성공 시, 인증 정보 제공
        return createAuth(id);
    }
    
    protected abstract boolean doAuthenticate(String id, String pw);

    protected abstract Auth createAuth(String id);
}

  1. 각 자식 클래스들은 부모 클래스를 상속 받아, 추상 메서드로 분리한 부분을 @Override를 통해 각각의 로직을 작성하면 된다.
public class LocalAuthenticator extends Authenticator {

    public Auth authenticate(String id, String pw) {
        @Override
        protected boolean doAuthenticate(String id, String pw) {
            User user = userDao.selectById(id);
            boolean auth = user.equalPassword(pw);
            return auth;
        }
        
        @Override
        protected Auth createAuth(String id) {
            User user = userDao.selectById(id);
            return new Auth(id, user.getName(id));
        }
    }
}
public class SnsAuthenticator extends Authenticator {
    public Auth authenticate(String id, String pw) {
        @Override
        protected boolean doAuthenticate(String id, String pw) {
            boolean auth = snsClient.authenticate(id, pw);
            return auth;
        }
        
        @Override
        protected Auth createAuth(String id) {
            return new Auth(id, snsClient.find(id));
        }
    }
}

코드를 개선함으로써 발생한 효과

  • 반드시 필수적으로 따라야 하는 제어 흐름을 왜곡해서 만드는 실수를 없앨 수 있다.
  • 중복된 코드가 줄어서 코드가 훨씬 명료해졌다.
  • 개발자들이 협업할 때 일관성 있는 개발을 끌어내기 좋아졌다.

References