[Advanced-3] 템플릿 메서드 패턴과 콜백 패턴

2025. 12. 31. 19:43·Spring/Core

1. 템플릿 메서드 패턴 - 시작

1.1. 문제 상황

 지금까지 로그 추적기를 성공적으로 개발하였다. 요구사항을 만족함은 물론, 파라미터 전송의 불편함을 해결하기 위해 쓰레드 로컬(ThreadLocal)까지 도입하였다. 하지만 실제 프로젝트에 적용하려는 단계에서 개발자들의 강한 반대에 부딪혔다. 가장 큰 이유는 본연의 비즈니스 로직보다 부가적인 로그 코드가 더 비대해졌기 때문이다.

1.2. 로그 추적기 도입 전 vs 후 비교

1.2.1. 도입 전 - V0 코드

// OrderControllerV0 코드
@GetMapping("/v0/request")
public String request(String itemId) {
    orderService.orderItem(itemId);
    return "ok";
}

// OrderServiceV0 코드
public void orderItem(String itemId) {
    orderRepository.save(itemId);
}

1.2.2. 도입 후 - V3 코드

// OrderControllerV3 코드
@GetMapping("/v3/request")
public String request(String itemId) {
    TraceStatus status = null;
    try {
        status = trace.begin("OrderController.request()");
        orderService.orderItem(itemId); // 핵심 기능
        trace.end(status);
    } catch (Exception e) {
        trace.exception(status, e);
        throw e;
    }
    return "ok";
}

// OrderServiceV3 코드
public void orderItem(String itemId) {
    TraceStatus status = null;
    try {
        status = trace.begin("OrderService.orderItem()");
        orderRepository.save(itemId); // 핵심 기능
        trace.end(status);
    } catch (Exception e) {
        trace.exception(status, e);
        throw e;
    }
}

1.3. 핵심 기능 vs 부가 기능

  • 핵심 기능: 해당 객체가 제공하는 고유의 기능이다. (예: orderService의 주문 로직, orderRepository의 저장 로직)
  • 부가 기능: 핵심 기능을 보조하기 위해 제공되는 기능이다. (예: 로그 추적, 트랜잭션, 권한 확인) 부가 기능은 단독으로 사용되지 않으며, 항상 핵심 기능과 결합하여 실행된다.

1.4. 문제점 분석

V0 시절 코드와 비교해서 V3 코드를 보면:

  • V0: 해당 메서드가 실제 처리해야 하는 핵심 기능만 깔끔하게 남아있음
  • V3: 핵심 기능보다 로그를 출력해야 하는 부가 기능 코드가 훨씬 더 많고 복잡함
  • 소위 "배보다 배꼽이 큰 상황"
  • 클래스가 수백 개라면 유지보수가 매우 어려움

1.5. 공통 패턴 발견

V3 코드를 유심히 살펴보면 다음과 같이 동일한 패턴이 있다:

TraceStatus status = null;
try {
    status = trace.begin("message");
    // 핵심 기능 호출
    trace.end(status);
} catch (Exception e) {
    trace.exception(status, e);
    throw e;
}

Controller, Service, Repository의 코드를 잘 보면:

  • 로그 추적기를 사용하는 구조는 모두 동일
  • 중간에 핵심 기능을 사용하는 코드만 다름

1.6. 해결 시도: 메서드 추출

 중복되는 부가 기능을 별도의 메서드로 뽑아내면 중복을 줄일 수 있기 때문에 모든게 해결이 될까? 실제로 부가 기능을 별도의 메서드로 뽑아내려고 시도하면 다음과 같은 기술적 난관에 봉착한다.

1.6.1. 핵심 로직이 중간에 위치한 '샌드위치' 구조

 부가 기능 로직이 핵심 로직의 시작과 끝을 감싸고 있는 형태이기 때문에, 단순한 잘라내기로는 중복을 제거하기 어렵다. 만약 시작 로그와 종료 로그만 각각 추출한다면 다음과 같은 형태가 된다.

public void orderItem(String itemId) {
    TraceStatus status = logBegin("OrderService.orderItem()"); // 시작 추출
    
    orderRepository.save(itemId); // 여전히 남겨진 핵심 로직
    
    logEnd(status); // 종료 추출 (catch 블록은 어떻게 처리할 것인가?)
}

이 방식은 try-catch 블록 자체를 분리하지 못했으므로, 모든 메서드에 여전히 예외 처리 코드를 직접 작성해야 한다. 결과적으로 중복 제거 효과가 미미하다.

1.6.2. 변수 공유(Scope) 및 생명주기 문제

try 블록에서 생성된 status 변수는 catch 블록의 trace.exception()과 정상 종료 시의 trace.end()에서 모두 사용되어야 한다. 만약 부가 기능을 쪼개서 추출하면 이 변수의 유효 범위(Scope)가 깨지게 되어, 값을 전달하는 과정이 비정상적으로 복잡해진다.

1.6.3. 범용적인 공통 메서드 제작의 불가능

try-catch를 포함하여 부가 기능을 통째로 추출하려 해도 문제가 발생한다.

private void wrapLogging() {
    TraceStatus status = null;
    try {
        status = trace.begin("...");
        // ??? 여기서 어떤 핵심 로직을 실행해야 할지 결정할 수 없음
        trace.end(status);
    } catch (Exception e) {
        trace.exception(status, e);
        throw e;
    }
}

 추출된 메서드 내부에서 각기 다른 핵심 로직(orderRepository.save, orderService.orderItem 등)을 실행할 방법이 없기 때문이다. 결국 이 메서드는 특정 로직에 종속되거나, 아무것도 할 수 없는 껍데기 코드가 된다.

1.7. 문제 해결: 템플릿 메서드 패턴의 도입

 좋은 설계는 변하는 것과 변하지 않는 것을 분리하는 것이다. 단순히 코드를 잘라내는 '메서드 추출' 방식의 한계를 극복하기 위해서는 "코드 블록 자체를 통째로 넘길 수 있는 방법"이 필요하다.

 

 템플릿 메서드 패턴(Template Method Pattern)은 부모 클래스에 변하지 않는 템플릿(부가 기능)을 정의하고, 변하는 부분(핵심 로직)은 자식 클래스에서 재정의(Overriding)하도록 하여 이 문제를 해결한다.


