FireDrago

템플릿 메서드 패턴과 적용 본문

프로그래밍/디자인패턴

템플릿 메서드 패턴과 적용

화이용 2024. 5. 10. 13:32

템플릿 메서드 패턴

템플릿 메서드 패턴은 핵심기능과 부가기능을 상속을 통해 분리하는 패턴을 말한다. 

하위 클래스에서 부가 기능만을 오버라이딩하면 되므로 기존 코드를 변경하지 않고도 알고리즘을 쉽게 확장할 수 있다.

@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);
}

위 코드는 핵심기능(로그 출력)과 부가기능 (실행시간)이 함께 섞여있다.

이 코드를 템플릿 메서드 패턴을 사용하여 분리해보자

 

우선 변하지 않는 부가기능 부분(실행시간)을 상위 클래스에서 정의한다. 

핵심기능의 호출은 추상 메서드를 통해 하위 클래스가 구현하도록 만들었다.

protected 접근제어자를 사용하여, 로직을 캡슐화 하면서도, 상속을 통해 확장할 수 있도록 했다.

@Slf4j
public abstract class AbstractTemplate {

    public void execute() {
        long startTime = System.currentTimeMillis();
        // 비즈니스 로직 실행
        call();
        // 비즈니스 로직 종료
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime={}", resultTime);
    }

    // protected : 캡슐화하면서도 상속성 유지 가능, 하위 클래스에서 재사용 및 확장 용이
    protected abstract void call();
}

 

AbstractTemplate 을 상속받은 각각의 클래스들은 핵심기능을 정의한다.

@Slf4j
public class SubClassLogic1 extends AbstractTemplate{

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

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

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

/**
 * 템플릿 메서드 패턴, 익명 내부 클래스 사용
 */
@Test
void templateMethodV2() {
    AbstractTemplate template1 = new AbstractTemplate() {
        @Override
        protected void call() {
            log.info("비즈니스 로직1 실행");
        }
    };
    log.info("클래스 이름1={}", template1.getClass());
    template1.execute();

    AbstractTemplate template2 = new AbstractTemplate() {
        @Override
        protected void call() {
            log.info("비즈니스 로직2 실행");
        }
    };
    log.info("클래스 이름2={}", template2.getClass());
    template2.execute();
}

템플릿 메서드를 사용하여 분리된 로직은 구현 클래스를 생성하거나, 익명클래스를 사용하여 호출할 수 있다.

익명 클래스를 사용하여 템플릿 메서드를 재정의할 수 있다. 이 방식은 코드를 간결하게 만들고,

짧은 코드 블록으로 로직을 구현해야 하는 경우 유용하다.

 

템플릿 메서드 패턴을 사용하게 되면, 부가기능의 로직을 변경하고 싶을때는 AbstractTemplate 만 변경하면된다.

템플릿 없이 모든 부가기능이 각각의 클래스에 함께 정의되어있었다면, 모든 클래스를 다 찾아서 고쳐야 한다.

단일 책임 원칙이 잘 지켜지는 좋은 설계가 되는 것이다.

 

하지만 템플릿 메서드 패턴은 상속을 사용한다. 따라서 상속에서 오는 단점들을 그대로 안고간다. 

특히 자식 클래스가 부모 클래스와 컴파일 시점에 강하게 결합되는 문제가 있다. 

자식 클래스 입장에서는 부모클래스의 기능을 전혀 사용하지 않는다.
그럼에도 불구하고 템플릿 메서드 패턴을 위해 자식 클래스는 부모 클래스를 상속 받고 있다.
따라서 부모 클래스의 기능을 사용하든 사용하지 않든 간에 부모 클래스를 강하게 의존하게 된다. 

이것은 좋은 설계가아니다. 그리고 이런 잘못된 의존관계 때문에 부모 클래스를 수정하면, 자식 클래스에도 영향을 줄 수 있다. 별도의 클래스나 익명 내부 클래스를 만들어야 하는 부분도 복잡하다.

템플릿 메서드 패턴과 비슷한 역할을 하면서 상속의 단점을 제거할 수 있는 디자인 패턴이

전략 패턴(Strategy Pattern)이다.

 

로그 추적기 적용

이전에 로그 추적기를 만들었다. 템플릿 메서드 패턴을 사용하여 로그 추적 기능을 사용하는 기능을 만들어보자

 

public abstract class AbstractTemplate<T> {

    private final LogTrace trace;

    public AbstractTemplate(LogTrace trace) {
        this.trace = trace;
    }
	
    // 제네릭스를 사용하여 동적으로 반환타입 결정할 수 있다.
    public T execute(String message) {
        TraceStatus status = null;
        try {
            status = trace.begin(message);

            T result = call();

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

    protected abstract T call();
}

먼저 로그 추적을 위한 부가기능을 담당하는 AbstractTemplate 클래스를 만들어 준다.

내부적으로 call() 메서드를 호출하고 있는데, call() 메서드는 익명클래스를 통해 호출할 수 있다.

 

컨트롤러에서 로그추적기를 사용하기위한 코드도 만들어보자

@RestController
@RequiredArgsConstructor
public class OrderControllerV4 {

    private final OrderServiceV4 orderService;
    private final LogTrace trace;

    @GetMapping("/v4/request")
    public String request(@RequestParam("itemId") String itemId) {
    	// 익명클래스로 call() 메서드를 오버라이딩했다.
        AbstractTemplate<String> template = new AbstractTemplate<String>(trace) {
            @Override
            protected String call() {
                orderService.orderItem(itemId);
                return "ok";
            }
        };
        // 템플릿을 실행한다. -> 내부에서 call() 호출된다.
        return template.execute("OrderController.request()");
    }
}

 

익명 클래스를 사용하여, 비즈니스 로직을 실행했다.

이렇게 되면 로그 추적을 위한 Trace.begin() 메서드 실행 후에 call() 메서드가 실행되고, Trace.end()메서드가 실행된다.

로그 추적이 잘 작동하게 되는것이다. 

 

하지만 템플릿 메서드 패턴역시 상속의 단점을 가진다. 이를 보완하기 위해서는 전략패턴을 적용할 수 있다.

@Service
@RequiredArgsConstructor
public class OrderServiceV4 {

    private final OrderRepositoryV4 orderRepository;
    private final LogTrace trace;

    public void orderItem(String itemId) {
        AbstractTemplate<Void>template = new AbstractTemplate<>(trace) {
            @Override
            protected Void call() {
                orderRepository.save(itemId);
                return null;
            }
        };
        template.execute("OrderService.orderItem()");
    }
}
@Repository
@RequiredArgsConstructor
public class OrderRepositoryV4 {

    private final LogTrace trace;

    public void save(String itemId) {
        AbstractTemplate<Void> template = new AbstractTemplate<>(trace){
            @Override
            protected Void call() {
                // 저장 로직
                if (itemId.equals("ex")) {
                    throw new IllegalStateException("예외발생!");
                }
                sleep(1000);
                return null;
            }
        };
        template.execute("OrderRepository.save()");
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}