쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다. 쉽게 이야기해서 물건 보관 창구. 여러 사람이 같은 물건 보관 창구를 사용하더라도 직원은 사용자를 인식해서 사용자별로 확실하게 물건을 구분.
FieldService
에서 밑줄 친 부분만 바꾸고 나머지는 메서드만 조금씩 바꾸면 된다.
A는 userA B는 userB로 각각 저장된 것을 확인할 수 있다.
FieldLogTrace
에서 발생했던 동시성 문제를 ThreadLocal
을 사용해서 해결하려고 한다.
package hello.advanced.trace.logtrace;
import hello.advanced.trace.TraceId;
import hello.advanced.trace.TraceStatus;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ThreadLocalLogTrace implements LogTrace{
private static final String START_PREFIX = "-->";
private static final String COMPLETE_PREFIX = "<--";
private static final String EX_PREFIX = "<X-";
// private TraceId traceIdHolder; //traceId 동기화, 동시성 이슈 발생
private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>();
@Override
public TraceStatus begin(String message) {
syncTraceId();
TraceId traceId = traceIdHolder.get();
Long startTimeMs = System.currentTimeMillis();
log.info("[{}] {}{}", traceId.getId(),
addSpace(START_PREFIX, traceId.getLevel()), message);
return new TraceStatus(traceId, startTimeMs, message);
}
private void syncTraceId() {
TraceId traceId = traceIdHolder.get();
if (traceId == null) {
traceIdHolder.set(new TraceId());
} else {
traceIdHolder.set(traceId.createNextId());
}
}
@Override
public void end(TraceStatus status) {
complete(status, null);
}
@Override
public void exception(TraceStatus status, Exception e) {
complete(status, e);
}
private void complete(TraceStatus status, Exception e) {
Long stopTimeMs = System.currentTimeMillis();
long resultTimeMs = stopTimeMs - status.getStartTimeMs();
TraceId traceId = status.getTraceId();
if (e == null) {
log.info("[{}] {}{} time={}ms", traceId.getId(),
addSpace(COMPLETE_PREFIX, traceId.getLevel()), status.getMessage(),
resultTimeMs);
} else {
log.info("[{}] {}{} time={}ms ex={}", traceId.getId(),
addSpace(EX_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs,
e.toString());
}
releaseTraceId();
}
private void releaseTraceId() {
TraceId traceId = traceIdHolder.get();
if (traceId.isFirstLevel()) {
traceIdHolder.remove(); //destroy
} else {
traceIdHolder.set(traceId.createPreviousId());
}
}
private static String addSpace(String prefix, int level) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < level; i++) {
sb.append( (i == level - 1) ? "|" + prefix : "| ");
}
return sb.toString();
}
}
ThreadLocal.remove()
쓰레드 로컬을 모두 사용하고 나면 꼭 ThreadLocal.remove()
를 호출해서 쓰레드 로컬에 저장된 값을 제거해줘야 한다.
테스트 생략
컨트롤러는 결국 logTrace
를 불러오기 때문에 이 logTrace
에 들어간 부분을 ThreadLocalLogTrace()
로 바꿔주면 적용이 끝난다.
자동으로 컨트롤러에서 logTrace -> ThreadLocalLogTrace
로 된다.
이렇게 연속으로 호출해도 레벨이 안꼬이고 잘 동작하는 것을 확인할 수 있다.
쓰레드 로컬의 값을 사용 후 제거하지 않고 두면 WAS(톰캣)처럼 쓰레드 풀을 사용하는 경우에 심각한 문제가 발생할 수 있다.
사용자 A가 호출할때, 그림이다.
이후에 요청이 종료되면,
이렇게 된다.
WAS는 사용이 끝난 thread-A
를 쓰레드 풀로 반환한다.
쓰레드를 생성하는 비용이 비싸기 때문에 쓰레드를 제거하지 않고, 보통 쓰레드 풀을 통해서 재사용한다.
thread-A
는 풀에 아직 살아있고, 사용자A
도 함께 살아있게 된다.
그 후에 B에서 조회 요청이 들어오면,
WAS는 쓰레드 풀에서 쓰레드를 하나 조회한다.
쓰레드 thread-A
가 할당되었다.(다른 쓰레드가 할당될 수도 있다.)
그럼 조회하는 요청이므로 thread-A
가 반환된다.
사용자A
를 조회하게 된다.
그렇기 때문에 요청이 끝나면 쓰레드 로컬의 값을 ThreadLocal.remove()
를 통해서 꼭 제거해야 한다.