스프링 고급편 - 쓰레드 로컬 ThreadLocal
✔️ 전 시간 필드 동기화에 대하여
매번 파라미터로 TraceId를 넘기는 것은 복잡하고 어렵다.
따라서 LogTrace 인터페이스를 만들고 traceHolder를 만들어서 여기에 traceId를 보관하여 동기화 한다.
하지만 동시성 이슈가 발생한다.
결론적으로는 쓰레드 로컬을 사용하면 동시성 이슈도 해결된다.
먼저 TraceId를 파라미터로 넘기지 않고 필드 동기화하는 법이다.
public interface LogTrace {
TraceStatus begin(String message);
void end(TraceStatus status);
void exception(TraceStatus status, Exception e);
}
@Slf4j
public class FieldLogTrace implements LogTrace {
private TraceId traceIdHolder; //traceId 동기화, 동시성 이슈 발생
@Override
public TraceStatus begin(String message) {
syncTraceId();
TraceId traceId = traceIdHolder;
long startTimeMs = System.currentTimeMillis();
// 로그 출력
log.info("[{}] {}{}", traceId.getId(), addSpace(START_PREFIX, traceId.getLevel()), message);
return new TraceStatus(traceId, startTimeMs, message);
}
//.. 기존 complete, end 코드등
// id 동기화
private void syncTraceId() {
if (traceIdHolder == null) {
traceIdHolder = new TraceId();
} else {
traceIdHolder = traceIdHolder.createNextId();
}
}
// 로그 추적이 끝나면 destroy
private void releaseTraceId() {
if (traceIdHolder.isFirstLevel()) {
traceIdHolder = null; // destroy
} else {
traceIdHolder = traceIdHolder.createPreviousId();
}
}
syncTraceId 메서드로 traceHolder를 체크해서 Id값을 동기화한다.
사용이 끝나면 releaseTraceId()를 호출하여 traceHolder를 삭제한다.
이는 Config에서 직접 Bean으로 등록하여 사용한다.
@Configuration
public class LogTraceConfig {
@Bean
public LogTrace logTrace() {
return new FieldLogTrace();
}
}
문제점은 동시성을 해결하지 못한다.
스프링 빈으로 등록했기에 싱글톤으로 관리되고
동시에 접근시 같은 로그 추적기를 접근하게되어
traceId값이 증가해버린다.
또한 traceId는 각 접근마다 다른 트랜잭션ID 값을 취해야하지만
싱글톤 로그 추적기에 동시 접근이기에 같은 ID값을 가지게 된다.
참고 : 동시성 문제는 읽기만 해서는 발생하지 않고 값이 수정되었기에 발생한다.
✔️ 쓰레드 로컬
쓰레드 로컬 : 해당 쓰레드만 접근할 수 있는 특별한 저장소
ThreadLocal<T>
우리가 현재 TraceId가 동시성 이슈가 있으므로 TraceId 타입으로 만들면 된다.
값 저장 : set(T t)
값 조회 : get()
값 제거 : remove()
ThreadA : ThreadA 저장소
ThreadB : ThreadB 저장소
이런식으로 각 쓰레드마다 저장소가 따로 지정되어있기에 동시성 이슈에서 안전하다.
@Slf4j
public class ThreadLocalLogTrace implements LogTrace {
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());
}
}
private void releaseTraceId() {
TraceId traceId = traceIdHolder.get();
if (traceId.isFirstLevel()) {
traceIdHolder.remove(); // 전용 보관소 제거
} else {
traceIdHolder.set(traceId.createPreviousId());
}
}
//그 외 로직들
}
❗ 주의점
쓰레드가 쓰레드로컬을 통해서 저장소에 값을 저장하고나서 쓰레드로컬 사용을 종료한다.
이때 remove로 쓰레드로컬에 저장된 값을 삭제하지 않으면,
상태가 유지되어 값을 가지게 된다.
문제는 다음 쓰레드가 하필 전에 썼던 쓰레드로 할당받게 된다면 접근 사용자의 데이터를 확인하게되는 심각한 문제를 초래한다.
따라서 항상 쓰레드 로컬 사용이 끝나면 remove로 저장된 값을 삭제하자.