2. 템플릿 메서드 패턴 - 예제1

2.1. 기본 예제 코드

package hello.advanced.trace.template;

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

2.2. 실행 결과

비즈니스 로직1 실행
resultTime=5
비즈니스 로직2 실행
resultTime=1

2.3. 문제점 분석

  • logic1()과 logic2()는 시간을 측정하는 부분과 비즈니스 로직을 실행하는 부분이 함께 존재
  • 변하는 부분: 비즈니스 로직
  • 변하지 않는 부분: 시간 측정

3. 템플릿 메서드 패턴 - 예제2

3.1. AbstractTemplate (템플릿 클래스)

package hello.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={}", resultTime);
    }

    protected abstract void call();
}

3.2. 템플릿 메서드 패턴 개념

  • 템플릿: 기준이 되는 거대한 틀
  • 변하지 않는 부분을 템플릿에 몰아둠
  • 변하는 부분은 별도로 호출해서 해결 (call() 메서드)
  • 부모 클래스에 변하지 않는 템플릿 코드를 두고, 변하는 부분은 자식 클래스에 두고 상속과 오버라이딩 사용

3.3. SubClassLogic1 (구현 클래스)

package hello.advanced.trace.template.code;

import lombok.extern.slf4j.Slf4j;

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

3.4. SubClassLogic2 (구현 클래스)

package hello.advanced.trace.template.code;

import lombok.extern.slf4j.Slf4j;

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

3.5. 테스트 코드

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

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

3.6. 실행 결과

비즈니스 로직1 실행
resultTime=0
비즈니스 로직2 실행
resultTime=1

3.7. 작동 원리

  1. template1.execute() 호출
  2. 템플릿 로직인 AbstractTemplate.execute() 실행
  3. 중간에 call() 메서드 호출
  4. call()은 오버라이딩되어 있으므로 SubClassLogic1.call() 실행
  5. 다형성을 사용해서 변하는 부분과 변하지 않는 부분을 분리

4. 템플릿 메서드 패턴 - 예제3

4.1. 익명 내부 클래스 사용

템플릿 메서드 패턴은 SubClassLogic1, SubClassLogic2처럼 클래스를 계속 만들어야 하는 단점이 있다. 익명 내부 클래스를 사용하면 이런 단점을 보완할 수 있다.

/**
 * 템플릿 메서드 패턴, 익명 내부 클래스 사용
 */
@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();
}

4.2. 실행 결과

클래스 이름1 class hello.advanced.trace.template.TemplateMethodTest$1
비즈니스 로직1 실행
resultTime=3
클래스 이름2 class hello.advanced.trace.template.TemplateMethodTest$2
비즈니스 로직2 실행
resultTime=0
  • 자바가 임의로 만들어주는 익명 내부 클래스 이름: TemplateMethodTest$1, TemplateMethodTest$2
  • 별도의 자식 클래스를 만들지 않아도 됨

5. 템플릿 메서드 패턴 - 적용1

5.1. AbstractTemplate (로그 추적기용)

package hello.advanced.trace.template;

import hello.advanced.trace.TraceStatus;
import hello.advanced.trace.logtrace.LogTrace;

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

5.2. 주요 특징

  • <T> 제네릭 사용: 반환 타입 정의
  • 객체 생성 시 LogTrace trace 전달 받음
  • 로그 출력할 message 파라미터로 전달 받음
  • 템플릿 코드 중간에 call() 메서드로 변하는 부분 처리
  • abstract T call(): 변하는 부분 처리 메서드 (상속으로 구현)

5.3. v3 → v4 복사 및 적용

5.3.1. 복사 과정

  1. hello.advanced.app.v4 패키지 생성
  2. 클래스 복사
    • v3.OrderControllerV3 → v4.OrderControllerV4
    • v3.OrderServiceV3 → v4.OrderServiceV4
    • v3.OrderRepositoryV3 → v4.OrderRepositoryV4
  3. 의존관계 변경
  4. @GetMapping("/v4/request")로 매핑 정보 변경

5.3.2. OrderControllerV4

package hello.advanced.app.v4;

import hello.advanced.trace.logtrace.LogTrace;
import hello.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<>(trace) {
            @Override
            protected String call() {
                orderService.orderItem(itemId);
                return "ok";
            }
        };
        return template.execute("OrderController.request()");
    }
}

5.3.3. OrderServiceV4

package hello.advanced.app.v4;

import hello.advanced.trace.logtrace.LogTrace;
import hello.advanced.trace.template.AbstractTemplate;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

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

5.3.4. OrderRepositoryV4

package hello.advanced.app.v4;

import hello.advanced.trace.logtrace.LogTrace;
import hello.advanced.trace.template.AbstractTemplate;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

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

5.4. 실행 결과

[aaaaaaaa] OrderController.request()
[aaaaaaaa] |-->OrderService.orderItem()
[aaaaaaaa] | |-->OrderRepository.save()
[aaaaaaaa] | |<--OrderRepository.save() time=1004ms
[aaaaaaaa] |<--OrderService.orderItem() time=1006ms
[aaaaaaaa] OrderController.request() time=1007ms

6. 템플릿 메서드 패턴 - 적용2

6.1. 코드 비교 분석

// OrderServiceV0 코드: 핵심 기능만 있음
public void orderItem(String itemId) {
    orderRepository.save(itemId);
}

// OrderServiceV3 코드: 핵심 기능과 부가 기능이 함께 섞여 있음
public void orderItem(String itemId) {
    TraceStatus status = null;
    try {
        status = trace.begin("OrderService.orderItem()");
        orderRepository.save(itemId); // 핵심 기능
        trace.end(status);
    } catch (Exception e) {
        trace.exception(status, e);
        throw e;
    }
}

// OrderServiceV4 코드: 핵심 기능과 템플릿을 호출하는 코드가 섞여 있음
AbstractTemplate<Void> template = new AbstractTemplate<>(trace) {
    @Override
    protected Void call() {
        orderRepository.save(itemId);
        return null;
    }
};
template.execute("OrderService.orderItem()");

6.2. 개선 효과

  • V4는 템플릿 메서드 패턴을 사용한 덕분에 핵심 기능에 좀 더 집중할 수 있게 됨
  • 변하는 코드와 변하지 않는 코드를 명확하게 분리
  • 로그 출력 템플릿 역할을 하는 변하지 않는 코드는 AbstractTemplate에 담아둠
  • 변하는 코드는 자식 클래스(익명 내부 클래스)로 분리

