Question
- ThreadLocal은 언제 사용하면 좋은가?
- ThreadLocal 사용시 꼭 주의해야 하는 점은?
ThreadLocal
- 해당 스레드만 접근할 수 있는 특별한 저장소
- 다른 스레드와는 공유되지 않음
ThreadLocal을 사용하면 좋은 경우
- 아래 예제에서 아래 코드가 쓰레드 로컬을 선언한 부분
private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>();
- 만약에 쓰레드 로컬을 사용하지 않았다면 아래와 같이 선언을 해서 사용을 했을 것임.
private TraceId traceIdHolder;
- 해당 클래스가 만약 Bean으로 사용된다면 Bean은 싱글톤 객체이므로 여러 스레드간 공유되는 자원이고, 싱글톤객체 사용시 가장 문제가 될 수 있는 Stateful 문제가 발생
- 이는 Heap에 저장되는 공유 자원이 있는 경우 여러 스레드가 공유하게 되며, 이 값에 대한 조회가 아니라 쓰기 작업을 하게 될 경우 동시성 문제 때문에 예기치 못한 큰 문제가 발생할 수 있음
- 따라서, 여러 쓰레드간에 공유되지 말아야할 자원에 대해서 ThreadLocal을 사용하면 좋음
ThreadLocal 사용시 주의 점
- 스레드는 일반적으로 사용이 완료된 후 제거되는게 아니라 스레드풀로 돌아가 재사용 되므로, 그 경우 ThreadLocal은 그 스레드에 남아서 새로운 요청에 재사용될 수있음
- 이는 내 계좌 정보가 다른 사람에게 보여지는것과 같은 치명적인 문제를 야기할 수 있으므로 ThreadLocal은 모든 작업이 끝나면 꼭 제거를 해줘야 함.
ThreadLocal을 적용한 Log Trace 예시
@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 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);
}
@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 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(); // 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();
}
}
@Configuration
public class LogTraceConfig {
@Bean
public LogTrace logTrace() {
// return new FieldLogTrace();
return new ThreadLocalLogTrace();
}
}
@RestController
@RequiredArgsConstructor
public class OrderControllerV3 {
private final OrderServiceV3 orderService;
private final LogTrace trace;
@GetMapping("/v3/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; // 예외를 꼭 다시 던져주어야 한다.
}
}
}
@Service
@RequiredArgsConstructor
public class OrderServiceV3 {
private final OrderRepositoryV3 orderRepository;
private final LogTrace trace;
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;
}
}
}
@Repository
@RequiredArgsConstructor
public class OrderRepositoryV3 {
private final LogTrace trace;
public void save(String itemId) {
TraceStatus status = null;
try {
status = trace.begin("OrderRepository.save()");
// 저장 로직
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
sleep(1000);
trace.end(status);
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}