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

프로그램을 구현하다 보면, 완전히 동일한 절차를 가진 코드를 작성하게 될 경우가 있다. 또한, 일부 구현만 다를 뿐 나머지의 구현은 똑같은 경우도 있다. 예를 들어 아래와 같이, DB 데이터와 LDAP을 이용해서 인증을 처리하는 객체는 사용자 정보를 가져오는 구현만 다를 뿐 인증을 처리하는 과정은 완전히 동일할 수 있다.

public class DbAthenticator {
    private UserDao userDao;

    public DbAthenticator(UserDao userDao) {
        this.userDao = userDao;

    }

    public Auth authenticate(String id, String pw) throws Exception {
        User user = userDao.selectById(id);
        boolean auth = user.equalPassword(pw);
        if(!auth) {
            throw new Exception("has not auth");

        }

        return new Auth(id, pw);

    }

}
public class LdapAuthenticator {
    private LdapClient ldapClient;

    public LdapAuthenticator(LdapClient ldapClient) {
        this.ldapClient = ldapClient;

    }

    public Auth authenticate(String id, String pw) throws Exception {
        boolean auth = ldapClient.authenticate(id, pw);
        if(!auth) {
            throw new Exception("has not auth");

        }

        return new Auth(id, pw);

    }

}

이렇듯, 실행 과정은 동일하지만 일부 구현이 다른 경우에 사용할 수 있는 패턴이 템플릿 메서드 패턴이다. 템플릿 메서드 패턴은 두 가지로 구성된다.

  • 실행 과정을 구현한 상위 객체

  • 실행 과정의 일부 단계를 구현한 하위 객체

상위 객체는 실행 과정을 구현한 메서드를 제공한다. 이 메서드는 기능을 구현하는데 필요한 각 단계를 정의하며, 이 중 일부 단계는 추상 메서드를 호출하는 방식으로 구현된다. 앞서 보인 예제에 템플릿 메서드를 적용해보면 상위 객체는 다음과 같이 구성할 수 있다.

public abstract class Authenticator {
    public Auth authenticate(String id, String pw) throws Exception {
        if(!doAuthenticate(id, pw)) {
            throw new Exception("has not authentication");

        }

        return createAuth(id);

    }

    protected abstract boolean doAuthenticate(String id, String pw);

    protected abstract Auth createAuth(String id);

}

authenticate() 메서드는 DbAthenticator와 LdapAuthenticator에서 동일했던 실행 과정을 구현하고 있고, 두 객체에서 차이가 나는 부분은 별도의 추상 메서드로 분리하였다.

id/pw를 이용하여 인증 여부를 확인하는 단계는 doAuthenticate() 추상 메서드로 분리하였고, Auth 객체를 생성하는 단계는 createAuth() 추상 메서드로 분리하였다. authenticate() 메서드는 모든 하위 타입에 동일하게 적용되는 실행 과정을 제공하기 때문에 템플릿 메서드(Template Method) 라고 부른다.

Authenticator 객체를 상속하는 하위 객체는 authenticate() 메서드에서 호출하는 추상 메서드만 자신의 상황에 알맞게 재정의하면 된다.

public class DbAthenticator extends Authenticator{
    private UserDao userDao;

    public DbAthenticator(UserDao userDao) {
        this.userDao = userDao;

    }

    @Override
    protected boolean doAuthenticate(String id, String pw) {
        User user = userDao.selectById(id);
        return user.equalPassword(pw);

    }

    @Override
    protected Auth createAuth(String id, String pw) {
        return new Auth(id, pw);

    }

}

결과적으로, DbAuthenticator 객체는 전체 실행 과정을 제공하지 않고, 일부 과정의 구현만을 제공한다. 전체 실행 과정은 상위 타입인 Authenticator인 authenticate() 템플릿 메서드에서 제공하게 된다.

상위 타입에서의 템플릿 메서드는 동일한 실행 과정의 구현을 제공하고, 각 하위 타입마다 다른 구현 부분은 추상 메서드로 분리하여 코드 중복을 방지할 수 있다. 중복된 코드가 출현한다는 것은 그 만큼 유지 보수를 어렵게 만드는데, 템플릿 메서드 패턴는 코드 중복을 제거하고 코드 재사용성을 향상시킨다.

상위 객체가 흐름 제어 주체

템플릿 메서드 패턴의 특징은 하위 객체가 아니라 상위 객체에서 흐름 제어를 한다는 것이다. 일반적으로 하위 타입이 상위 타입 메서드에 대한 재사용 여부를 결정하기 때문에, 하위 타입애서 흐름 제어를 하게 된다. 반면에 템플릿 메서드 패턴에서는 상위 타입의 템플릿 메서드가 모든 실행 흐름을 제어하고, 하위 타입의 메서드는 템플릿 메서드에서 호출되는 구조를 갖게 된다.