6.3. 좋은 설계란?

좋은 설계라는 것은 무엇일까? 진정한 좋은 설계는 변경이 일어날 때 자연스럽게 드러난다.

변경 시나리오 비교:

  1. V4 (템플릿 메서드 패턴 적용): 로그를 남기는 로직 변경 시 AbstractTemplate 코드만 변경
  2. V3 (템플릿 없음): 로그를 남기는 로직 변경 시 모든 클래스를 찾아서 고쳐야 함 (수백 개 클래스라면 끔찍함)

6.4. 단일 책임 원칙(SRP) 준수

V4는 단순히 템플릿 메서드 패턴을 적용해서 소스코드 몇 줄을 줄인 것이 전부가 아니다. 로그를 남기는 부분에 단일 책임 원칙(SRP)을 지킨 것이다.

  • SRP: 한 클래스는 하나의 책임만 가져야 한다
  • 적용: 변경 지점을 하나로 모아서 변경에 쉽게 대처할 수 있는 구조를 만듦
  • 결과: 로그 추적 로직 변경이 한 곳에서만 발생

7. 템플릿 메서드 패턴 - 정의

7.1. GOF 정의

"작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기합니다. 템플릿 메서드를 사용하면 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의할 수 있습니다." [GOF]

7.2. 풀이

  • 부모 클래스에 알고리즘의 골격인 템플릿을 정의
  • 일부 변경되는 로직은 자식 클래스에 정의
  • 자식 클래스가 알고리즘의 전체 구조를 변경하지 않고, 특정 부분만 재정의 가능
  • 상속과 오버라이딩을 통한 다형성으로 문제 해결

7.3. 템플릿 메서드 패턴의 한계

7.3.1. 상속의 문제점

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

  1. 강한 결합: 자식 클래스가 부모 클래스와 컴파일 시점에 강하게 결합
  2. 의존관계 문제: 자식 클래스 입장에서는 부모 클래스의 기능을 전혀 사용하지 않음에도 불구하고 의존

7.3.2. 의존성 분석

public class SubClassLogic1 extends AbstractTemplate {
    // 부모 클래스의 기능을 전혀 사용하지 않는데도 AbstractTemplate에 강하게 의존
    @Override
    protected void call() {
        log.info("비즈니스 로직1 실행");
    }
}
  • 강한 의존: extends 다음에 바로 부모 클래스가 코드상에 지정
  • 불필요한 의존: 부모 클래스 기능을 사용하지 않는데도 부모 클래스를 알아야 함
  • 변경 영향도: 부모 클래스를 수정하면 자식 클래스에도 영향을 줄 수 있음

7.3.3. UML 관점

  • UML에서 상속을 받으면 삼각형 화살표가 자식 → 부모를 향함
  • 이는 의존관계를 반영하는 것
  • 자식 클래스는 부모 클래스에 강하게 의존

7.3.4. 추가 단점

  1. 복잡성 증가: 상속 구조 사용 → 별도의 클래스나 익명 내부 클래스 생성 필요
  2. 유연성 부족: 실행 시점에 전략을 변경하기 어려움

7.4. 자식이 부모의 메서드를 호출하는가?

❓질문: 템플릿 메서드 패턴에서는 자식이 부모에게 의존적인데, 부모의 메서드를 전혀 사용하지 않는다는 문제가 있다고 하잖아. 근데 실제로는 자식 객체에서는 call() 오버라이딩해서 메서드 재구성한 뒤, 템플릿(부모) 객체의 execute()를 호출하니까 자식도 결국에는 부모의 메서드 호출하는거 아닌가?

❗답변: 이 부분은 미묘하지만 중요한 차이점이 있다.

  1. 의존성의 방향:
    • 자식 클래스는 extends AbstractTemplate를 통해 컴파일 타임에 부모 클래스에 강하게 의존한다.
    • 이는 코드 구조적 의존성이다.
  2. 메서드 호출 흐름:
    // 클라이언트 코드
    AbstractTemplate template1 = new SubClassLogic1();
    template1.execute();  // 1. 부모의 execute() 호출
    
    // AbstractTemplate의 execute()
    public void execute() {
        // ... 변하지 않는 로직 ...
        call();  // 2. 자식의 call() 호출 (다형성)
        // ... 변하지 않는 로직 ...
    }
    실제 호출 흐름은:
    • 클라이언트 → template1.execute() (부모 메서드)
    • 부모의 execute() 내부 → call() (자식 메서드, 다형성)
  3. 핵심 문제:
    • 구조적 의존성: 자식 클래스가 부모 클래스를 모르고 싶어도 extends로 인해 알 수밖에 없음
    • 컴파일 타임 결합: 부모 클래스 변경 시 자식 클래스도 재컴파일 필요
    • 상속의 오용: "is-a" 관계가 아닌 "has-a" 관계를 구현하기 위해 상속 사용
  4. 더 나은 설계:
    // 나쁜 예: 상속 사용
    class Child extends Parent {
        void call() { /* 구현 */ }
    }
    
    // 좋은 예: 컴포지션 사용 (전략 패턴)
    class Context {
        private Strategy strategy;
        void execute() {
            // ... 변하지 않는 로직 ...
            strategy.call();
            // ... 변하지 않는 로직 ...
        }
    }
    

결론: 자식 클래스는 부모의 execute() 메서드를 직접 호출하지 않지만, extends를 통해 구조적으로 강하게 결합되어 있다. 이게 문제의 본질이다. 실행 시 메서드 호출은 클라이언트가 부모의 execute()를 호출하고, 그 내부에서 다형성으로 자식의 call()이 호출되는 구조인 것이다.

7.5. 대안: 전략 패턴(Strategy Pattern)

템플릿 메서드 패턴과 비슷한 역할을 하면서 상속의 단점을 제거할 수 있는 디자인 패턴이 바로 전략 패턴(Strategy Pattern)이다.

전략 패턴 특징:
1) 상속 대신 컴포지션(합성) 사용
2) 런타임에 전략 변경 가능
3) 느슨한 결합(Loose Coupling)
4) 인터페이스 기반 프로그래밍

