⬜섹션3 템플릿 메서드 패턴과 콜백 패턴
◼️템플릿 메서드 패턴 - 시작
- 문제점🤔: 앞서 로그 추적기를 만들면서 핵심기능보다 로그 출력을 위한 부가 기능 코드가 더 많고 복잡해졌다.아래의 동일한 패턴이 중복해서 나타난다.
TraceStatus status = null;
try {
status = trace.begin("message");
//핵심 기능 호출
trace.end(status);
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
◼️템플릿 메서드 패턴 - 예제
아래에서 변하는 부분은 비즈니스 로직, 변하지 않는 부분은 시간 측정이다. 이제 템플릿 메서드 패턴을 사용해서 변하는 부분과 변하지 않는 부분을 분리해보자.
@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);
}
}
🟢템플릿 메서드 패턴
템플릿이라는 틀에 변하지 않는 부분을 몰아두고, 일부 변하는 부분을 별도로 호출해서 해결하는 방식
부모 클래스에는 변하지 않는 템플릿 코드를 둔다. 자식 클래스에 변하는 부분을 두고 상속, 오버라이딩을 사용해 처리한다.
@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 실행");
}
}
/**
* 템플릿 메서드 패턴 적용
* */
@Test
void templateMethodV1() {
SubClassLogic1 template1 = new SubClassLogic1();
template1.execute();
SubClassLogic2 template2 = new SubClassLogic2();
template2.execute();
}
문제점🤔: 위에서는 변경부분을 정의할때마다 SubClassLogic1,2...로 클래스를 계속 만들었어야 했다.
해결책😉:익명 내부 클래스를 사용하면 객체 인스턴스를 생성하면서, 동시에 생성할 클래스를 상속 받은 자식 클래스를 정의할 수 있다.
🪄익명 내부 클래스: 직접 지정하는 이름이 없고 클래스 내부에 선언되는 클래스
/**
* 템플릿 메서드 패턴, 익명 내부 클래스 사용
* */
@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();
}
◼️템플릿 메서드 패턴 - 적용
이제 앞서 만든 애플리케이션의 로그 추적기 로직에 템플릿 메서드 패턴을 적용해보자.
public abstract class AbstractTemplate<T> {
private final LogTrace trace;
protected 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(); // 변하는 부분 처리. 상속으로 구현
}
@Repository
@RequiredArgsConstructor
public class OrderRepositoryV4 {
private final LogTrace trace;
public void save(String itemId) {
AbstractTemplate<Void> template = new AbstractTemplate<Void>(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();
}
}
}
@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()");
}
}
@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()"); // 로그로 남길 메시지 전달
}
}
🟢단일 책임 원칙(SRP)
이로써 로그를 남기는 부분에 단일 책임 원칙(SRP)을 지킨 것이다. 변경 지점을 하나로 모아서 변경에 쉽게 대처할 수 있는 구조를 만들었다.
◼️템플릿 메서드 패턴 - 정의
템플릿 디자인 패턴의 목적:
- 작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기한다.
- 템플릿 메서드를 사용하면 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의할 수 있다.
문제점🤔: 자식 클래스를 작성할 때 부모 클래스 기능을 사용한 것이없다...그럼에도 자식 클래스는 부모클래스를 강하게 의존한다.
해결책😉: 전략 패턴으로 이러한 문제를 해결할 수 있다!
◼️전략 패턴
전략패턴은 변하지않는 부분은 Context에, 변하는 부분은 Strategy라는 인터페이스를 만들고 이 인터페이스를 구현하도록 해서 문제를 해결한다. (상속X,위임)
전략 패턴의 목적:
- 알고리즘 제품군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만든다.
- 전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다
◼️전략 패턴 예제
위에서 봤던 동일한 예제 문제를 풀어보자.
public interface Strategy {
void call();
}
@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 실행");
}
}
@Slf4j
public class ContextV1 { // 변하지 않는 로직을 가지고 있는 템플릿 역할 코드
private Strategy strategy;
//전략 패턴의 핵심; Context 는 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);
위를 완성 후 테스트를 해보면 정상작동한다. Context 안에 원하는 전략을 주입 후 context.execute()으로 context를 실행한다.
/**
* 전략 패턴 적용
*/
@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();
}
◼️전략 패턴 - 익명 내부 클래스 사용
익명 내부클래스를 사용할 수 있다.
/**
* 전략 패턴 익명 내부 클래스1
* */
@Test
void strategyV2() {
Strategy strategyLogic1 = new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
};
//hello.advanced.trace.strategy.ContextV1Test$1
log.info("strategyLogic1={}", strategyLogic1.getClass());
ContextV1 context1 = new ContextV1(strategyLogic1);
context1.execute();
Strategy strategyLogic2 = new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
};
//hello.advanced.trace.strategy.ContextV1Test$2
log.info("strategyLogic2={}", strategyLogic2.getClass());
ContextV1 context2 = new ContextV1(strategyLogic2);
context2.execute();
}
익명 내부 클래스에 변수를 담지않고, 생성하면서 바로 ContextV1에 전달해도 된다.
/**
* 전략 패턴 익명 내부 클래스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();
}
◼️전략 패턴 - 람다 사용
람다로 변경하려면 인터페이스에 메서드가 1개만 있으면 된다. Strategy인터페이스는 메서드가 1개이므로 람다로 사용할 수 있다.
/**
* 전략 패턴, 람다
* */
@Test
void strategyV4() {
ContextV1 context1 = new ContextV1(() -> log.info("비즈니스 로직1 실행"));
context1.execute();
ContextV1 context2 = new ContextV1(() -> log.info("비즈니스 로직2 실행"));
context2.execute();
}
}
◼️전략 패턴 - 파라미터 전달
이전에는 Strategy를 Context 필드에 주입해 사용했지만, 이번에는 직접 파라미터로 전달해서 사용해보자.
아래처럼 전략을 필드로 가지지않고 항상 파라미터로 전달받는다.
/**
* 전략을 파라미터로 전달 받는 방식
* */
public class ContextV2 {
public void execute(Strategy strategy) {
long startTime = System.currentTimeMillis();
//비즈니스 로직 실행
strategy.call(); //위임
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
}
}
@Slf4j
public class ContextV2Test {
/**
* 전략 패턴 적용
* */
@Test
void strategyV1() {
ContextV2 context = new ContextV2();
context.execute(new StrategyLogic1());
context.execute(new StrategyLogic2());
}
}
앞선 것과 같이 익명 클래스, 람다를 사용해 코드를 단순화 할 수 있다.
@Slf4j
public class 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 실행");
}
});
}
/**
* 전략 패턴 익명 내부 클래스2, 람다
*/
@Test
void strategyV3() {
ContextV2 context = new ContextV2();
context.execute(() -> log.info("비즈니스 로직1 실행"));
context.execute(() -> log.info("비즈니스 로직2 실행"));
}
}
🟢선 조립, 후 실행
- ContextV1은 필드에 Strategy 를 저장하는 방식으로 전략 패턴을 구사했다.
- 선 조립, 후 실행 방법에 적합하다.
- Context 를 실행하는 시점에는 이미 조립이 끝났기 때문에 전략을 신경쓰지 않고 단순히 실행만 하면 된다.
- ContextV2 는 파라미터에 Strategy를 전달받는 방식으로 전략 패턴을 구사했다.
- 실행할 때 마다 전략을 유연하게 변경할 수 있다.
- 단점 역시 실행할 때 마다 전략을 계속 지정해주어야 한다는 점이다.
◼️템플릿 콜백 패턴 - 시작
🟢콜백
- 다른 코드 인수로서 넘겨주는 실행 가능한 코드.
- 코드가 호출(call)되는데 코드를 넘겨준 곳 뒤에서(back) 실행된다는 뜻이다.
- 콜백을 넘겨받은 코드는 필요할때 해당 콜백을 실행한다.
콜백은 주된 코드 흐름의 나중에 실행된다는 것을 강조한다.
🟢콜백의 실행 순서
- 콜백을 전달: 콜백 함수는 다른 함수의 인수로 전달한다. 이 때, 전달된 콜백 함수는 즉시 실행되지 않고, 어떤 조건이 충족되거나 작업이 완료되면 나중에 호출된다.
- 주 코드 흐름의 뒤에서 실행: 주 코드 흐름은 콜백이 전달된 함수의 나머지 작업을 먼저 수행한다. 그러고 나서 그 함수가 필요로 하는 작업이 끝나면(예: 파일 입출력 완료, 네트워크 요청 완료 등) 콜백 함수가 호출된다.
- 이는 주 코드 흐름이 완료된 후에(혹은 그 이후 시점에) 실행되므로 "뒤에서"라는 표현을 사용하는것이다.뒤에서 실행한다는건 즉,주 코드가 자신이 할 일을 다 끝낸 후에, 또는 비동기 작업이 끝난 후에, 콜백이 실행된다는 것을 뜻한다.
🔶비동기적 작업 결과 콜백 처리 예시
interface Callback {
void onComplete(String result);
}
class DataProcessor { // 데이터 처리
// 데이터 처리 후 콜백 호출
public void fetchData(Callback callback) {
new Thread(() -> {
try {
// 데이터 가져오는 중...
Thread.sleep(2000); // 2초 대기
String data = "서버에서 가져온 데이터";
System.out.println("data = " + data);
// 작업 완료 후 콜백 호출
callback.onComplete(data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
public class CallBackExample {
public static void main(String[] args) {
DataProcessor processor = new DataProcessor();
// 콜백 구현
Callback myCallback = new Callback() {
@Override
public void onComplete(String data) {
System.out.println("onComplete:" + data);
}
};
// fetchData 메서드에 콜백 전달
processor.fetchData(myCallback);
}
}
ContextV2는 변하지 않는 템플릿 역할은 한다. 변하는 부분은 파라미터 Strategy 코드를 실행해 처리한다.여기에서 콜백은 Strategy다.
/**
* 전략 패턴 적용
* */
@Test
void strategyV1() {
ContextV2 context = new ContextV2();
context.execute(new StrategyLogic1());
context.execute(new StrategyLogic2());
}
◼️템플릿 콜백 패턴 - 예제
context를 Template으로, Strategy를 Callback으로 바꾸면 템플릿 콜백 패턴이다.
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);
}
}
@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 실행"));
}
}
◼️템플릿 콜백 패턴 - 적용
템플릿 콜백 패턴을 애플리케이션에 적용해보자.
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;
}
}
}
@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();
}
}
}
@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;
});
}
}
@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";
}
});
}
}
'Spring' 카테고리의 다른 글
[Spring] 스프링 핵심 원리 - 고급편 섹션5 동적 프록시 기술 (0) | 2024.08.04 |
---|---|
[Spring] 스프링 핵심 원리 - 고급편 섹션4 프록시 패턴과 데코레이터 패턴 (0) | 2024.07.27 |
[Spring] 스프링 핵심 원리 - 고급편 섹션2 쓰레드 로컬 - ThreadLocal (0) | 2024.07.19 |
[Spring] 스프링 핵심 원리 - 고급편 섹션1 예제 만들기 (0) | 2024.07.17 |
[Spring] 스프링 DB 2편 - 데이터 접근 핵심 원리 섹션11 스프링 트랜잭션 전파2 - 활용 (0) | 2024.07.14 |