Design Pattern

Template Method Pattern

cwchoiit 2023. 12. 12. 11:35
728x90
반응형
SMALL
728x90
SMALL

 

자바의 패턴 중 하나인 템플릿 메서드 패턴에 대해서 공부한 것을 작성해 보고자한다. 

 

템플릿 메서드 패턴은 다형성을 이용하여 공통 부분을 하나로 정의하고 변하는 부분만을 유연하게 변경하게 하는 방법이다.

예를 들어, 모든 메서드에 대해 시작 시간과 종료 시간을 구해 메서드 실행 시간을 보여주는 로그를 모든 메서드에 적용하고 싶다면 어떻게 하면 될까? 방법은 많겠지만 템플릿 메서드 패턴이 그 중에 하나가 될 수 있다. 

 

위 상황에서 모든 메서드에 대해 공통 부분은 무엇인가? 바로 메서드의 시작 시간과 종료 시간을 구해 소요시간을 구해내는 부분이다. 변하는 부분은 무엇인가? 각 메서드가 가지고 있는 비즈니스 로직이다.

 

이 모든 공통 부분에 대해 모든 메서드에 각각 적용한다면 적용하는 것도 일이겠지만 추후 요구사항의 변경으로 인해 코드가 변경되어야 한다면 모든 메서드에 대해 변경하는 작업 또한 일이다. 심지어 메서드 개수가 천개, 만개, 이만개라면 상상도 못할 일이다. 이런 떨어지는 유지보수성을 높여줄 수 있는 패턴이다.

 

위 예시를 토대로 코드를 작성해보자.

 

AbstractTemplate.java

package com.example.advanced.trace.template.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public abstract class AbstractTemplate {

    public void execute() {
        long startTime = System.currentTimeMillis();

        call();

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime={}ms", resultTime);
    }

    protected abstract void call();
}

 

이 코드는 템플릿이 될 추상 클래스이다. 이 추상 클래스에서 공통 부분을 정의한다. 즉 시작 시간, 종료 시간, 소요 시간을 구하는 부분이다.

그럼 그 사이에 실행될 비즈니스 로직은? 이 추상 클래스를 상속받는 클래스에서 정의를 하게끔 만드는 것이다. 그럼 이 추상 클래스를 상속받은 클래스마다 해당 로직이 달라질 것이다. 그 로직은 'call()' 메서드가 담당한다. 

 

이 추상 클래스를 구현한 구현 클래스를 살펴보자.

 

SubClassLogic1.java

package com.example.advanced.trace.template.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SubClassLogic1 extends AbstractTemplate {
    @Override
    protected void call() {
        log.info("비즈니스 로직 1 실행");
    }
}

 

SubClassLogic2.java

package com.example.advanced.trace.template.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SubClassLogic2 extends AbstractTemplate {
    @Override
    protected void call() {
        log.info("비즈니스 로직 2 실행");
    }
}

 

두 개의 클래스는 각각 AbstractTemplate 클래스를 상속받는다. 이 클래스를 상속받으면 반드시 구현해야 할 'call()' 메서드를 각 클래스 별 상이하게 구현했다. 이제 이 템플릿 메서드 패턴을 실제로 사용한 예시 코드를 보자.

 

TemplateMethodTest.java

package com.example.advanced.trace.template;

import com.example.advanced.trace.template.code.AbstractTemplate;
import com.example.advanced.trace.template.code.SubClassLogic1;
import com.example.advanced.trace.template.code.SubClassLogic2;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

@Slf4j
public class TemplateMethodTest {

    @Test
    void templateMethodV0() {
        logic1();
        logic2();
    }

    private void logic1() {
        long startTime = System.currentTimeMillis();

        log.info("비즈니스 로직1 실행");

        long endTime = System.currentTimeMillis();

        long resultTime = endTime - startTime;

        log.info("resultTime={}", resultTime);
    }

    private void logic2() {
        long startTime = System.currentTimeMillis();

        log.info("비즈니스 로직2 실행");

        long endTime = System.currentTimeMillis();

        long resultTime = endTime - startTime;

        log.info("resultTime={}", resultTime);
    }

    /**
     * 템플릿 메서드 패턴 적용
     * */
    @Test
    void templateMethodV1() {
        AbstractTemplate template1 = new SubClassLogic1();
        template1.execute();

        AbstractTemplate template2 = new SubClassLogic2();
        template2.execute();
    }
}

 

위 코드에서 먼저 템플릿 메서드 패턴을 적용하지 않은 코드를 보자. 공통 부분인 메서드의 시작 시간, 종료 시간, 소요 시간을 구하는 코드와 각 메서드 별 가지는 비즈니스 로직을 메서드 별로 만들었다면 'logic1()', 'logic2()'처럼 표현된다.

 

그리고 그 메서드를 실행하는 위임 메서드 'templateMethodV0()'이 있다. 단 두개의 메서드만으로도 얼마나 비효율적인지 알 수 있다. 거의 최악의 코드라고 보면 된다. 유지보수성이 '0'에 가까운.

 

반면에 템플릿 메서드 패턴을 적용한 'templateMethodV1()' 메서드를 보자.

