로그 추적기 도입 후
//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]
→ "작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기합니다. 템플릿 메서드를 사용하면 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의할 수 있습니다."
부모 클래스에 알고리즘의 골격인 템플릿을 정의하고, 일부 변경되는 로직은 자식 클래스에 정의하는 것이다.
자식 클래스가 알고리즘의 전체 구조를 변경하지 않고, 특정 부분만 재정의할 수 있다.
결국 상속과 오버라이딩을 통한 다형성으로 문제를 해결하는 것
템플릿 메서드 패턴의 단점 : 상속 (의존관계 문제)
- 자식 클래스가 부모 클래스와 컴파일 시점에 강하게 결합되는 문제가 있다.
- 부모 클래스를 수정하면, 자식 클래스에도 영향을 줄 수 있다.
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 를 실행할 때 마다 전략을 인수로 전달한다.
★ 정리
- ContextV1 은 필드에 Strategy 를 저장하는 방식
선 조립, 후 실행
Context 를 실행하는 시점에는 이미 조립이 끝났기 때문에 전략을 신경쓰지 않고 단순히 실행만 하면 된다. - 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 |