8. 전략 패턴 - 시작

8.1. 기본 예제 복습

전략 패턴의 이해를 돕기 위해 템플릿 메서드 패턴에서 만들었던 동일한 예제를 사용해보자.

8.1.1. ContextV1Test

package hello.advanced.trace.strategy;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

@Slf4j
public class ContextV1Test {
    @Test
    void strategyV0() {
        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);
    }
}

8.1.2. 실행 결과

비즈니스 로직1 실행
resultTime=5
비즈니스 로직2 실행
resultTime=1

잘 동작하면 동일한 문제를 전략 패턴으로 풀어보자.


9. 전략 패턴 - 예제1

9.1. 전략 패턴 개념

 템플릿 메서드 패턴은 부모 클래스에 변하지 않는 템플릿을 두고, 변하는 부분을 자식 클래스에 두어서 상속을 사용해서 문제를 해결했다. 전략 패턴은 변하지 않는 부분을 Context라는 곳에 두고, 변하는 부분을 Strategy라는 인터페이스를 만들고 해당 인터페이스를 구현하도록 해서 문제를 해결한다. 상속이 아니라 위임으로 문제를 해결하는 것이다.

 

전략 패턴에서:

  • Context: 변하지 않는 템플릿 역할
  • Strategy: 변하는 알고리즘 역할

GOF 디자인 패턴에서 정의한 전략 패턴의 의도:

알고리즘 제품군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만들자. 전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.

9.2. Strategy 인터페이스

package hello.advanced.trace.strategy.code.strategy;

public interface Strategy {
    void call();
}

이 인터페이스는 변하는 알고리즘 역할을 한다.

9.3. StrategyLogic1

package hello.advanced.trace.strategy.code.strategy;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class StrategyLogic1 implements Strategy {
    @Override
    public void call() {
        log.info("비즈니스 로직1 실행");
    }
}

변하는 알고리즘은 Strategy 인터페이스를 구현하면 된다. 여기서는 비즈니스 로직1을 구현했다.

9.4. StrategyLogic2

package hello.advanced.trace.strategy.code.strategy;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class StrategyLogic2 implements Strategy {
    @Override
    public void call() {
        log.info("비즈니스 로직2 실행");
    }
}

비즈니스 로직2를 구현했다.

9.5. ContextV1

package hello.advanced.trace.strategy.code.strategy;

import lombok.extern.slf4j.Slf4j;

/**
 * 필드에 전략을 보관하는 방식
 */
@Slf4j
public class ContextV1 {
    private Strategy strategy;

    public ContextV1(Strategy strategy) {
        this.strategy = strategy;
    }

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

 ContextV1은 변하지 않는 로직을 가지고 있는 템플릿 역할을 하는 코드이다. 전략 패턴에서는 이것을 컨텍스트(문맥)이라 한다. 쉽게 이야기해서 컨텍스트(문맥)는 크게 변하지 않지만, 그 문맥 속에서 strategy를 통해 일부 전략이 변경된다 생각하면 된다.

 

 Context는 내부에 Strategy strategy 필드를 가지고 있다. 이 필드에 변하는 부분인 Strategy의 구현체를 주입하면 된다. 전략 패턴의 핵심은 Context는 Strategy 인터페이스에만 의존한다는 점이다. 덕분에 Strategy의 구현체를 변경하거나 새로 만들어도 Context 코드에는 영향을 주지 않는다.

 

 어디서 많이 본 코드 같지 않은가? 그렇다. 바로 스프링에서 의존관계 주입에서 사용하는 방식이 바로 전략 패턴이다.

9.6. ContextV1Test - 추가

/**
 * 전략 패턴 적용
 */
@Test
void strategyV1() {
    Strategy strategyLogic1 = new StrategyLogic1();
    ContextV1 context1 = new ContextV1(strategyLogic1);
    context1.execute();

    Strategy strategyLogic2 = new StrategyLogic2();
    ContextV1 context2 = new ContextV1(strategyLogic2);
    context2.execute();
}

 전략 패턴을 사용해보자. 코드를 보면 의존관계 주입을 통해 ContextV1에 Strategy의 구현체인 strategyLogic1을 주입하는 것을 확인할 수 있다. 이렇게 해서 Context 안에 원하는 전략을 주입한다. 이렇게 원하는 모양으로 조립을 완료하고 난 다음에 context1.execute()를 호출해서 context를 실행한다.

9.7. 전략 패턴 실행 과정

  1. Context에 원하는 Strategy 구현체를 주입한다.
  2. 클라이언트는 context를 실행한다.
  3. context는 context 로직을 시작한다.
  4. context 로직 중간에 strategy.call()을 호출해서 주입 받은 strategy 로직을 실행한다.
  5. context는 나머지 로직을 실행한다.

9.8. 실행 결과

StrategyLogic1 - 비즈니스 로직1 실행
ContextV1 - resultTime=3
StrategyLogic2 - 비즈니스 로직2 실행
ContextV1 - resultTime=0

10. 전략 패턴 - 예제2

10.1. 익명 내부 클래스 사용

전략 패턴도 익명 내부 클래스를 사용할 수 있다.

10.1.1. ContextV1Test - 추가

/**
 * 전략 패턴 익명 내부 클래스1
 */
@Test
void strategyV2() {
    Strategy strategyLogic1 = new Strategy() {
        @Override
        public void call() {
            log.info("비즈니스 로직1 실행");
        }
    };
    log.info("strategyLogic1={}", strategyLogic1.getClass());
    ContextV1 context1 = new ContextV1(strategyLogic1);
    context1.execute();

    Strategy strategyLogic2 = new Strategy() {
        @Override
        public void call() {
            log.info("비즈니스 로직2 실행");
        }
    };
    log.info("strategyLogic2={}", strategyLogic2.getClass());
    ContextV1 context2 = new ContextV1(strategyLogic2);
    context2.execute();
}

10.1.2. 실행 결과

ContextV1Test - strategyLogic1=class hello.advanced.trace.strategy.ContextV1Test$1
ContextV1Test - 비즈니스 로직1 실행
ContextV1 - resultTime=0
ContextV1Test - strategyLogic2=class hello.advanced.trace.strategy.ContextV1Test$2
ContextV1Test - 비즈니스 로직2 실행
ContextV1 - resultTime=0

실행 결과를 보면 ContextV1Test$1, ContextV1Test$2와 같이 익명 내부 클래스가 생성된 것을 확인할 수 있다.

10.2. ContextV1Test - 추가 (익명 내부 클래스2)

/**
 * 전략 패턴 익명 내부 클래스2
 */
@Test
void strategyV3() {
    ContextV1 context1 = new ContextV1(new Strategy() {
        @Override
        public void call() {
            log.info("비즈니스 로직1 실행");
        }
    });
    context1.execute();

    ContextV1 context2 = new ContextV1(new Strategy() {
        @Override
        public void call() {
            log.info("비즈니스 로직2 실행");
        }
    });
    context2.execute();
}

익명 내부 클래스를 변수에 담아두지 말고, 생성하면서 바로 ContextV1에 전달해도 된다.

10.3. ContextV1Test - 추가 (람다)

/**
 * 전략 패턴, 람다
 */
@Test
void strategyV4() {
    ContextV1 context1 = new ContextV1(() -> log.info("비즈니스 로직1 실행"));
    context1.execute();

    ContextV1 context2 = new ContextV1(() -> log.info("비즈니스 로직2 실행"));
    context2.execute();
}

