
@GetMapping("/v3/request")
public String request(String itemId) {
TraceStatus status = null;
try {
status = trace.begin("OrderController.request()");
// 비즈니스 로직 시작
orderService.orderItem(status.getTraceId(), itemId);
// 비즈니스 로직 종료
trace.end(status);
return "ok";
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
}
로그 기능을 붙이기 위해 위아래로 주석이 달린 한 줄의 비즈니스 코드 외의 로그와 관련된 코드를 작성해야 한다.
이를 모두 작성했다 하더라도 만약 로그 추적기가 변경된다면 로그 기능이 붙은 모든 메서드에 대해 수정이 불가피하다.
위의 코드는 변경이 자유롭지 못하다는 단점이 존재한다.
한 가지 희망은 비즈니스 로직(orderService.orderItem())을 제외한 나머지 형태(로그 로직)는 코드 형식이 고정적이라는 것이다.
@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이라는 추상클래스를 만든다.
이 클래스의 메서드 execute()는 내부 코드로 call()을 제외한 시간 체크 기능을 수행한다.
execute() 내부의 call()메서드는 AbstractTemplate의 추상 메서드로 오버라이딩 메서드를 강제한다.
AbstractTemplate은 추상 클래스이므로 이 클래스만으로 객체를 생성해서 execute()를 수행할 수 없으며 call()로직을 사용하기 위해서는 AbstractTemplate을 상속받는 구현체 클래스가 필요하다.
구현체를 만들어보자.
@Slf4j
public class SubClassLogic1 extends AbstractTemplate {
@Override
protected void call() {
log.info("비즈니스 로직1 실행");
}
}
SubClassLogic1클래스는 AbstractTemplate을 상속받은 구현 클래스이다. call메서드를 오버라이딩받아 구현한다.
만약 새로운 저장로직과 조회로직이 존재한다고 가정해보자.
이 두 로직의 수행시간을 재는 로직을 붙이고 싶다면 AbstractTemplate을 상속받아 call에 저장로직을 구현한 구현 클래스 하나, AbstractTemplate을 상속받아 call에 조회로직을 구현한 구현 클래스 하나를 만들어야 한다.
위의 예시를 통해 우리는 프로젝트의 모든 비즈니스 로직 메서드에 시간 경과 코드를 붙이고 싶을 경우 그 숫자만큼 AbstractTemplate을 구현해야 한다는 것을 깨닫는다.
클래스 파일이 지나치게 많아질 것이고 관리가 힘들어질 것이다.
아래와 같이 익명 클래스 방식을 활용해서 이를 약간이나마 해소할 수 있으나 이 역시 많은 작성을 요구한다.
@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();
}
템플릿 메서드 패턴은 상속을 활용하기 때문에 컴파일 시점에 강결합되는 문제가 존재한다.
템플릿 메서드는 인터페이스로 구현할 수 없다.
무조건 추상 클래스로 설계되어야 하는데 그 이유는 추상 클래스만이 그 안에 일반 메서드를 포함할 수 있기 때문이다.(아래 코드 그림의 execute()가 일반 메서드에 해당한다.)