템플릿 메서드는 외부에 제공하는 기능에 해당되기 때문에 public 접근 범위를 가진다. 반면에, 하위 타입에서 재정의하는 추상 메서드는 템플릿 메서드에서만 호출되기 때문에 public 보단 protected 접근 범위가 적합하다.

앞서 보인 예제에서는 템플릿 메서드에서 호출하는 메서드를 추상 메서드로 정의하였는데, 기본 구현을 제공하고 하위 객체에서 알맞게 재정의하도록 구현할 수도 있다. 이러한 경우, 해당 메서드는 기능의 확장 지점으로 사용할 수 있다. 예를 들어, 아래의 validate() 메서드는 하위 객체에서 재정의 하지 않는다면 항상 true 값을 반환할 것이다.

public abstract class Authenticator {
    public Auth authenticate(String id, String pw) throws Exception {
        if(!validate(id, pw)) {
            throw new Exception("id, pw is Invalid");

        }

        if(!doAuthenticate(id, pw)) {
            throw new Exception("has not authentication");

        }

        return createAuth(id, pw);

    }

    protected abstract boolean doAuthenticate(String id, String pw);

    protected abstract Auth createAuth(String id, String pw);

    protected boolean validate(String id, String pw) {
        return true;

    }

}

Authenticator 상속한 하위 객체는 doAuthenticate(), createAuth() 추상 메서드는 반드시 구현해야 하지만, validate() 메서드는 필요한 경우에만 구현해주면 된다. 즉, validate() 메서드는 상위 객체 입장에서는 제어 대상이 되는 확장 Point가 되며, 하위 객체 입장에서는 자신의 상황에 맞는 확장 기능을 구현할 위치가 된다. 위 예에서 하위 객체에서 인자 유효성 검사가 필요할 경우, validate() 메서드는 기능 확장의 Point가 된다.

public class DbAthenticator extends Authenticator{

    ...

    @Override
    protected boolean validate(String id, String pw) {
        return id.startsWith("_") && pw.endsWith("_");

    }

}

Hook 메서드

상위 객체에서 실행 시점이 제어되고, 기본 구현을 제공하면서, 하위 객체에서 알맞게 확장할 수있는 메서드를 훅(Hook) 메서드라고 부른다.

템플릿 메서드와 전략 패턴의 조합

템플릿 메서드와 전략 패턴을 함께 사용하면 상속이 아닌 조립 방식으로 템플릿 메서드 패턴을 활용할 수 있는데, 대표적으로 스프링 프레임워크의 Template으로 끝나는 클래스가 있다. 해당 클래스들은 템플릿 메서드를 실행할 때, 변경되는 부분을 실행할 객체를 파라미터로 전달받는 방식으로 구현되어 있다. 예를 들어, JDBC 기능을 제공하는 JdbcTemplate 객체의 execute() 메서드는 아래와 같이 구현되어 있다.

public <T> T execute(ConnectionCallback<T> action) throws DataAccessException {
      Assert.notNull(action, "Callback object must not be null");
      Connection con = DataSourceUtils.getConnection(obtainDataSource());

      try {
            Connection conToUse = createConnectionProxy(con);
            return action.doInConnection(conToUse);

        } catch (SQLException ex) {
            // 생략

        } finally {
            // 생략

        }

    }

execute() 메서드는 사용 가능한 Connection을 찾아서 데이터 액세스 작업을 실행하는 템플릿 메서드인데, 앞서 살펴본 템플릿 메서드와 몇 가지 차이점이 있다.

  • 앞서 템플릿 메서드는 추상 메서드를 호출한다.

  • JdbcTemplate 객체의 execute() 메서드는 인자로 전달받은 ConnectionCallback 타입 객체의 메서드를 호출하고 있다.

따라서, JdbcTemplate의 execute() 메서드를 사용하는 Client는 원하는 기능을 구현한 ConnectionCallback 객체를 조립 방식으로 전달한다. 만약 객체를 전달하지 않는다면 해당 템플릿 메서드는 정상적으로 동작할 수 없기 때문에 스프링 프레임워크에서도 Assert로 객체를 전달하도록 요구한다.

jdbcTemplate.execute(new ConnectionCallback<Object>() {
      @Override
      public Object doInConnection(Connection con) throws SQLException, DataAccessException {
                // 커넥션 범위 안에서 실행할 코드

      }

});

템플릿 메서드 패턴과 전략 패턴을 조합하게 되면, 상속에 기반을 둔 템플릿 메서드 구현과 비교해서 유연할을 갖는다. 상속으로 재사용할 경우 클래스가 불필요하게 증가할 수 있고 런타임에 교체할 수 없는 단점이 있다. 반면에, 조립/위임 방식의 경우에는 런타임에 템플릿 메서드에서 사용할 객체를 교체할 수 있다는 장점을 갖는다.

상속 방식은 Hook 메서드를 하위 객체에 재정의하여 쉽게 확장할 수 있지만, 조립/위임 방식은 확장 기능을 제공하려면 다소 복잡해지는 단점이 있다.

Last updated