템플릿 메소드 패턴 Template Method Pattern
- 이전 로그 추적기 예제를 보면 V0는 실제 처리해야할 핵심 기능만 있는 반면에 V3는 핵심 기능보다 로그 출력을 위한 부가 기능이 더 많고 복잡하다.
- 핵심 기능 vs 부가 기능
- 핵심 기능 : 해당 객체가 제공하는 고유의 기능이다.
- ex) OrderService 클래스의 orderItem()의 핵심 기능은 주문 데이터를 저장하기 위해 레포지토리를 호출하는 orderRepository.save(itemId)가 핵심 기능이다.
- 부가 기능 : 핵심 기능을 보조하기 위해 제공되는 기능이다.
- 부가 기능은 단독으로 사용되지 않고 핵심 기능과 함께 사용된다.
- ex) 로그 추적기, 트랜잭션 기능
- 핵심 기능 : 해당 객체가 제공하는 고유의 기능이다.
- 좋은 설계는 변하는 것과 변하지 않는 것을 분리하는 것이다.
- 변하는 것과 변하지 않는 것을 분리해서 모듈화 해야한다.
- ex) 로그 추적기 예제에서는 핵심 기능은 변하고 로그 추적기를 사용하는 부분은 변하지 않는 부분이다.
- 템플릿 메소드 패턴(Template Method Pattern)을 사용하면 변하는 부분과 변하지 않는 부분을 분리해서 모듈화할 수 있다.
템플릿 메소드 패턴 - 예제
@Slf4j
public class TemplateMethodTest {
@Test
void templateMethodV0() {
logic1();
logic2();
}
@Test
private void logic1() {
long startTime = System.currentTimeMillis();
//비지니스 로직 실행
log.info("비지니스 로직1 실행");
//비지니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
@Test
private void logic2() {
long startTime = System.currentTimeMillis();
//비지니스 로직 실행
log.info("비지니스 로직2 실행");
//비지니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
}
- 템플릿 메소드 패턴을 위한 예제 코드
- logic1(), logic2()
- 시간을 측정하는 부분과 비지니스 로직을 실행하는 부분이 함께 존재한다.
- 변하는 부분 - 비지니스 로직
- 변하지 않는 부분 - 시간 측정
- logic1(), logic2()
- 템플릿 메소드 패턴: 템플릿(기준이 되는 거대한 틀)을 사용하는 방식으로 템플릿이라는 틀에 변하지 않는 부분을 몰아두고 일부 변하는 부분들을 별도로 호출해서 해결한다.
- 템플릿 메소드 패턴 구조
- 부모(상위)클래스에 변하지 않는 부분을 몰아놓고 변하는 부분은 자식(하위) 클래스에서 오버라이딩하도록 한다.
- 그림에서 execute()는 변하지 않는 부분, call()은 변하는 부분
- 부모(상위)클래스에 변하지 않는 부분을 몰아놓고 변하는 부분은 자식(하위) 클래스에서 오버라이딩하도록 한다.
//템플릿 메소드 패턴 - 부모 클래스
@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();
}
//템플릿 메소드 패턴 - 자식 클래스
@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 실행");
}
}
- 템플릿 메소드 패턴 예제
- 변하지 않는 부분인 시간 측정 로직을 몰아둠으로 하나의 템플릿으로 만들었으며 변하는 부분은 call()을 호출해서 처리한다.
- 템플릿 메소드 패턴은 부모 클래스에 변하지 않는 템플릿 코드를 두고 변하는 부분은 자식 클래스에 두고 상속과 오버라이딩을 사용해서 처리한다.
- 템플릿 메소드 패턴의 단점은 SubClassLogic1, SubClassLogic2와 같이 클래스를 계속 만들어야한다는 단점이 있다.
- 익명 내부 클래스를 사용하면 이런 단점을 보완할 수 있다.
- 익명 내부 클래스 : 객체 인스턴스를 생성하면서 동시에 생성할 클래스를 상속받은 자식 클래스로 정의할 수 있다.
- 익명 내부 클래스는 SubClassLogic1처럼 직접 지정하는 이름이 없고 클래스 내부에 선언되는 클래스이다.
@Test
public 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();
}
- 익명 클래스 사용 예제 코드
템플릿 메소드 패턴 - 적용
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();
}
- 로그 추적기 예제에 템플릿 메소드 패턴 적용
- 템플릿 메소드 패턴에서 부모 클래스이며 템플릿 역할을 한다.
- <T> : 핵심 기능마다 반환 타입이 다르기에 제네릭 타입을 사용
- call() : 변하는 부분(핵심 기능)을 처리하는 메소드로 상속을 통해 구현해야 한다.
@RestController
@RequiredArgsConstructor
public class OrderControllerV4 {
private final OrderServiceV4 orderService;
private final LogTrace trace;
@GetMapping("/v4/request")
public String request(@RequestParam("itemId") String itemId) {
AbstractTemplate<String> template = new AbstractTemplate<>(trace) {
@Override
protected String call() {
orderService.orderItem(itemId);
return "ok";
}
};
return template.execute("OrderController.request()");
}
}
- OrderControllerV4 예제
- AbstractTemplate<String>
- 제네릭을 String으로 설정함에 따라 반환 타입도 String이 된다.
- 익명 내부 클래스 template 사용
- execute()에 로그로 남길 message를 파라미터로 전달
- AbstractTemplate<String>
@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()");
}
}
- OrderServiceV4 예제
- 제네릭 타입에서 반환 값이 없는 경우 void가 아닌 래퍼클래스인 Void를 사용
@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();
}
}
}
- OrderRepositoryV4 예제
- V4 예제는 템플릿 메소드 패턴을 적용해서 소스 코드를 줄였을 뿐만 아니라 로그 추적기 관련 코드에 대해 단일 책임 원칙(SRP)를 지켰다.
- V4 예제는 핵심 기능에 더 집중할 수 있고 핵심 기능과 템플릿을 호출하는 코드가 섞여있다.
- 좋은 설계란?
- 좋은 설계는 변경이 일어날 때 드러난다.
- ex) 템플릿 메소드 패턴을 적용하지 않은 V3 코드에서 로그 추적기 관련 코드를 변경하려면 모든 클래스의 모든 로직을 다 변경해야한다.
템플릿 메소드 패턴을 적용한 V4 코드에서는 템플릿인 AbstractTemplate만 변경하면 된다.
- ex) 템플릿 메소드 패턴을 적용하지 않은 V3 코드에서 로그 추적기 관련 코드를 변경하려면 모든 클래스의 모든 로직을 다 변경해야한다.
- 좋은 설계는 변경이 일어날 때 드러난다.
템플릿 메소드 패턴 - 정의
- 템플릿 메소드 디자인 패턴 목적 : 작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기한다.
- 템플릿 메소드를 사용하면 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의할 수 있다.
- = 부모 클래스에 알고리즘의 골격인 템플릿을 정의하고 일부 변경되는 로직은 자식 클래스에 정의하는 것이다.
자식 클래스는 알고리즘의 전체 구조를 변경하지 않고 특정 부분만 재정의할 수 있다.- 상속과 오버라이딩을 통한 다형성으로 문제 해결
- 템플릿 메소드 패턴은 상속을 사용
- 상속에서 오는 단점을 가지고 있다.
- 컴파일 시점에 자식 클래스가 부모클래스에게 강하게 결합되는 문제가 발생한다.
- 자식 클래스 입장에서는 부모 클래스의 기능을 전혀 사용하지 않아도 템플릿 메소드 패턴을 위해서 부모 클래스를 상속받아야 한다.
- 상속 구조를 사용하기에 별도의 클래스나 익명 내부 클래스를 만들어야한다.
- 컴파일 시점에 자식 클래스가 부모클래스에게 강하게 결합되는 문제가 발생한다.
- 상속을 받는다는 것은 특정 부모 클래스를 의존하고 있다는 것
- 부모 클래스 기능을 사용하든 사용하지 않든 부모 클래스를 강하게 의존하게 된다.
- 강하게 의존한다 = 자식 클래스의 코드에 부모 클래스의 코드가 명확하게 적혀있다.
ex) 자식 클래스 extends 부모 클래스 - UML에서 상속을 받으면 자식 -> 부모를 향하고 있는 것은 의존관계를 반영하는 것
- 자식 클래스 입장에서는 부모 클래스의 기능을 전혀 사용하지 않아도 부모 클래스를 알아야한다.
=> 좋은 설계가 아니다.- 이런 의존관계로 부모 클래스를 수정하면 자식 클래스에도 영향을 줄 수 있다.
- 부모 클래스 기능을 사용하든 사용하지 않든 부모 클래스를 강하게 의존하게 된다.
- 상속에서 오는 단점을 가지고 있다.
- 템플릿 메소드 패턴과 비슷한 역할을 하면서 상속의 단점을 제거한 디자인 패턴이 전략 패턴(Strategy Pattern)이다.
전략 패턴 Stragtegy Pattern
전략 패턴 - 정의
- 템플릿 메소드 패턴과 동일한 예제를 전략 패턴을 사용해서 해결하기
- 전략 패턴 : 변하지 않는 부분을 Context라는 곳에 두고 변하는 부분을 Strategy 라는 인터페이스를 만들고 인터페이스를 구현하도록 한다.
- 상속이 아닌 위임으로 문제를 해결하는 방법이다.
- Context는 변하지 않는 템플릿 역할, Strategy는 변하는 알고리즘의 역할을 한다.
- 전략 패턴 의도 : 알고리즘 제품군 (Strategy)을 정의하고 각각(StrategyLogic1, 2)을 캡슐화해서 상호 교환 가능하게 만들자.
- 전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.
=> Strategy를 호출하는 Context와 독립적으로 의존관계 주입을 통해 알고리즘을 변경
- 전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.
전략 패턴 - 예제
//변하는 알고리즘 Strategy 인터페이스
public interface Strategy {
void call();
}
//Strategy 구현체
@Slf4j
public class StrategyLogic1 implements Strategy {
@Override
public void call() {
log.info("비지니스 로직1 실행");
}
}
@Slf4j
public class StrategyLogic2 implements Strategy {
@Override
public void call() {
log.info("비지니스 로직2 실행");
}
}
- Strategy 인터페이스와 구현체 예제
- Strategy 인터페이스는 변하는 알고리즘 역할을 한다.
- 변하는 알고리즘은 인터페이스를 구현하면 된다.
- Strategy 인터페이스는 변하는 알고리즘 역할을 한다.
/**
* 필드에 전략을 보관하는 방식
*/
@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 예제
- Context는 변하지 않는 로직을 가지고 있는 템플릿 역할을 한다.
- 이것을 전략 패턴에서는 컨텍스트(문맥)이라 한다.
- Context는 내부에 Strategy strategy 필드를 가지고 있다.
- 필드에 변하는 부분인 Strategy 구현체를 주입하면 된다.
- Context는 변하지 않는 로직을 가지고 있는 템플릿 역할을 한다.
- 전략 패턴의 핵심
- Context는 Strategy 인터페이스에만 의존한다.
- Strategy 구현체를 변경하거나 새로 만들어도 Context에는 영향을 주지 않는다.
- 스프링 의존관계 주입에서 사용하는 방식이 전략 패턴이다.
- Context는 Strategy 인터페이스에만 의존한다.
@Test
public void strategyV1() {
StrategyLogic1 strategyLogic1 = new StrategyLogic1();
ContextV1 context1 = new ContextV1(strategyLogic1);
context1.execute();
StrategyLogic2 strategyLogic2 = new StrategyLogic2();
ContextV1 context2 = new ContextV1(strategyLogic2);
context2.execute();
}
@Test
public void strategyV2() {
Strategy strategyLogic1 = new Strategy() {
@Override
public void call() {
log.info("비지니스 로직1 실행");
}
};
ContextV1 context1 = new ContextV1(strategyLogic1);
context1.execute();
Strategy strategyLogic2 = new Strategy() {
@Override
public void call() {
log.info("비지니스 로직2 실행");
}
};
ContextV1 context2 = new ContextV1(strategyLogic2);
context2.execute();
}
@Test
public void strategyV3() {
ContextV1 context1 = new ContextV1(new Strategy() {
@Override
public void call() {
log.info("비지니스 로직1 실행");
}
});
context1.execute();
Strategy strategyLogic2 = new Strategy() {
@Override
public void call() {
log.info("비지니스 로직2 실행");
}
};
ContextV1 context2 = new ContextV1(strategyLogic2);
context2.execute();
}
@Test
public void strategyV4() {
ContextV1 context1 = new ContextV1(() -> log.info("비지니스 로직1 실행"));
context1.execute();
ContextV1 context2 = new ContextV1(() -> log.info("비지니스 로직2 실행"));
context2.execute();
}
- ContextV1 전략 패턴 사용 예제
- 구현 클래스를 생성해서 사용하는 방식V1
- 익명 내부 클래스를 변수에 담아서 사용하는 방식V2
- 익명 내부 클래스를 생성하면서 Context 파라미터로 전달하는 방식V3
- 익명 내부 클래스를 람다로 변경해서 사용하는 방식V4
- 람다를 사용하려면 인터페이스에 메소드가 1개만 있어야한다.
- 의존관계 주입을 통해 ContextV1에 Strategy 구현체를 주입한다.
- Context안에 원하는 전략을 주입해서 원하는 모양으로 조립을 완료하고 Context를 실행한다.
- ContextV1 전략 패턴 실행 순서
- Context에 원하는 Strategy 구현체 주입
- 클라이언트가 Context 실행
- Context 로직인 execute() 실행
- execute() 중간에 strategy.call() 호출해서 주입받은 strategy의 call() 실행
- 이후 나머지 execute() 로직 실행
- ContextV1 전략 패턴은 선 조립, 후 실행의 방식
- Context 내부 필드에 Strategy를 두고 사용하는 경우 Context와 Strategy를 실행 전에 원하는 모양으로 조립해두고 Context를 실행하는 선 조립, 후 실행방식에 적합하다.
- Context와 Strategy를 한 번 조립하고 난 후에는 Context를 실행하기만 하면 된다.
- 단점
- 조립한 이후 Strategy 전략을 변경하기 어렵다.
- setter를 제공해서 변경하려면 Context를 싱글톤으로 사용하는 경우 동시성 이슈와 같이 고려할게 많기 때문에 Context를 하나 더 생성해서 다른 Strategy를 주입하는 것이 더 나은 선택이다.
- 조립한 이후 Strategy 전략을 변경하기 어렵다.
- Context 내부 필드에 Strategy를 두고 사용하는 경우 Context와 Strategy를 실행 전에 원하는 모양으로 조립해두고 Context를 실행하는 선 조립, 후 실행방식에 적합하다.
전략 패턴 - 예제2
/**
* 전략을 파라미터로 전달받는 방식
*/
@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 예제
- 전략을 파라미터로 전달받는 방식
- Strategy 전략을 필드로 가지지 않고 execute()가 호출될 때마다 항상 파라미터로 전달받는다.
- 전략을 파라미터로 전달받는 방식
@Test
public void strategyV1() {
ContextV2 context = new ContextV2();
context.execute(new StrategyLogic1());
context.execute(new StrategyLogic2());
}
- ContextV2 전략 패턴 사용 예제
- 마찬가지로 익명 내부 클래스, 람다 모두 사용 가능
- Context와 Strategy를 선 조립, 후 실행방식이 아닌 Context를 실행할 때마다 전략을 파라미터로 전달한다.
- 클라이언트는 Context를 실행하는 시점에 원하는 Strategy를 전달할 수 있기에 이전과 다르게 원하는 전략을 더욱 유연하게 변경할 수 있다.
- Context도 하나만 생성해서 실행 시점에 여러 전략을 파라미터로 전달해서 유연하게 실행한다.
- ContextV2 전략 패턴 실행 순서
- 클라이언트가 Context를 실행하면서 파라미터로 Strategy를 전달
- Context는 execute() 로직 실행
- Context는 파라미터로 넘어온 strategy의 call() 로직을 실행
- Context의 execute() 나머지 로직 실행 후 종료
- ContextV1 방식 vs ContextV2 방식
- ContextV1
- 필드에 Strategy를 저장하는 방식의 전략 패턴
- 선 조립, 후 실행 방식에 적합
- Context를 실행하는 시점에 이미 조립이 끝났기에 전략을 신경쓰지 않고 실행만 하면 된다.
- ContextV2
- 파라미터에 Strategy를 전달받는 방식의 전략 패턴
- 실행할 때마다 전략을 유연하게 변경 가능하지만 실행할 때마다 전략을 계속 지정해줘야 한다.
- ContextV1
- 지금 해결하고 싶은 문제는 변하는 부분과 변하지 않는 부분을 분리하는 것
- 의존 관계를 설정함으로 선 조립, 후 실행 방식인 ContextV1보다 실행 시점에 원하는 부분만 유연하게 실행 코드 조각을 전달하는 방식인 ContextV2가 지금 해결하는 문제에는 더 적합하다고 볼 수 있다.
- 디자인 패턴에서 중요한 것은 형태(모양)보다도 의도가 중요하다.
템플릿 콜백 패턴 Template Callback Pattern
템플릿 콜백 패턴 - 정의
- 이전 예제인 ContextV2는 변하지 않는 템플릿 역할을 하고 변하는 부분은 파라미터로 전달받은 Strategy의 코드를 실행해서 처리한다.
- 콜백(callback), 콜애프터 함수(call-after function) : 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 의미한다.
- 콜백을 넘겨받는 코드는 콜백을 필요에 따라 즉시 실행할 수도 있고 나중에 실행할 수도 있다.
- callback -> 코드가 호출(call)은 되는데 코드를 넘겨준 곳의 뒤(back)에서 실행된다.
- ex) ContextV2 예제의 context.execute(new StrategyLogic1());에서 new StrategyLogic1() 부분이 콜백이다.
클라이언트가 직접 Strategy를 실행하는 것이 아닌 context.execute(...)를 실행할 때 Strategy를 넘겨주고 ContextV2 뒤에서 Strategy가 실행된다.
- 자바언어에서의 콜백
- 자바에서 실행 가능한 코드를 인수로 넘기려면 객체가 필요하다.
- 자바 8 이전에는 인터페이스를 구현 / 익명 내부 클래스 사용
- 자바 8 이후부터 주로 람다 사용
- 자바에서 실행 가능한 코드를 인수로 넘기려면 객체가 필요하다.
- 스프링에서의 콜백
- ContextV2와 같은 방식의 전략 패턴을 템플릿 콜백 패턴이라 한다.
- 전략 패턴에서 Context가 템플릿 역할을 하고 Strategy가 콜백으로 넘어오는 개념이다.
- 템플릿 콜백 패턴은 GOF 패턴은 아니고 스프링 내부에서 자주 사용하는 방식으로 스프링 안에서만 불린다.
- 템플릿 콜백 패턴은 전략 패턴에서 탬플릿과 콜백 부분이 강조된 패턴이다.
- JdbcTemplate, RestTemplate, TransactionTemplate 등 다양한 템플릿 콜백 패턴이 사용된다.
- 스프링에서 XxxTemplate은 템플릿 콜백 패턴으로 만들어진 것이다.
- ContextV2와 같은 방식의 전략 패턴을 템플릿 콜백 패턴이라 한다.
- 템플릿 콜백 패턴 실행 순서
- Template을 실행하면서 callback 전달
- Template 내부 로직 실행
- callback을 뒤에서 호출
- Template 나머지 로직 실행
템플릿 콜백 패턴 - 예제
public interface Callback {
void call();
}
@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);
}
}
- 템플릿 콜백 패턴 예제
- ContextV2 예제에서 Context -> TimeLogTemplate으로 Strategy -> Callback로 이름만 변경
@Slf4j
public class TemplateCallbackTest {
/**
* 템플릿 콜백 패턴 - 익명 내부 클래스
*/
@Test
public 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
public void callbackV2() {
TimeLogTemplate template = new TimeLogTemplate();
template.execute(() -> log.info("비지니스 로직1 실행"));
template.execute(() -> log.info("비지니스 로직2 실행"));
}
}
- 템플릿 콜백 패턴 사용 예제
- 별도의 클래스를 만들어서 전달해도 되지만 콜백을 사용하는 경우 익명 내부 클래스나 람다를 사용하는 것이 편리하다.
- 여러 곳에서 함께 사용하는 경우 재사용을 위해 콜백을 별도의 클래스로 만들어줘도 된다.
- callbackV1()에서 new Callback(){...} 익명 내부 클래스가 콜백, callbackV2()에서 () -> log.info(...) 람다가 콜백
- 이러한 콜백들은 템플릿(TimeLogTemplate의 execute()) 안에서 (뒤에서) 실행된다.
- 별도의 클래스를 만들어서 전달해도 되지만 콜백을 사용하는 경우 익명 내부 클래스나 람다를 사용하는 것이 편리하다.
템플릿 콜백 패턴 - 적용
public interface TraceCallback<T> {
T call();
}
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()에서 Callback을 전달받는다.
@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(@RequestParam("itemId") String itemId) {
return template.execute("OrderController.request()", new TraceCallback<>() {
@Override
public String call() {
orderService.orderItem(itemId);
return "ok";
}
});
}
}
- OrderControllerV5 예제
- 예제와 같이 LogTrace 의존관계를 주입 받아서 TraceTemplate을 생성해도되고 TraceTemplate을 스프링 빈으로 등록해서 주입받아도 된다.
- 콜백으로 익명 내부 클래스를 사용
@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;
});
}
}
- OrderServiceV5 예제
- 콜백으로 람다 사용
@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();
}
}
}
- OrderRepositoryV5 예제
- 콜백으로 람다 사용
정리
- 템플릿 메소드 패턴
- 작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기함으로 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의할 수 있다.
- 부모(상위) 클래스에 변하지 않는 템플릿 코드를 두고 변하는 부분은 자식(하위) 클래스에 두고 상속과 오버라이딩을 사용해서 처리한다.
- 문제점
- 상속을 사용하는 방식이기에 컴파일 시점에 자식 클래스가 부모 클래스에게 강하게 결합된다.
- 자식 클래스는 부모 클래스를 사용하지 않더라도 상속을 받아야 하기 때문에 부모 클래스를 수정하면 자식 클래스에도 영향을 줄 수 있다.
- 상속을 사용하는 방식이기에 컴파일 시점에 자식 클래스가 부모 클래스에게 강하게 결합된다.
- 전략 패턴
- 변하지 않는 부분을 Context라는 곳에 두고 변하는 부분을 Strategy 라는 인터페이스를 만들고 인터페이스를 구현하도록 한다.
- 상속이 아닌 위임으로 문제를 해결하는 방법이다.
- Context는 변하지 않는 템플릿 역할, Strategy는 변하는 알고리즘의 역할을 한다.
- 전략 패턴 의도
- 알고리즘 제품군 (Strategy)을 정의하고 각각(구현체 - StrategyLogic1, 2)을 캡슐화해서 상호 교환 가능하게 만드는 것
- 전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.
=> Strategy를 호출하는 Context와 독립적으로 의존관계 주입을 통해 알고리즘을 변경
- 전략 패턴 방식
- Context 내부 필드에 Strategy를 저장하는 방식의 전략 패턴
- 선 조립, 후 실행 방식에 적합하다.
- Context를 실행하는 시점에 이미 조립이 끝났기에 전략을 신경쓰지 않고 실행만 하면 된다.
- Context 로직의 파라미터에 Strategy를 전달받는 방식의 전략 패턴
- 실행할 때마다 전략을 유연하게 변경 가능하지만 실행할 때마다 전략을 계속 지정해줘야 한다.
- Context 내부 필드에 Strategy를 저장하는 방식의 전략 패턴
- 변하지 않는 부분을 Context라는 곳에 두고 변하는 부분을 Strategy 라는 인터페이스를 만들고 인터페이스를 구현하도록 한다.
- 템플릿 콜백 패턴
- 특정 코드 조각(콜백)을 템플릿에서 실행하기 위해서 전달해주는 것으로 전략 패턴에서 파라미터로 Strategy를 넘겨주는 방식과 같다.
- 콜백(callback), 콜애프터 함수(call-after function) : 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 의미다.
- 템플릿 뒤에서 콜백 실행
- 탬플릿과 콜백 부분이 강조된 패턴으로 스프링 내부에서 자주 사용되며 스프링에서만 사용되는 용어다.
- 특정 코드 조각(콜백)을 템플릿에서 실행하기 위해서 전달해주는 것으로 전략 패턴에서 파라미터로 Strategy를 넘겨주는 방식과 같다.
출처 : [인프런 김영한 스프링 핵심 원리 - 고급편]
스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런
김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기
www.inflearn.com
'Spring > [인프런 김영한 스프링 핵심 원리 - 고급편]' 카테고리의 다른 글
[인프런 김영한 스프링 핵심 원리 - 고급편] 스프링이 지원하는 프록시 (0) | 2024.12.10 |
---|---|
[인프런 김영한 스프링 핵심 원리 - 고급편] 동적 프록시 기술 (2) | 2024.12.08 |
[인프런 김영한 스프링 핵심 원리 - 고급편] 프록시 패턴과 데코레이터 패턴 (3) | 2024.12.07 |
[인프런 김영한 스프링 핵심 원리 - 고급편] 쓰레드 로컬 - ThreadLocal (0) | 2024.12.03 |
[인프런 김영한 스프링 핵심 원리 - 고급편] 예제 만들기 (1) | 2024.12.01 |