본문 바로가기
Spring/Advanced

템플릿 메서드 패턴과 콜백 패턴

by JHyun0302 2023. 8. 13.
728x90

로그 추적기 도입 후

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

 

 

 

 

 

문제 : 핵심 기능보다 로그를 출력해야 하는 부가 기능 코드가 훨씬 많다.

 

 

 

 

 

★ 핵심 기능 vs 부가 기능

  • 핵심 기능은 해당 객체가 제공하는 고유의 기능이다.                    ex) orderService 의 핵심 기능은 주문 로직이다. 
  • 부가 기능은 핵심 기능을 보조하기 위해 제공되는 기능이다.         ex)  로그 추적 로직, 트랜잭션 기능이 있다. 

 

 

 

 

★ 변하는 것과 변하지 않는 것을 분리

  • 여기서 핵심 기능 부분은 변하고, 로그 추적기를 사용하는 부분은 변하지 않는 부분이다. 이 둘을 분리해서 모듈화해야 한다.

 

 

 

 

 

 

 

 

 

 

 

 


템플릿 메서드 패턴 - 예제

 

 

템플릿 메서드 패턴 구조

 

 

 

AbstractTemplate

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

 

템플릿이라는 틀에 변하지 않는 부분을 몰아둔다. 그리고 일부 변하는 부분을 별도로 호출해서 해결한다.

AbstractTemplate : 하나의 템플릿 ≒ 부가 기능, call() : 변하는 부분 ≒ 핵심 기능

 

 

 

 

SubClassLogic1

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

 

 

 

 

SubClassLogic2

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

 

 

 

 

 

 

 

 

템플릿 메서드 패턴 인스턴스 호출

 

 

 

 

템플릿 메서드 패턴은 다형성을 사용해서 변하는 부분과 변하지 않는 부분을 분리하는 방법이다.

 

 

 

 

 

 

 

 

 

 


익명 내부 클래스 사용하기

 

 

 

 

문제 : 템플릿 메서드 패턴은 SubClassLogic1 , SubClassLogic2 처럼 클래스를 계속 만들어야 한다.

보완 : 익명 내부 클래스를 사용

 

 

 

익명 내부 클래스를 사용하면 객체 인스턴스를 생성하면서 동시에 생성할 클래스를 상속 받은 자식 클래스를 정의할 수 있다.

 

 

 

 

 

 

 

 

 


템플릿 메서드 패턴 - 적용

 

 

 

 

AbstractTemplate

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 : 템플릿 역할(부모 클래스)
  • abstract T call() : 상속으로 구현해야 함.(변하는 부분을 처리)

 

 

 

 

 

 

OrderController

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

 

  • AbstractTemplate<String> : 제네릭을 String 으로 설정했다. 따라서 AbstractTemplate 의 반환 타입은 String 이 된다.

 

 

 

 

 

OrderService

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

 

  • AbstractTemplate<Void> : 제네릭에서 반환 타입이 필요한데, 반환할 내용이 없으면 Void 타입을 사용하고 null 을 반환하면 된다.
  • 제네릭은 기본타입인 void,int 등 선언 할 수 없다.

 

 

 

 

OrderRepository

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

 

 

 

 

 

단일 책임 원칙(SRP)

  • 로그를 남기는 부분에 단일 책임 원칙(SRP)을 지킨 것이다. 변경 지점을 하나로 모아서 변경에 쉽게 대처할 수 있는 구조를 만든 것

 

 

 

 

 

 

 

 

 


템플릿 메서드 패턴 - 정의

 

 

 

[GOF] 

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

 

 

 

 

 

GOF 템플릿 메서드 패턴 정의

 

 

 

 

부모 클래스에 알고리즘의 골격인 템플릿을 정의하고, 일부 변경되는 로직은 자식 클래스에 정의하는 것이다.

자식 클래스가 알고리즘의 전체 구조를 변경하지 않고, 특정 부분만 재정의할 수 있다.

결국 상속과 오버라이딩을 통한 다형성으로 문제를 해결하는 것

 

 

 

 

 

 

 

템플릿 메서드 패턴의 단점 : 상속 (의존관계 문제)

 

  • 자식 클래스가 부모 클래스와 컴파일 시점에 강하게 결합되는 문제가 있다.
  • 부모 클래스를 수정하면, 자식 클래스에도 영향을 줄 수 있다.
        ex) 자식 클래스 입장에서는 부모 클래스의 기능을 전혀 사용하지 않는다.

 

 

 

 

 

보완 : 전략 패턴 (Strategy Pattern)

 

 

 

 

 

 

 

 

 

 


