로그 추적기를 만들어 보니 심각한 문제가 보였다.
//로그 추적기 적용 전
@GetMapping("/v1/request")
public String request(String itemId) {
orderService.orderItem(itemId);
return "ok";
}
//로그 추적기 적용 후
@GetMapping("/v1/request")
public String request(String itemId) {
TraceStatus status = null;
try {
status = trace.begin("OrderController.request()");
orderService.orderItem(itemId);
trace.end(status);
return "ok";
} catch (Exception e) {
trace.exception(status, e);
throw e; //예외를 꼭 다시 던져주어야 한다.
}
}
기존의 단일 클래스로 만들었던 로그 추적기를 확장성까지 생각해서 인터페이스 형태로 바꾸었고, 기능도 추가했다.
직접 빈에 등록했던 Component도 빼서, LogConfigure라는 설정 파일을 따로 만들어 관리.
private TraceId traceIdHolder; // 클래스의 필드로 줬음.
// 새롭게 아이디 관리를 위해 만든 메서드들
private void syncTraceId() {
if (traceIdHolder == null) {
traceIdHolder = new TraceId(); // 최초 호출시 새로운 TraceId() 생성
} else {
traceIdHolder = traceIdHolder.createNextId(); // level 증가
}
}
private void releaseTraceId() {
if (traceIdHolder.isFirstLevel()) {
traceIdHolder = null; //destroy
} else {
traceIdHolder = traceIdHolder.createPreviousId(); // level 감소
}
}
기존의 방식과 다르게, sync와 release 메서드를 만들어 필드 동기화를 해주었다. 다만 이 방식을 사용할 시 빈으로 관리되는 스프링에서는 객체의 인스턴스가 싱글톤인데, 동시성 문제가 발생할 수 있다. (traceIdHolder는 전역변수이다.)
트래픽이 적으면 문제가 없어 보일 수 도 있다. 하지만 트래픽이 많아질수록 동시성 문제가 자주 발생하게 될 것이다.
거기다 Java의 Spring을 쓴다면 기본으로 알아야된다.(Why? -> Spring은 기본적으로 Singleton으로 객체를 관리하는 방법을 사용하기 때문이다)
동시성 문제를 해결하기 위한 방법중 하나. ThreadLocal을 사용한다면 Thread 1의 창고가 생기게 되는것이고, Thread 2는 다른 창고를 사용하게 된다.
각 쓰레드가 자기의 창고가 생기는 것이기 때문에 동시성 문제를 해결 가능함.
중요!! 쓰레드 로컬은 쓰고나서 꼭 remove로 threadlocal을 없애야 한다.
-> why? Threadpool을 사용하는 was. 만들어진 Thread안에 threadlocal이 생성되 삭제되지 않으면 계속 남아 있고, 그 threadlocal의 데이터가 남아 있다.
ThreadLocal을 적용하든, 인터페이스를 적용하든 문제점 해결을 하고는 있지만 근본적으로 해결되지 않는 문제가 있다.
-> 바로 비지니스 로직에 남아있는 부가 코드들이다. 로그 추적기를 고치게 되면 그 코드들을 이용하는 클래스가 몇개가 되었든 전부 고쳐줘야 된다.
여기서 우리는 로그 추적기 코드를 살펴보면 똑같은 코드들이 많다는 것을 알것이다. 정작 바뀌는 부분은 기존 핵심코드의 비니지스 로직 부분과 간단한 메시지 정도이다.
TraceStatus status = null;
try {
status = trace.begin("메시지");
// 비지니스 로직 부분
orderService.orderItem(itemId);
// 비지니스 로직 끝
trace.end(status);
return "ok"; // 응답 메시지
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
공통 부분 추출
TraceStatus status = null;
try {
status = trace.begin("메시지");
// 비지니스 로직 부분
trace.end(status);
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
이런 공통적인 부분은 따로 관리하고, 바뀌는 부분들에 대해서만 어떻게 관리할 수 없나?(변하는 것과 변하지 않는 것을 분리하자) -> 이런 부분을 디자인패턴을 적용해 해결 해보자.
// 템플릿 클래스, 로그의 공통 부분 추출.
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();
}
// 상속으로 구현한 변하는 부분 로직
@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을 상속받은 클래스를 구현해서 사용해서 된다.
템플릿 메서드 패턴을 적용해서 소스코드 몇줄을 줄인 것이 전부가 아니다.
로그를 남기는 부분에 단일 책임 원칙(SRP)을 지킨 것이다. 변경 지점을 하나로 모아서 변경에 쉽게 대처할 수 있는 구조를 만든 것이다
Context
라는 곳에 두고, 변하는 부분을 Strategy
라는 인터페이스를 만들고 해당 인터페이스를 구현하도록 해서 문제를 해결한다. 상속이 아니라 위임으로 문제를 해결하는 것이다. 전략 패턴에서 Context
는 변하지 않는 템플릿 역할을 하고, Strategy
는 변하는 알고리즘 역할을 한다.
// context
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);
}
}
//Strategy 인터페이스
public interface Strategy {
void call();
}
// 실제 구현
@Test
@DisplayName("lamda 활용 - 전략패턴")
void strategy() {
ContextV1 context1 = new ContextV1(() -> log.info("비지니스 로직1 실행"));
context1.execute();
ContextV1 context2 = new ContextV1(() -> log.info("비지니스 로직1 실행"));
context2.execute();
}
자식과 부모의 강력한 의존관계가 없이, 위임을 통해 구현하는 방식.
현재는 Context
내부에 Strategy
를 두고 사용중.은 Context
와 Strategy
를 실행 전에 원하는 모양으로 조립해두고, 그 다음에 Context
를
실행하는 선 조립, 후 실행 방식에서 매우 유용하다
하지만 이 방식은 조립한 이후에는 전략 변경이 까다롭다. 다음 방식은 전략 패턴에서 파라미터를 통해 전략을 받는 방식을 사용해 보자.
//parameter를 통해 전략을 받는 방식
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);
}
}
// 실제 구현
void strategyV3() {
ContextV2 context = new ContextV2();
context.execute(() -> log.info("비즈니스 로직1 실행"));
context.execute(() -> log.info("비즈니스 로직2 실행"));
}
현재 우리가 원하는 것은 애플리케이션 의존 관계를 설정하는 것처럼 선조립 후실행이 아니다. 단순히 코드 실행시 변하지 않는 템플릿이 존재하고, 그 안에서 원하는 부분(전략)만 바꿔 실행하고 싶음.
따라서 뒤의 방식이 좀 더 적절하고, 이 뒤의 방식을 따로 스프링에서는 템플릿 콜백 패턴이라 한다. (이 템플릿 콜백 패턴은 GOF 패턴은 아니다고 스프링 내부에서 이런 방식을 주로 사용하기 때문에 스프링에서 이렇게 부른다.) 전략 부분에서 템플릿과 콜백 부분이 강조된 패턴.
참고 : 본 글은 김영한님의 스프링 강의를 정리한 것이다.