FieldLogTrace 에서 발생했던 동시성 문제를 ThreadLocal로 해결해보자!
TraceId traceIdHolder 필드를 쓰레드 로컬을 사용하도록
ThreadLocal<TraceId>
traceIdHolder로 변경하면된다.
필드 대신에 쓰레드 로컬을 사용해서 데이터를 동기화하는 ThreadLoclLogTrace를 새로 생성
ThreadLocal데이터타입을 TraceId 타입으로 저장
traceIdHolder을 get()으로 가지고 오고,
package study.advanced.trace.logtrace;
import lombok.extern.slf4j.Slf4j;
import study.advanced.trace.TraceId;
import study.advanced.trace.TraceStatus;
@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();
//꺼낸 값이 null이면, traceIdHolder 에 새로운 값을 넣어주고
if(traceId == null) {
traceIdHolder.set(new TraceId());
}else {
// null아닐때는 기존아이디에 증가 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.getStartITmeMs();
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() {
//traceIdHolder에서 traceId 을 꺼낸다.
TraceId traceId = traceIdHolder.get();
//traceId가 첫번째 레벨이면
if(traceId.isFirstLevel()) {
//
traceIdHolder.remove(); // 쓰레드전용 보관소만 제거
}else {
//이전아이디를 넣어준다.
traceIdHolder.set(traceId.createPreviousId());
}
}
private Object 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()를 호출해서 쓰레드 로컬에 저장된 값을 제거해주어야 한다.
쉽게 이야기해서 다음의 마지막 로그를 출력하고 나면 쓰레드 로컬의 값을 제거해야 한다.
테스트패키지에서 study.advanced.trace.logtrace에 ThreadLocalLogTraceTest 생성 후,
package study.advanced.trace.logtrace;
import org.junit.jupiter.api.Test;
import study.advanced.trace.TraceStatus;
public class ThreadLocalLogTraceTest {
ThreadLocalLogTrace trace = new ThreadLocalLogTrace();
@Test
void begin_end_level2() {
TraceStatus status1 = trace.begin("hello1");
TraceStatus status2 = trace.begin("hello2");
trace.end(status2);
trace.end(status1);
}
@Test
void begin_exception_level2() {
TraceStatus status1 = trace.begin("hello1");
TraceStatus status2 = trace.begin("hello2");
trace.exception(status2, new IllegalStateException());
trace.exception(status1, new IllegalStateException());
}
}
쓰레드가 잘 동작하는걸 볼 수 있다.