전략 패턴 - 예제

 

 

 

  • Context :  변하지 않는 부분
  • Strategy : 인터페이스 (변하는 부분)

 

상속이 아니라 위임으로 문제를 해결하는 것

 

 

 

 

[GOF]

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

 

 

 

 

 

 

 

 

Strategy 인터페이스

public interface Strategy {
      void call();
}

 

 

 

 

StrategyLogic1

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

 

 

 

 

 

StrategyLogic2

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

 

 

 

 

 

 

ContextTest

@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 안에 원하는 전략을 주입한다.

 

 

 

 

 

 

전략 패턴 실행

 

 

 

 

 

 


전략 패턴 - 익명 내부 클래스

 

 

 

 

ContextTest

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

→ 익명 내부 클래스가 생성

 

 

 

★ 정리

변하지 않는 부분을 Context, 변하는 부분을 Strategy 를 구현해서 만든다

 

 

 

 

 

 

문제 : 선 조립, 후 실행

Context Strategy 를 조립한 이후에는 전략을 변경하기가 번거롭다. (Context 를 싱글톤으로 사용할 때는 동시성 이슈 고려) 

 

 

 

 

 

 

 

 

 

 

 


전략 패턴  - 파라미터 전달

 

 

Context

/**
* 전략을 파라미터로 전달 받는 방식
*/
@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);
    } 
}

→ Context Strategy '선 조립 후 실행'하는 방식이 아니라 Context 를 실행할 때 마다 전략을 인수로 전달한다.

 

 

 

 

 

 

 

 

전략 패턴 파라미터 실행

 

 

 

 

 

 

 

★ 정리

  1. ContextV1 은 필드에 Strategy 를 저장하는 방식
    선 조립, 후 실행
    Context 를 실행하는 시점에는 이미 조립이 끝났기 때문에 전략을 신경쓰지 않고 단순히 실행만 하면 된다.
  2. ContextV2 는 파라미터에 Strategy 를 전달받는 방식
    실행할 때 마다 전략을 유연하게 변경 가능
    단점 : 실행할 때 마다 전략을 계속 지정해주어야 한다.

 

 

 

 

 

 

 

 

 


템플릿 콜백 패턴 - 시작

 

 

 

변하지 않는 템플릿 역할변하는 부분은 파라미터로 넘어온 Strategy 의 코드를 실행해서 처리한다.

→ 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 콜백(callback)이라 한다.

 

 

callback 코드가 호출( call )은 되는데 코드를 넘겨준 곳의 뒤( back )에서 실행된다는 뜻

 

 

 

ex) Context 에서 콜백은 Strategy 이다.
클라이언트에서 직접 Strategy 를 실행하는 것이 아니라, 클라이언트가 ContextV2.execute(..) 를 실행할 때 Strategy 를 넘겨주고, ContextV2 뒤에서 Strategy 가 실행된다.

 

 

 

 

템플릿 콜백 패턴

  • 전략 패턴 : Context (템플릿 역할), Strategy  (콜백)
  • 참고로 템플릿 콜백 패턴은 GOF 패턴은 아니고, 스프링 내부에서 이런 방식을 자주 사용하기 때문에, 스프링 안에서만 이렇게 부른다.
  • 전략 패턴에서 템플릿과 콜백 부분이 강조된 패턴이라 생각하면 된다.
  • 스프링에서는 JdbcTemplate , RestTemplate , TransactionTemplate , RedisTemplate 등 템플릿 콜백 패턴이 사용된다.
  • 스프링에서 이름에 XxxTemplate 가 있다면 템플릿 콜백 패턴으로 만들어져 있다 생각하면 된다.

 

 

 

 

템플릿 콜백 패턴

 

 

 

 

 

 

 

 

 


템플릿 콜백 패턴 - 예제

 

 

  • Context → Template
  • Strategy → Callback

 

 

 

 

Callback - 인터페이스

public interface Callback {
	void call();
}

 

 

 

 

 

 

TimeLogTemplate

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

 

 

 

 

 

 

 

TemplateCallbackTest

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

 

 

 

 

 

 

 

 

 


한계

 

아무리 최적화를 해도 결국 로그 추적기를 적용하기 위해서 원본 코드를 수정해야 한다는 점

 

 

 

해결 방안 : 프록시

 

 

 

반응형

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

스프링이 지원하는 프록시  (0) 2023.08.17
동적 프록시 기술  (0) 2023.08.16
프록시 패턴과 데코레이터 패턴  (0) 2023.08.14
쓰레드 로컬 - ThreadLocal  (0) 2023.08.12
예제 만들기  (0) 2023.08.12