
안녕하세요!! 우기의 정신차린 취준일기입니다. 거의 빠짐없이 하루에 한개씩 올렸는데요... 일주일에 2~3개 올리는걸로 생각해두겠습니다 너무 정신이없네요...😂 😂 😂
자 오늘은 전략 패턴에 대해서 설명해보도록 하겠습니다 !! 템플릿 메서드 패턴의 단점을 보완할수있을거같다고 제가 전 포스팅에 말씀을 드렸습니다 부모가 바뀌면 자식이 바뀔수있다는 상속의 단점을 보완한 전략패턴..! 한번 알아볼까요?
그 전에 알아두자 !!!
변하지 않을 부분, 변하는 부분을 강조를 하는이유는 변하지않을 부분은 로그 추적기를 칭하고, 변하는 부분을 나의 핵심로직에 대해 이야기하는것이다.
변하지 않을 부분 = 보조로직 = 로그추적기 = 핵심 기능을 보조하기 위해 제공되는 기능
변할수 있는 부분 = 핵심로직 = 주문 로직 = 해당객체가 제공하는 고유의 기능
❤ 전략 패턴이란?
❤ 템플릿 콜백 패턴의 등장
❤ 결과와 회고
❤ 전략패턴이란?
전략 패턴(Strategy Pattern)이란 변하지 않을 부분 'Context'라는 곳에 두고, 변하는 부분을 Strategy 라는 인터페이스를 만들고 해당 인터페이스를 구현하여 문제를 해결하는 패턴 방식이다.
결국은 특정 컨텍스트에서 알고리즘을 별도로 분리하는 설계 방법을 의미 한다는말이다. 템플릿 메소드 패턴과 별반 다를것이 없다. 하지만 다른점은 딱 하나. 상속이 아닌 위임으로 해결하는것이 전략패턴의 큰 차이점이다.

GOF 디자인 패턴에선 알고리즘 제품군(StrategyLogic1,2)을 각각 정의하고 각각을 캡슐화하고 상호 교환하게 만든다. 이 전략을 사용한다면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할수있다.
🧡 전략 패턴을 구현해보자. [ 개별 클래스, 익명 내부 클래스, 람다]
변하는 부분(Strategy)과 변하지 않는 부분(Context)을 구현해보자
💛 Strategy Interface
이 인터페이스는 변하는 알고리즘 역할을한다.
public interface Strategy {
void call();
}
💛 [개별 클래스] StragtegyLogic1,2
인터페이스의 구현체 (실제 알고리즘의 존재)
@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 실행");
}
}
💛 ContextV1
이 클래스는 변하지 않는 역할로 Context로 지칭한다
/*
* 필드에 전략을 보관하는 방식
* */
//변하지 않은 로직을 가지고있는 템플릿 역할을 하는 코드(클래스)이다. 전략패턴에서는 이것을 컨텍스트(문맥)이라고한다.
//쉽게 이야기하자면 컨텍스트는 크게 변하지 않지만, 그 문맥 속에서 'strategy'를 통해 일부 전략이 변경된다 생각하면 된다.
//코드의 장점 ContextV1도 변하지 않는점이 있지만, Strategy를 주입받아 사용하기때문에 Strategy의 변경이 존재하여도 Context에도
//문제가 생기지않는다. -> 의존관계 주입이 전략 패턴이다.
@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);
}
}
🧡전략 패턴 호출 방식
💚 개별 클래스로 호출 하기
@Test
void strategyV1(){
StrategyLogic1 strategyLogic1 = new StrategyLogic1();
ContextV1 contextV1 = new ContextV1(strategyLogic1);
contextV1.execute();
StrategyLogic2 strategyLogic2 = new StrategyLogic2();
ContextV1 contextV2 = new ContextV1(strategyLogic2);
contextV2.execute();
}
💚 익명 내부 클래스 로 호출 하기
@Test
void strategyV2(){
Strategy strategyLogic1 = new Strategy() {
@Override
public void call() {
log.info("비지니스 로직1 실행");
}
};
ContextV1 contextV1 = new ContextV1(strategyLogic1);
log.info("strateLogic1={}",strategyLogic1.getClass());
contextV1.execute();
Strategy strategyLogic2 = new Strategy() {
@Override
public void call() {
log.info("비지니스 로직2 실행");
}
};
ContextV1 contextV2 = new ContextV1(strategyLogic2);
log.info("strateLogic2={}",strategyLogic2.getClass());
contextV2.execute();
}
💚 직접 파라미터로 Strategy를 호출 하기 (전략 패턴 파라미터 실행)
@Test
void strategyV3(){
ContextV1 contextV1 = new ContextV1(new Strategy() {
@Override
public void call() {
log.info("비지니스 로직1 실행");
}
});
contextV1.execute();
ContextV1 contextV2 = new ContextV1(new Strategy() {
@Override
public void call() {
log.info("비지니스 로직2 실행");
}
});
contextV2.execute();
}
💚 람다 형식으로 호출하기 (전략 패턴 파라미터 실행)
@Test
void strategyV4(){
ContextV1 contextV1 = new ContextV1(() -> log.info("비지니스 로직1 실행"));
contextV1.execute();
ContextV1 contextV2 = new ContextV1(() -> log.info("비지니스 로직2 실행"));
contextV2.execute();
}
💛 실행에 대한 예시 그림 (전략 패턴 실행, 전략 패턴 파라미터 실행)
전략 패턴 실행