 익명 내부 클래스를 자바8부터 제공하는 람다로 변경할 수 있다. 람다로 변경하려면 인터페이스에 메서드가 1개만 있으면 되는데, 여기에서 제공하는 Strategy 인터페이스는 메서드가 1개만 있으므로 람다로 사용할 수 있다.

10.4. 정리

 지금까지 일반적으로 이야기하는 전략 패턴에 대해서 알아보았다. 변하지 않는 부분을 Context에 두고 변하는 부분을 Strategy를 구현해서 만든다. 그리고 Context의 내부 필드에 Strategy를 주입해서 사용했다.

10.4.1. 선 조립, 후 실행

 여기서 이야기하고 싶은 부분은 Context의 내부 필드에 Strategy를 두고 사용하는 부분이다. 이 방식은 Context와 Strategy를 실행 전에 원하는 모양으로 조립해두고, 그 다음에 Context를 실행하는 선 조립, 후 실행 방식에서 매우 유용하다.

 

 Context와 Strategy를 한번 조립하고 나면 이후로는 Context를 실행하기만 하면 된다. 우리가 스프링으로 애플리케이션을 개발할 때 애플리케이션 로딩 시점에 의존관계 주입을 통해 필요한 의존관계를 모두 맺어두고 난 다음에 실제 요청을 처리하는 것과 같은 원리이다.

10.4.2. 단점

 이 방식의 단점은 Context와 Strategy를 조립한 이후에는 전략을 변경하기가 번거롭다는 점이다. 물론 Context에 setter를 제공해서 Strategy를 넘겨 받아 변경하면 되지만, Context를 싱글톤으로 사용할 때는 동시성 이슈 등 고려할 점이 많다. 그래서 전략을 실시간으로 변경해야 하면 차라리 이전에 개발한 테스트 코드처럼 Context를 하나 더 생성하고 그곳에 다른 Strategy를 주입하는 것이 더 나은 선택일 수 있다. 이렇게 먼저 조립하고 사용하는 방식보다 더 유연하게 전략 패턴을 사용하는 방법은 없을까?


11. 전략 패턴 - 예제3

 이번에는 전략 패턴을 조금 다르게 사용해보자. 이전에는 Context의 필드에 Strategy를 주입해서 사용했다. 이번에는 전략을 실행할 때 직접 파라미터로 전달해서 사용해보자.

11.1. ContextV2

package hello.advanced.trace.strategy.code.strategy;

import lombok.extern.slf4j.Slf4j;

/**
 * 전략을 파라미터로 전달 받는 방식
 */
@Slf4j
public class ContextV2 {
    public void execute(Strategy strategy) {
        long startTime = System.currentTimeMillis();
        // 비즈니스 로직 실행
        strategy.call(); // 위임
        // 비즈니스 로직 종료
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime={}", resultTime);
    }
}

ContextV2는 전략을 필드로 가지지 않는다. 대신에 전략을 execute(..)가 호출될 때마다 항상 파라미터로 전달받는다.

11.2. ContextV2Test

package hello.advanced.trace.strategy;

import hello.advanced.trace.strategy.code.strategy.ContextV2;
import hello.advanced.trace.strategy.code.strategy.Strategy;
import hello.advanced.trace.strategy.code.strategy.StrategyLogic1;
import hello.advanced.trace.strategy.code.strategy.StrategyLogic2;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

@Slf4j
public class ContextV2Test {
    /**
     * 전략 패턴 적용
     */
    @Test
    void strategyV1() {
        ContextV2 context = new ContextV2();
        context.execute(new StrategyLogic1());
        context.execute(new StrategyLogic2());
    }
}

 Context와 Strategy를 '선 조립 후 실행'하는 방식이 아니라 Context를 실행할 때마다 전략을 인수로 전달한다. 클라이언트는 Context를 실행하는 시점에 원하는 Strategy를 전달할 수 있다. 따라서 이전 방식과 비교해서 원하는 전략을 더욱 유연하게 변경할 수 있다.

 

 테스트 코드를 보면 하나의 Context만 생성한다. 그리고 하나의 Context에 실행 시점에 여러 전략을 인수로 전달해서 유연하게 실행하는 것을 확인할 수 있다.

11.3. 전략 패턴 파라미터 실행 과정

  1. 클라이언트는 Context를 실행하면서 인수로 Strategy를 전달한다.
  2. Context는 execute() 로직을 실행한다.
  3. Context는 파라미터로 넘어온 strategy.call() 로직을 실행한다.
  4. Context의 execute() 로직이 종료된다.

11.4. ContextV2Test - 추가 (익명 내부 클래스)

/**
 * 전략 패턴 익명 내부 클래스
 */
@Test
void strategyV2() {
    ContextV2 context = new ContextV2();
    context.execute(new Strategy() {
        @Override
        public void call() {
            log.info("비즈니스 로직1 실행");
        }
    });
    context.execute(new Strategy() {
        @Override
        public void call() {
            log.info("비즈니스 로직2 실행");
        }
    });
}

여기도 물론 익명 내부 클래스를 사용할 수 있다. 코드 조각을 파라미터로 넘긴다고 생각하면 더 자연스럽다.

11.5. ContextV2Test - 추가 (람다)

/**
 * 전략 패턴 익명 내부 클래스2, 람다
 */
@Test
void strategyV3() {
    ContextV2 context = new ContextV2();
    context.execute(() -> log.info("비즈니스 로직1 실행"));
    context.execute(() -> log.info("비즈니스 로직2 실행"));
}

람다를 사용해서 코드를 더 단순하게 만들 수 있다.

11.6. 정리: ContextV1 vs ContextV2 (매뉴얼 북 비유) (★)

전략 패턴의 핵심인 Strategy(전략)를 '특정 업무를 수행하는 방법이 적힌 매뉴얼 북'이라고 비유하면 두 방식의 차이가 명확해진다.

11.6.1. ContextV1 (필드 저장 방식: "전담 요원")

