Don't give up!

[Design Pattern] 템플릿 메소드 패턴 (Template Method Pattern) 본문

개발서적/Head First Design Patterns

[Design Pattern] 템플릿 메소드 패턴 (Template Method Pattern)

Heang Lee 2021. 12. 21. 00:49
이 글은 에릭 프리먼의 'Head First Design Patterns'를 읽고 TIL(Today I Learned)로써 정리한 내용을 기록하는 글입니다.
자세한 내용은 책을 통해 확인하실 수 있습니다.

알라딘: Head First Design Patterns (aladin.co.kr)

 

Head First Design Patterns

볼 거리가 많고 재미있으면서도, 머리 속에 쏙쏙 들어오는 방식으로 구성된 Head First 시리즈. 패턴의 근간이 되는 객체지향 디자인 원칙, 중요한 패턴, 디자인 적용 방법, 쓰지 말아야 하는 이유

www.aladin.co.kr

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

템플릿 메소드 패턴은 메소드에서 알고리즘의 골격을 정의하는 디자인 패턴입니다.

알고리즘의 구조를 메소드에서 정의하고, 하위 클래스에서 알고리즘 구조의 변경없이 알고리즘을 재정의함으로써 코드 재사용에 크게 도움을 줄 수 있습니다.

public abstract class CaffeineBeverage {
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }
    
    abstract void brew();
    
    abstract void addCondiments();
    
    void boilWater() {
        System.out.println("물 끓이는 중");
    }
    
    void pourInCup() {
        System.out.println("컵에 따르는 중");
    }
}

public class Tea extends CaffeineBeverage {
    public void brew() {
        System.out.println("차를 우려내는 중");
    }
    
    public void addCondiments() {
        System.out.println("레몬을 추가하는 중");
    }
}

public class Coffee extends CaffeineBeverage {
    public void brew() {
        System.out.println("필터로 커피를 우려내는 중");
    }
    
    public void addCondiments() {
        System.out.println("설탕과 커피를 추가하는 중");
    }
}

Spring에서의 Template Method Pattern

Spring의 JPA에서 Template Method Pattern가 적용된 것을 확인할 수 있었습니다.

JPA의 AbstractJpaQuery는 CreateQuery에서 doCreateQuery, CreateCountQuery에서 doCreateQuery를 호출하며, 

두 메소드는 추상화되어 있습니다.

public abstract class AbstractJpaQuery implements RepositoryQuery {
    private final JpaQueryMethod method;
    private final EntityManager em;
    private final JpaMetamodel metamodel;
    private final PersistenceProvider provider;
    private final Lazy<JpaQueryExecution> execution;
    final Lazy<ParameterBinder> parameterBinder = new Lazy(this::createBinder);

    public AbstractJpaQuery(JpaQueryMethod method, EntityManager em) {
        Assert.notNull(method, "JpaQueryMethod must not be null!");
        Assert.notNull(em, "EntityManager must not be null!");
        this.method = method;
        this.em = em;
        this.metamodel = JpaMetamodel.of(em.getMetamodel());
        this.provider = PersistenceProvider.fromEntityManager(em);
        this.execution = Lazy.of(() -> {
            if (method.isStreamQuery()) {
                return new StreamExecution();
            } else if (method.isProcedureQuery()) {
                return new ProcedureExecution();
            } else if (method.isCollectionQuery()) {
                return new CollectionExecution();
            } else if (method.isSliceQuery()) {
                return new SlicedExecution();
            } else if (method.isPageQuery()) {
                return new PagedExecution();
            } else {
                return method.isModifyingQuery() ? null : new SingleEntityExecution();
            }
        });
    }

    @Nullable
    private Object doExecute(JpaQueryExecution execution, Object[] values) {
        JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(this.method.getParameters(), values);
        Object result = execution.execute(this, accessor);
        ResultProcessor withDynamicProjection = this.method.getResultProcessor().withDynamicProjection(accessor);
        return withDynamicProjection.processResult(result, new AbstractJpaQuery.TupleConverter(withDynamicProjection.getReturnedType()));
    }