1. 클라이언트가 strategy 주입을한다 (변하는 알고리즘을 채택하여 인터페이스 구현체를 만듦)
2. Context를 실행한다 (변하지 않는 구문을 실행시킨다)
3. execute() 실행
4. strategy.call() 을 실행 ( 이때 변하지 않는 구문에서 변하는 알고리즘을 채택하여 실행시킨다)
5. execute() 종료
전략 패턴 파라미터 실행

1. Context를 실행한다 (Strategy를 파라미터로 같이 전달)
3. execute() 실행
4. strategy.call() 을 실행 ( 이때 변하지 않는 구문에서 변하는 알고리즘을 채택하여 실행시킨다)
5. execute() 종료
❤ 템플릿 콜백 패턴의 등장
💚 콜백이란?
ContextV2는 변하지 않을 템플릿 역할을 한다. 그리고 변하는 부분을 파라미터로 넘어온 'Strategy'의 코드를 실행해서 처리한다. 이렇게 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 콜백이라한다.
💚 자세하게 알고싶은 콜백이란?
프로그래밍에서 콜백 또는 콜애프터함수는 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 말한다.
콜백을 넘겨받는 코드는 콜백을 필요에 따라 즉시 실행할수도, 나중에 실행할수도잇다.
context.execute(()-> log.info("콜백 함수 실행")); <- 콜백함수 사용
💚쉽게 알고싶은 콜백이란?
쉽게이야기해서 callback은 코드가 호출(=call)은 되는데 코드를 넘겨준 곳의 뒤(back)에서 실행된다는 의미이다.
ContextV2 예제에서 콜백은 Strategy이다.
여기에서는 클라이언트에서 직접 Strategy를 실행하는것이 아닌 클라이언트가 Context2.execute(..)를 실행할때 Strategy를 넘겨주고,
ContextV2뒤에서 Strategy가 실행한다.
💚콜백 언제부터 쓰이고, 왜 쓰는가?
자바언어에서 실행 가능한 코드를 인수로 넘기려면 객체가 필요하다 (그 예시가 ContextV2Test의 익명 내부 클래스)
자바8부터는 객체를 감싼 람다를 사용할수있다. 자바8이전에는 보통 하나의 메소드를 가진 인터페이스를 구현하고, 주로 익명 내부 클래스를 사용
최근에는 주로 람다 함수를 사용한다.
💚템플릿 콜백 패턴이란?
스프링에서는 ContextV2와 같은 방식의 전략패턴을 템플릿 콜백 패턴이라고한다. 전략 패턴에서는 Context가 템플릿 역할을 하고, Strategy부분이 콜백으로 넘어온다 생각하면 된다.
참고로 템플릿 콜백 패턴은 GOF패턴은 아니고, 스프링 내부에서 이런 방식을 자주 사용하기 때문에 스프링안에서만 이렇게 부른다. 전략패턴에서 템플릿과 콜백 부분이 강조된 패턴이라고 생각하면된다.
example ) JdbcTemplate, RestTemplate, TransactionTemplate ...
즉 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);
}
}
💚CallBackTest
@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 실행");
}
});
}
익명 내부 클래스나 람다 식을 이용하여 템플릿 콜백 패턴함수를 구현하였다.
🧡 템플릿 콜백 패턴 실제로 구현해보자
💛 TraceCallBack<T>
public interface TraceCallback<T>{
T call();
}
💛 TraceTemplate
public class TraceTemplate {
private final LogTrace trace;
public TraceTemplate(LogTrace trace) {
this.trace = trace;
}
//message 데이터와 콜백함수는 TraceCallback을 전달받는다. (call임)
public <T> T execute(String message, TraceCallback<T> callback){
TraceStatus status = null;
try {
status = trace.begin(message);
//로직 호출
//여기서 back이 이루어짐
T result =callback.call();
trace.end(status);
return result;
} catch (Exception e)
{
trace.exception(status, e);
throw e;
}
}
}
💛 OrderController
private final OrderServiceV5 orderServiceV5;
private final TraceTemplate template;
//private final LogTrace trace;
//@Autowired가 필요없는 이유 생성자가 하나라서 (싱글톤이라는소리)
public OrderControllerV5(OrderServiceV5 orderServiceV5, LogTrace trace) {
this.orderServiceV5 = orderServiceV5;
//LogTrace가 의존관계 주입을 받는다. 받으면서 필요한 Trace 템플릿을 여기서 생성을한다.
//그렇지 않다면 매번 호출할때마다 new을 생성해야하기때문에 싱글톤패턴으로 만들어두는게 낫다.
//근데 애초에 TraceTemplate를 처음부터 스프링 빈으로 등록하고 주입받아도된다. 이부분은 선택이다.
this.template = new TraceTemplate(trace);
}
@GetMapping("/v5/request")
public String request(String itemId) {
return template.execute("OrderController.request()", new TraceCallback<>() {
@Override
public String call() {
orderServiceV5.orderItem(itemId);
return "ok";
}
});
}
💚 OrderController : TraceTemplate을 사용하여 생성자를 만들어 한 객체를 사용한다.
💛 OrderService
@Service
public class OrderServiceV5 {
//private final HelloTraceV2 trace;
//이제 이건 사용하지않고 스프링 빈으로 등록된것을 사용할것임
private final OrderRepositoryV5 orderRepositoryV0;
private final TraceTemplate template;
public OrderServiceV5(OrderRepositoryV5 orderRepositoryV0, LogTrace trace) {
this.orderRepositoryV0 = orderRepositoryV0;
this.template = new TraceTemplate(trace);
}
public void orderItem(String itemId) {
//익명 내부 클래스
// template.execute("OrderService.orderItem()", new TraceCallback<>() {
// @Override
// public Object call(){
// orderRepositoryV0.save(itemId);
// return null;
// }
// });
//익명 내부 클래스를 람다로 전환
template.execute("OrderService.orderItem()", () -> {
orderRepositoryV0.save(itemId);
return null;
});
}
}
💛 OrderRepository
@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.request()", () -> {
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생");
}
sleep(1000);
return null;
});
// template.execute("OrderRepository.request()", new TraceCallback<>() {
// @Override
// public Object call() {
// 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();
}
}
}
💛 실행결과는 어떨까?