메서드 별 달리 적용되는 부분을 상속받는 각 클래스가 상속받아 구현하고 부모 클래스가 가지고 있는 'execute()' 메서드를 실행만 하면 된다. 부모 타입의 변수를 만들었기 때문에 부모 클래스가 가진 메서드('execute()')를 호출할 수 있고, 그 안에서 call() 메서드를 호출하는데 그 메서드는 자식이 오버라이딩 했다면 무조건 오버라이딩 메서드가 우선순위를 가지는 다형성이 주는 강력함을 이용해서 공통부분과 달라지는 부분을 분리할 수 있다. 추후에 공통 부분에 변경 사항이 생기더라도 딱 한 개의 클래스인 추상 클래스(부모 클래스)만 수정하면 된다.

 

그러나, 이 코드도 문제가 없는 것은 아니다. 메서드가 100개면 100개를 수정하듯, 서로 다른 로직이 필요한 부분이 100개라면 100개의 클래스를 만들어야 한다. 이 부분을 어떻게 해결할까?

 

익명 내부 클래스

익명 내부 클래스는 위 문제를 해결해준다. 즉, 클래스를 사전에 정의해서 호출하는 방법이 아니라, 추상 클래스를 만드는 시점에 정의하는 것이다. 이는 100개의 서로 다른 로직이 필요할 때 100개의 클래스 파일을 만들어야 하는게 아니라 그 때마다 익명 내부 클래스를 선언해주면 된다.

package com.example.advanced.trace.template;

import com.example.advanced.trace.template.code.AbstractTemplate;
import com.example.advanced.trace.template.code.SubClassLogic1;
import com.example.advanced.trace.template.code.SubClassLogic2;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

@Slf4j
public class TemplateMethodTest {

    /**
     * 익명 내부 클래스로 클래스 파일을 일일이 만들지 않고 바로바로 정의해서 사용하기
     * */
    @Test
    void templateMethodV2() {
        AbstractTemplate template1 = new AbstractTemplate() {
            @Override
            protected void call() {
                log.info("비즈니스 로직1 실행");
            }
        };

        template1.execute();

        AbstractTemplate template2 = new AbstractTemplate() {
            @Override
            protected void call() {
                log.info("비즈니스 로직2 실행");
            }
        };

        template2.execute();
    }
}

 

위 'templateMethodV2()' 메서드를 보자. 추상 클래스인 AbstractTemplate를 구현한 클래스를 호출하는 게 아니라 추상 클래스를 사용하고자 할 때마다 직접 클래스를 정의하고 있다. 이렇게 되면 클래스를 하나하나 정의하여 가져다가 사용하지 않아도 된다.

 

 

다양한 반환 타입을 가지는 템플릿 메서드 패턴

여기서 한걸음 더 나아가서 실제 로직이 들어갈 call() 메소드의 반환 타입이 그때 그때 달라질 수 있는 경우도 생각해보자.

다음 코드를 보자.

package com.example.advanced.trace.template;

import com.example.advanced.trace.TraceStatus;
import com.example.advanced.trace.logtrace.LogTrace;

public abstract class AbstractTemplate<T> {

    private final LogTrace logTrace;

    public AbstractTemplate(LogTrace logTrace) {
        this.logTrace = logTrace;
    }

    public T execute(String message) {
        TraceStatus status = null;

        try {
            status = logTrace.begin(message);

            T result = call();

            logTrace.end(status);

            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }

    protected abstract T call();
}

 

공통 부분을 제외한 변경되는 부분의 반환 타입이 그때마다 달라지는 경우 제네릭을 사용하면 된다. 이 추상 클래스를 보면 제네릭 타입으로 'call()' 메서드의 반환 타입을 지정했다. 이 추상 클래스를 상속받는 클래스를 만들거나 익명 내부 클래스로 그 때마다 정의를 하더라도 반환 타입을 유연하게 가져갈 수 있게 됐다. 

 

생성자로는 LogTrace라는 객체를 받게 되어 있는데 이 부분은 LogTrace가 무엇인가에 초점을 두지 말고 이렇게 생성자로 주입을 받을 수도 있다는 것에 초점을 두면 좋겠다.

 

이 추상클래스를 실제 익명 내부 클래스를 선언해서 사용해보는 코드를 살펴 보자.

package com.example.advanced.app.v4;

import com.example.advanced.trace.TraceStatus;
import com.example.advanced.trace.logtrace.LogTrace;
import com.example.advanced.trace.template.AbstractTemplate;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class OrderControllerV4 {

    private final OrderServiceV4 orderService;
    private final LogTrace trace;

    @GetMapping("/v4/request")
    public String request(String itemId) {
        AbstractTemplate<String> template = new AbstractTemplate<String>(trace) {
            @Override
            protected String call() {
                orderService.orderItem(itemId);
                return "ok";
            }
        };

        return template.execute("OrderController.request()");
    }
}

 

실제 사용하는 예시 코드를 보면, 익명 내부 클래스를 이용해 제네릭 타입으로 실제 반환 타입을 String으로 전달하고 생성자에 주입할 LogTrace의 객체를 전달한다. 그리고 구현해야 하는 'call()' 메서드에 변경되는 비즈니스 로직이 들어간다. 

 

이 추상클래스가 가지는 'execute()' 메서드가 'call()' 메서드가 반환하는 값을 그대로 반환하므로 최종적으로 'execute()' 메서드를 호출한다.

 

 

이렇게 공통 부분을 한 곳에서 공통으로 처리하고 변경되는 부분만을 그때그때 유연하게 변경하는 방법 중 하나인 템플릿 메서드 패턴을 알아보았다.

728x90
반응형
LIST

'Design Pattern' 카테고리의 다른 글

Template Callback Pattern  (0) 2023.12.12