  • 특징: 특정 매뉴얼 북을 아예 손에 들고 태어난(생성된) 전담 요원과 같다.
  • 작동: 요원을 부를 때 이미 어떤 매뉴얼을 쓸지 정해져 있다. 사용자는 그저 "실행해!"(execute())라고만 하면 된다.
  • 유연성: 요원이 든 매뉴얼을 바꾸려면 요원을 새로 고용하거나, 요원이 든 책을 뺏고 새 책을 쥐여줘야 하므로 번거롭다.

11.6.2. ContextV2 (파라미터 전달 방식: "전문 대행사")

  • 특징: 어떤 매뉴얼이든 시키는 대로 수행할 능력이 있는 전문 대행사와 같다.
  • 작동: 대행사를 부를 때 매번 매뉴얼 북을 함께 던져준다. "이 매뉴얼대로 실행해!"(execute(strategy))라고 지시한다.
  • 유연성: 대행사 하나만 불러놓고, 그때그때 필요한 매뉴얼 북(전략)만 갈아 끼우면 되므로 매우 유연하다.

11.7. '유연성'의 개념적 차이: 관계 vs 실행

  • ContextV1(관계 중심): 객체 간의 '관계'를 미리 설정하는 방식이다. 스프링의 의존관계 주입(DI)과 같으며, 한 번 조립되면 그 관계가 유지된다.
  • ContextV2(실행 중심): '실행' 시점의 유연성에 집중한다. 단 하나의 컨텍스트 객체만 생성해 두고, 실행하는 시점에 원하는 코드 조각(매뉴얼 북)을 전달한다. 컨텍스트는 아무런 상태를 가지지 않으므로 멀티쓰레드 동시성 문제에서도 자유롭다.

12. 템플릿 콜백 패턴(전략 패턴의 변형) - 시작 (★★)

 ContextV2는 변하지 않는 템플릿 역할을 한다. 그리고 변하는 부분은 파라미터로 넘어온 Strategy의 코드를 실행해서 처리한다. 이렇게 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 콜백(callback)이라 한다.

12.1. 콜백(Callback): "내가 안 해요, 당신이 대신해 주세요"

콜백의 핵심은 "실행권을 남에게 맡긴 매뉴얼 북"이다.

  • 비유: 사용자는 call() 메서드라는 매뉴얼 북을 직접 실행하지 않는다. 대신 "이 로직은 이렇게 돌아가야 해요"라는 내용만 적어서 Context(템플릿)에게 던져준다.
  • 작동: Context는 자신의 공통 업무(로그 시작, 시간 측정 등)를 하다가, 사용자가 준 매뉴얼 북이 필요한 시점에 비로소 그 안의 call()을 호출(Call)한다.
  • 의미: 내가 코드를 호출하는 것이 아니라, 넘겨준 코드가 나중에 뒤(Back)에서 호출되므로 콜백(Callback)이라 부른다.

12.2. 왜 '템플릿 콜백 패턴'이라고 부르는가?

이 패턴은 사실 전략 패턴(Strategy Pattern) 중에서도 "파라미터로 전략을 넘겨주는 방식(ContextV2)"을 스프링에서 특별히 부르는 이름이다.

  • 템플릿(Template): 변하지 않는 거대한 틀(예: ContextV2.execute()). 시간 측정이나 리소스 반납 등 반복되는 로직이 담겨 있다.
  • 콜백(Callback): 그 틀 안에서 매번 변하는 로직(예: Strategy). 파라미터로 전달되는 코드 조각이다.

즉, "변하지 않는 틀(템플릿)을 실행하면서, 변하는 부분(콜백)을 인자로 전달하여 나중에 실행하게 하는 방식"이기에 템플릿 콜백 패턴이라 한다.

12.3. 구조적 이해 (ContextV2 다시 보기)

// Template 역할 (틀)
public void execute(Strategy strategy) {
    long startTime = System.currentTimeMillis();
    
    // 이 시점에 넘겨받은 콜백을 실행한다 (Back)
    strategy.call(); 
    
    long endTime = System.currentTimeMillis();
    // ... 시간 측정 로그 ...
}
  • 클라이언트가 execute(new StrategyLogic1())을 호출하면, execute라는 틀이 돌아가다가 중간에 우리가 넣어준 StrategyLogic1의 코드 조각(메뉴얼)을 실행한다.
  • 자바에서는 코드 조각만 따로 보낼 수 없으므로, 인터페이스를 구현한 객체에 담아서 보내는 것이다. (최근에는 람다를 사용하여 더 간결하게 보낸다.)

12.4. 스프링에서의 활용: XxxTemplate

스프링이 제공하는 JdbcTemplate, RedisTemplate 등 이름 뒤에 Template이 붙은 도구들은 모두 이 패턴을 사용한다.

  • JdbcTemplate의 예:
    • 템플릿(스프링이 제공): DB 연결하기, 트랜잭션 시작하기, SQL 실행 후 자원 닫기, 예외 처리하기 등 (복잡하고 매번 똑같은 작업)
    • 콜백(개발자가 전달): 실행할 SQL 문장, 결과를 어떻게 객체로 바꿀지(Mapping)에 대한 로직
  • 결과: 개발자는 번거로운 DB 연결/해제 로직(템플릿)은 신경 쓰지 않고, 오직 어떤 데이터를 가져올 것인지(콜백)에만 집중할 수 있다.

13. 템플릿 콜백 패턴 - 예제

템플릿 콜백 패턴을 직접 구현해 보자. 구현에 앞서 우리가 이전 전략 패턴 예제에서 사용했던 용어들이 어떻게 변화하는지 정리하는 것이 중요하다.

13.0 용어 및 개념의 변화 (Context → Template / Strategy → Callback)