❤결론과 회고
💚 전략 패턴을 사용한 후기
이번엔 변하는 코드와 변하지 않는 코드를 분리하고, 더 적은 코드로 로그 추적기를 적용하였다.
템플릿 메서드 패턴, 전략 패턴, 템플릿 콜백 패턴까지 진행하면서 변하는코드, 변하지않는코드로 분리하였다.
최종적으로 템플릿 콜백 패턴을 적용하여 콜백으로 람다를 사용해서 코드사용도 최소화로 만들어보았다.
💚 배웠던 패턴들의 한계
하지만 지금까지 설명한 방식의 한계는 아무리 최적화를 해도 결국 로그 추적기를 적용하기 위해서 원본 코드를 수정해야한다는 점이다.
클래스가 수백개면 수백개를 더힘들게 수정하는가? 더힘든것을 덜힘들게 수정하는가의 차이일뿐인것이다. 본질적으로 코드를 다 수정해야하는것은
마찬가지이다.
💚 그다음은 어떻게 할것인가?
결국 원본코드를 손대지 않고 로그 추적기를 적용할수있는 방법에대해서 알아봐도록 해보자 그것을 알기위해서는 프록시 개념부터 이해해야한다고 생각한다.
참고!!!!
지금까지 설명한 방식은 실제 스프링 안에서 많이 사용되는 방식이다. "XXXTemplate"를 만난다면 이번에 학습한 내용을 떠올리면서
어떻게 돌아가는지 쉽게 이해할수 있을것이다.
'🧶Spring > Tech-Stack' 카테고리의 다른 글
| [Tech-Stack] 템플릿 메서드 패턴이란 ? (1) | 2023.11.22 |
|---|---|
| [Tech Stack] 로그 추적기란? - 3(완료) (1) | 2023.11.22 |
| [Tech Stack] 로그 추적기란? - 2 (1) | 2023.11.21 |
| [Tech Stack] 로그 추적기란? - 1 (0) | 2023.11.20 |