    protected JpaQueryExecution getExecution() {
        JpaQueryExecution execution = (JpaQueryExecution)this.execution.getNullable();
        if (execution != null) {
            return execution;
        } else {
            return (JpaQueryExecution)(this.method.isModifyingQuery() ? new ModifyingExecution(this.method, this.em) : new SingleEntityExecution());
        }
    }
    
    protected Query createQuery(JpaParametersParameterAccessor parameters) {
        return this.applyLockMode(this.applyEntityGraphConfiguration(this.applyHints(this.doCreateQuery(parameters), this.method), this.method), this.method);
    }
    
    protected Query createCountQuery(JpaParametersParameterAccessor values) {
        Query countQuery = this.doCreateCountQuery(values);
        return this.method.applyHintsToCountQuery() ? this.applyHints(countQuery, this.method) : countQuery;
    }

    protected abstract Query doCreateQuery(JpaParametersParameterAccessor var1);

    protected abstract Query doCreateCountQuery(JpaParametersParameterAccessor var1);
}

AbstractJpaQuery를 상속받는 AbstractStringBasedJpaQuery는 doCreateQuery, doCreateCountQuery를 정의함으로써 알고리즘 구조를 유지한 상태에서 목적에 맞게 재정의를 할 수 있습니다.

abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
    private final DeclaredQuery query;
    private final DeclaredQuery countQuery;
    private final QueryMethodEvaluationContextProvider evaluationContextProvider;
    private final SpelExpressionParser parser;
    private final QueryMetadataCache metadataCache = new QueryMetadataCache();

    public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, String queryString, QueryMethodEvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {
        super(method, em);
        Assert.hasText(queryString, "Query string must not be null or empty!");
        Assert.notNull(evaluationContextProvider, "ExpressionEvaluationContextProvider must not be null!");
        Assert.notNull(parser, "Parser must not be null!");
        this.evaluationContextProvider = evaluationContextProvider;
        this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation(), parser);
        DeclaredQuery countQuery = this.query.deriveCountQuery(method.getCountQuery(), method.getCountQueryProjection());
        this.countQuery = ExpressionBasedStringQuery.from(countQuery, method.getEntityInformation(), parser);
        this.parser = parser;
        Assert.isTrue(method.isNativeQuery() || !this.query.usesJdbcStyleParameters(), "JDBC style parameters (?) are not supported for JPA queries.");
    }

    public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
        String sortedQueryString = QueryUtils.applySorting(this.query.getQueryString(), accessor.getSort(), this.query.getAlias());
        ResultProcessor processor = this.getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
        Query query = this.createJpaQuery(sortedQueryString, processor.getReturnedType());
        QueryMetadata metadata = this.metadataCache.getMetadata(sortedQueryString, query);
        return ((ParameterBinder)this.parameterBinder.get()).bindAndPrepare(query, metadata, accessor);
    }

    protected ParameterBinder createBinder() {
        return ParameterBinderFactory.createQueryAwareBinder(this.getQueryMethod().getParameters(), this.query, this.parser, this.evaluationContextProvider);
    }

    protected Query doCreateCountQuery(JpaParametersParameterAccessor accessor) {
        String queryString = this.countQuery.getQueryString();
        EntityManager em = this.getEntityManager();
        Query query = this.getQueryMethod().isNativeQuery() ? em.createNativeQuery(queryString) : em.createQuery(queryString, Long.class);
        QueryMetadata metadata = this.metadataCache.getMetadata(queryString, (Query)query);
        ((ParameterBinder)this.parameterBinder.get()).bind(metadata.withQuery((Query)query), accessor, ErrorHandling.LENIENT);
        return (Query)query;
    }

    public DeclaredQuery getQuery() {
        return this.query;
    }

    public DeclaredQuery getCountQuery() {
        return this.countQuery;
    }

    protected Query createJpaQuery(String queryString, ReturnedType returnedType) {
        EntityManager em = this.getEntityManager();
        if (!this.query.hasConstructorExpression() && !this.query.isDefaultProjection()) {
            Class<?> typeToRead = this.getTypeToRead(returnedType);
            return (Query)(typeToRead == null ? em.createQuery(queryString) : em.createQuery(queryString, typeToRead));
        } else {
            return em.createQuery(queryString);
        }
    }
}