 전략 패턴의 예제였던 ContextV2와 템플릿 콜백 패턴은 구조적으로 동일하다. 하지만 스프링에서는 그 의도를 더욱 명확히 하기 위해 다음과 같이 이름을 바꾸어 부른다.

  • Template (이전의 Context): 변하지 않는 거대한 로직의 '틀'을 의미한다.
  • Callback (이전의 Strategy): 실행 시점에 인수로 전달되는 '매뉴얼 북'이자, 나중에 뒤에서 호출되는 코드 조각을 의미한다.
"V1(전략 패턴)" vs. "xxxTemplate(템플릿 콜백 패턴)"
 V1은 전형적인 전략 패턴으로, 객체 생성 시점에 전략을 미리 주입하여 관계를 고정하는 '선 조립, 후 실행' 방식이다. 마치 특정 매뉴얼 북을 손에 들고 태어난 '전담 요원(Context)'과 같다.XXXTemplate에서 사용하는 방식은 전략 패턴을 변형한 '템플릿 콜백 패턴'이다.실행 시점에 매뉴얼 북을 던져주듯 전략을 동적으로 전달하므로, 고정된 틀을 의미하는 '템플릿(Template)'과 나중에 호출되는 실행 코드를 의미하는 '콜백(Callback)'이라는 용어를 사용한다.Spring에서는 '뒤에서 실행되는 전략'이라는 점에 초점을 두어 Strategy 대신 Callback이라는 인터페이스 명칭을 사용하여 그 성격을 명확히 규정한다.

13.1. Callback 인터페이스

package hello.advanced.trace.strategy.code.template;

/**
 * 콜백 로직을 전달할 인터페이스
 * 전략 패턴의 Strategy와 동일한 역할을 수행한다.
 */
public interface Callback {
    void call();
}

 

13.2. TimeLogTemplate

package hello.advanced.trace.strategy.code.template;
import lombok.extern.slf4j.Slf4j;

/**
 * 변하지 않는 로직을 가진 템플릿
 * 이전의 Context 역할을 수행하며, 파라미터로 콜백(매뉴얼 북)을 받는다.
 */
@Slf4j
public class TimeLogTemplate {
    public void execute(Callback callback) {
        long startTime = System.currentTimeMillis();
        
        // 비즈니스 로직 실행 (전달받은 매뉴얼 북을 뒤에서 호출)
        callback.call(); // 위임
        
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime={}", resultTime);
    }
}

13.3. TemplateCallbackTest

package hello.advanced.trace.strategy;

import hello.advanced.trace.strategy.code.template.Callback;
import hello.advanced.trace.strategy.code.template.TimeLogTemplate;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

@Slf4j
public class TemplateCallbackTest {
    /**
     * 템플릿 콜백 패턴 - 익명 내부 클래스
     */
    @Test
    void callbackV1() {
        TimeLogTemplate template = new TimeLogTemplate();
        template.execute(new Callback() {
            @Override
            public void call() {
                log.info("비즈니스 로직1 실행");
            }
        });

        template.execute(new Callback() {
            @Override
            public void call() {
                log.info("비즈니스 로직2 실행");
            }
        });
    }

    /**
     * 템플릿 콜백 패턴 - 람다
     */
    @Test
    void callbackV2() {
        TimeLogTemplate template = new TimeLogTemplate();
        template.execute(() -> log.info("비즈니스 로직1 실행"));
        template.execute(() -> log.info("비즈니스 로직2 실행"));
    }
}

 별도의 클래스를 만들어서 전달해도 되지만, 콜백을 사용할 경우 익명 내부 클래스나 람다를 사용하는 것이 편리하다. 물론 여러 곳에서 함께 사용되는 경우 재사용을 위해 콜백을 별도의 클래스로 만들어도 된다.


14. 템플릿 콜백 패턴 - 적용

이제 템플릿 콜백 패턴을 애플리케이션에 적용해보자.

14.1. TraceCallback 인터페이스

package hello.advanced.trace.callback;

public interface TraceCallback<T> {
    T call();
}

콜백을 전달하는 인터페이스이다.

  • <T> 제네릭을 사용했다. 콜백의 반환 타입을 정의한다.

14.2. TraceTemplate

package hello.advanced.trace.callback;

import hello.advanced.trace.TraceStatus;
import hello.advanced.trace.logtrace.LogTrace;

public class TraceTemplate {
    private final LogTrace trace;

    public TraceTemplate(LogTrace trace) {
        this.trace = trace;
    }

    public <T> T execute(String message, TraceCallback<T> callback) {
        TraceStatus status = null;
        try {
            status = trace.begin(message);
            // 로직 호출
            T result = callback.call();
            trace.end(status);
            return result;
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;
        }
    }
}

TraceTemplate는 템플릿 역할을 한다.

  • execute(..)를 보면 message 데이터와 콜백인 TraceCallback callback을 전달 받는다.
  • <T> 제네릭을 사용했다. 반환 타입을 정의한다.

14.3. v4 → v5 복사