템플릿 메서드 패턴을 사용할 템플릿 추상 클래스의 1번 메서드(execute())는 공통로직을 중점적으로 2번 메서드(변화로직)을 함께 처리한다.
2번 메서드는 추상 메서드로 선언하여 무조건 구현하게 둔다.
call메서드는 추상 메서드이기에 구현에 따라 동적으로 execute()는 다르게 동작할 것이다.
그림과 함께한 설명은 강결합을 이해하기 위해 템플릿 메서드 패턴의 구조를 다시 확인한 과정이다.
abstract class AbstractTemplate {
// 템플릿 메서드 - 고정된 알고리즘 구조
public final void execute() {
시작 시간 체크(); // 공통 로직
call(); // 변화 가능한 로직
종료 시간 체크 및 경과 시간 계산(); // 공통 로직
}
protected abstract void call();
위 코드는 기존 AbstractTemplate의 execute()를 설명을 위해 약간 수정한 부분이다.
공통 로직과 변화 가능한 로직이 실행 로직인 execute()에 섞여있는 모습이다.
템플릿 메서드 패턴을 사용한다면 1번 메서드(공통 로직)에 해당하는 openFile(), closeFile()의 코드는 고정된다. 만약 이 구조를 바꾸려 한다면(예를 들어 closeFile()을 변화 가능한 로직으로 추상 메서드로 바꾼다면) 수많은 템플릿 구현체의 closeFile()을 재작성해야 한다.
그러므로 템플릿 메서드 패턴은 유연함이 부족하다는 단점이 있다. 이렇게 강하게 결합되어 부모 클래스의 수정이 어려운 현상을 강결합이라 한다.
전략 패턴은 interface를 사용한다. abstract 클래스를 사용한 템플릿 메서드 패턴과 달리 전략 패턴은 인터페이스안에는 변화가 가능한 로직만 담는다는 것이다. 공통 로직은 외부의 Context라는 임의의 클래스에서 실행한다. 인터페이스는 abstarct 클래스의 성격을 가지면서 abstract와 달리 일반 메서드를 포함할 수 없다. 극한의 껍데기이다.(당연히 무조건 구현체가 필요하다.)
스프링 컨테이너는 기본적으로 이 전략패턴으로 설계되어있다.
// 전략 인터페이스 - 변화 가능한 로직 정의
interface PaymentStrategy {
void pay(int amount);
}
// 전략 구현체들 - 변화 가능한 로직의 구체 구현
class CreditCardPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card.");
}
}
class PayPalPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal.");
}
}
// Context 클래스 - 공통 로직 담당, 필요한 전략을 주입받아 실행
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(int amount) {
// 공통 로직
System.out.println("Starting checkout...");
paymentStrategy.pay(amount); // 변화 가능한 로직
System.out.println("Checkout complete.");
}
}
기존 템플릿 메서드 패턴은 결국 변화가능한로직 + 공통로직이 부모-자식으로 묶여있었다. 변화 가능한 로직은 자식 클래스쪽에서 작성되고 부모 클래스를 붙여 사용하는 방식이다.
하지만 전략 패턴은 변화가능한로직(부모-자식)구조를 가지며 공통로직은 관계가 전혀 없는 Context클래스에서 호출하여 사용한다.
또한 전략 패턴은 클라이언트로부터 보통 생성자 주입 방식(스프링 컨테이너를 잘 생각해보자.)으로 변화가능한로직을 담은 인터페이스 타입의 구현체를 주입받는다.
강결합이 특징인 템플릿 메서드 패턴과 비교해보자. 공통 로직에 있는 내용을 변화 로직으로 바꾼다면 전략 패턴에서는 단순히 공통 로직의 한 번의 수정과 변화 로직의 한 번의 수정으로 끝이난다.
템플릿 콜백 패턴은 약간의 제한이 추가된 전략 패턴이다. 전략 패턴과 개념은 동일하지만 이름 설정과 람다 사용을 위한 제한이 존재한다.
일단 공통로직과 변화로직을 담은 객체를 주입받을 기존의 Context는 Template으로 작성법이 바뀐다.(JdbcTemplate , RestTemplate , TransactionTemplate , RedisTemplate와 같은 빈 객체를 본 적이 있을 것이다.)
콜백(callback)은 call(호출)이 back(뒤)에서 실행된다는 뜻으로 각 Template에서 변화로직에 해당하는 부분을 call()로 처리한다.
변화 로직을 담은 인터페이스는 "단 하나의" 메서드를 가진다. (보통 call()로 명명한다.)
그리고 우리는 이 인터페이스를 구현하여 call()안에서 실행될 작업을 정의해준다. 일반적인 전략 패턴에 메서드 숫자의 제한은 없다. 결론적으로 템플릿 콜백 패턴은 전략 패턴과의 다른 점에 있어 템플릿 콜백 패턴이 인터페이스가 단 하나의 메서드만을 가진다는 차이 빼고는 다른 점이 없다.
@GetMapping("/v5/request")
public String request(String itemId) {
LogTemplate logTemplate = new LogTemplate(trace);
/**
* 내부 익명 클래스 사용
*/
logTemplate.execute(new TemplateCallback<Void>() {
@Override
public Void call() {
orderService.orderItem(itemId);
return null;
}
}, "OrderController.request()");
/**
* 람다 사용
*/
logTemplate.execute(() -> {
orderService.orderItem(itemId);
return null;
}, "OrderController.request()");
return "ok";
}
람다를 사용하면 "코드 조각"이 넘어가는 것이 굉장히 직관적으로 잘 보이게 된다. () -> {}의 형식에서 ()에 작성해야할 것은 인터페이스 메서드의 파라미터이다. call() 인터페이스 구현 메서드는 파라미터를 받지 않으므로 빈 상태로 두었으며 {}에 작성할 것은 실행시킬 코드 조각이다.
콜백의 뜻 처럼 실행시킬 코드 조각을 넘겨 TemplateCallback에서 이를 실행시켰다.