  1. hello.advanced.app.v5 패키지 생성
  2. 복사
    • v4.OrderControllerV4 → v5.OrderControllerV5
    • v4.OrderServiceV4 → v5.OrderServiceV5
    • v4.OrderRepositoryV4 → v5.OrderRepositoryV5
  3. 코드 내부 의존관계를 클래스를 V5으로 변경
    • OrderControllerV5: OrderServiceV4 → OrderServiceV5
    • OrderServiceV5: OrderRepositoryV4 → OrderRepositoryV5
  4. OrderControllerV5 매핑 정보 변경
    • @GetMapping("/v5/request")
  5. TraceTemplate을 사용하도록 코드 변경

14.4. OrderControllerV5

package hello.advanced.app.v5;

import hello.advanced.trace.callback.TraceCallback;
import hello.advanced.trace.callback.TraceTemplate;
import hello.advanced.trace.logtrace.LogTrace;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderControllerV5 {
    private final OrderServiceV5 orderService;
    private final TraceTemplate template;

    public OrderControllerV5(OrderServiceV5 orderService, LogTrace trace) {
        this.orderService = orderService;
        this.template = new TraceTemplate(trace);
    }

    @GetMapping("/v5/request")
    public String request(String itemId) {
        return template.execute("OrderController.request()", new TraceCallback<>() {
            @Override
            public String call() {
                orderService.orderItem(itemId);
                return "ok";
            }
        });
    }
}
  • this.template = new TraceTemplate(trace): trace 의존관계 주입을 받으면서 필요한 TraceTemplate 템플릿을 생성한다. 참고로 TraceTemplate를 처음부터 스프링 빈으로 등록하고 주입받아도 된다. 이 부분은 선택이다.
  • template.execute(.., new TraceCallback(){..}): 템플릿을 실행하면서 콜백을 전달한다. 여기서는 콜백으로 익명 내부 클래스를 사용했다.

14.5. OrderServiceV5

package hello.advanced.app.v5;

import hello.advanced.trace.callback.TraceTemplate;
import hello.advanced.trace.logtrace.LogTrace;
import org.springframework.stereotype.Service;

@Service
public class OrderServiceV5 {
    private final OrderRepositoryV5 orderRepository;
    private final TraceTemplate template;

    public OrderServiceV5(OrderRepositoryV5 orderRepository, LogTrace trace) {
        this.orderRepository = orderRepository;
        this.template = new TraceTemplate(trace);
    }

    public void orderItem(String itemId) {
        template.execute("OrderService.orderItem()", () -> {
            orderRepository.save(itemId);
            return null;
        });
    }
}
  • template.execute(.., new TraceCallback(){..}): 템플릿을 실행하면서 콜백을 전달한다. 여기서는 콜백으로 람다를 전달했다.

14.6. OrderRepositoryV5

package hello.advanced.app.v5;

import hello.advanced.trace.callback.TraceTemplate;
import hello.advanced.trace.logtrace.LogTrace;
import org.springframework.stereotype.Repository;

@Repository
public class OrderRepositoryV5 {
    private final TraceTemplate template;

    public OrderRepositoryV5(LogTrace trace) {
        this.template = new TraceTemplate(trace);
    }

    public void save(String itemId) {
        template.execute("OrderRepository.save()", () -> {
            // 저장 로직
            if (itemId.equals("ex")) {
                throw new IllegalStateException("예외 발생!");
            }
            sleep(1000);
            return null;
        });
    }

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

앞의 로직과 같다.

14.7. 정상 실행 결과

http://localhost:8080/v5/request?itemId=hello

[aaaaaaaa] OrderController.request()
[aaaaaaaa] |-->OrderService.orderItem()
[aaaaaaaa] | |-->OrderRepository.save()
[aaaaaaaa] | |<--OrderRepository.save() time=1001ms
[aaaaaaaa] |<--OrderService.orderItem() time=1003ms
[aaaaaaaa] OrderController.request() time=1004ms

15. 정리

15.1. 패턴 진화 과정

지금까지 우리는 변하는 코드와 변하지 않는 코드를 분리하고, 더 적은 코드로 로그 추적기를 적용하기 위해 고군분투했다.

진화 과정:

  1. 템플릿 메서드 패턴: 상속을 통해 변하는 부분과 변하지 않는 부분 분리
  2. 전략 패턴: 인터페이스를 통해 변하는 부분을 추상화하고 주입
  3. 템플릿 콜백 패턴: 실행 시점에 콜백을 전달하는 방식으로 유연성 극대화

15.2. 각 패턴 비교

패턴 사용 기술 장점 단점 적합한 상황
템플릿 메서드 패턴 상속, 오버라이딩 간단한 구현, 구조 명확 강한 결합, 컴파일 타임 의존 부모-자식 관계가 명확할 때
전략 패턴 (V1) 인터페이스, 주입 런타임 전략 변경 가능 선 조립 필요, 변경 시 재조립 애플리케이션 초기화 시 전략 설정
전략 패턴 (V2) 인터페이스, 파라미터 실행 시점마다 전략 변경 매번 전략 지정 필요 유연한 전략 변경이 필요할 때
템플릿 콜백 패턴 인터페이스, 람다 코드 간결, 유연성 최대 개념 이해 필요 스프링의 템플릿 클래스 사용

15.3. 한계

그런데 지금까지 설명한 방식의 한계는 아무리 최적화를 해도 결국 로그 추적기를 적용하기 위해서 원본 코드를 수정해야 한다는 점이다. 클래스가 수백 개이면 수백 개를 더 힘들게 수정하는가 조금 덜 힘들게 수정하는가의 차이가 있을 뿐, 본질적으로 코드를 다 수정해야 하는 것은 마찬가지이다.

15.4. 개발자의 욕심

개발자의 게으름에 대한 욕심은 끝이 없다. 수많은 개발자가 이 문제에 대해서 집요하게 고민해왔고, 여러 가지 방향으로 해결책을 만들어왔다. 지금부터 원본 코드를 손대지 않고 로그 추적기를 적용할 수 있는 방법을 알아보자. 그러기 위해서 프록시 개념을 먼저 이해해야 한다.

15.5. 참고

지금까지 설명한 방식은 실제 스프링 안에서 많이 사용되는 방식이다. XxxTemplate를 만나면 이번에 학습한 내용을 떠올려보면 어떻게 돌아가는지 쉽게 이해할 수 있을 것이다.

'Spring > Core' 카테고리의 다른 글

[Advanced-5] 동적 프록시 기술  (0) 2026.01.02
[Advanced-4] 프록시 패턴과 데코레이터 패턴  (0) 2025.12.31
[Advanced-2] 스레드 로컬 - ThreadLocal  (0) 2025.12.31
[Advanced-1] 예제 만들기  (0) 2025.12.30
[Basic-8] 빈 생명주기 콜백과 빈 스코프의 활용  (0) 2025.12.30
'Spring/Core' 카테고리의 다른 글
  • [Advanced-5] 동적 프록시 기술
  • [Advanced-4] 프록시 패턴과 데코레이터 패턴
  • [Advanced-2] 스레드 로컬 - ThreadLocal
  • [Advanced-1] 예제 만들기
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30) N
        • Core (18) N
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[Advanced-3] 템플릿 메서드 패턴과 콜백 패턴
상단으로

티스토리툴바