[Spring AOP] 쓰레드 로컬 - Thread Local

2025. 7. 6. 16:24·Spring/AOP

쓰레드 로컬 - Thread Local

#Spring/고급

예제 프로젝트 만들기 - V0

일반적인 웹 애플리케이션에서 Controller → Service → Repository로 이어지는 흐름을 최대한 단순하게 구현

@Repository
@RequiredArgsConstructor
public class OrderRepositoryV0 {

    public void save(String itemId) {
        //저장 로직
        if (itemId.equals("ex")) {
            throw new IllegalStateException("예외 발생!");
        }
        sleep(1000);
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
@Service
@RequiredArgsConstructor
public class OrderServiceV0 {

    private final OrderRepositoryV0 orderRepository;

    public void orderItem(String itemId) {
        orderRepository.save(itemId);
    }
}
@RestController
@RequiredArgsConstructor
public class OrderControllerV0 {

    private final OrderServiceV0 orderService;

    @GetMapping("/v0/request")
    public String request(String itemId) {
        orderService.orderItem(itemId);
        return "ok";
    }
}

로그 추적기 - 요구사항 분석

로그 추적기를 만들어보자.

  • 모든 PUBLIC 메서드의 호출과 응답 정보를 로그로 출력
  • 애플리케이션의 흐름을 변경하면 안됨
    • 로그를 남긴다고 해서 비즈니스 로직의 동작에 영향을 주면 안됨
  • 메서드 호출에 걸린 시간
  • 정상 흐름과 예외 흐름 구분
    • 예외 발생시 예외 정보가 남아야 함
  • 메서드 호출의 깊이 표현
  • HTTP 요청을 구분
    • HTTP 요청 단위로 특정 ID를 남겨서 어떤 HTTP 요청에서 시작된 것인지 명확하게 구분이 가능해야 함
    • 트랜잭션 ID (DB 트랜잭션X), 여기서는 하나의 HTTP 요청이 시작해서 끝날 때 까지를 하나의 트랜잭션이라 함

정상 요청

[796bccd9] OrderController.request()
[796bccd9] |-->OrderService.orderItem()
[796bccd9] |   |-->OrderRepository.save()
[796bccd9] |   |<--OrderRepository.save() time=1004ms
[796bccd9] |<--OrderService.orderItem() time=1014ms
[796bccd9] OrderController.request() time=1016ms

예외 발생

[b7119f27] OrderController.request()
[b7119f27] |-->OrderService.orderItem()
[b7119f27] |   |-->OrderRepository.save()
[b7119f27] |   |<X-OrderRepository.save() time=0ms ex=java.lang.IllegalStateException: 예외 발생!
[b7119f27] |<X-OrderService.orderItem() time=10ms ex=java.lang.IllegalStateException: 예외 발생!
[b7119f27] OrderController.request() time=11ms ex=java.lang.IllegalStateException: 예외 발생!

로그 추적기 V1 - 프로토타입 개발

  • 모든 Application 로직에 로그를 남겨볼까?
    • 모두 일일히 남긴다면, 매 번 코드를 중복되게 작성해야 함.
    • 이 코드에는 실행 시간 측정 같은 것들도 중복되고, 트랜잭션 ID, 깊이 정보를 넘겨 받아야 함.

일단 프로토타입을 만들어보고, 개선해보자.

일단, 모든 스탭에서 로그를 출력할 때, 트랜잭션ID와 깊이를 호출시/호출 완료시 전달받아야 한다.
ID와 LEVEL을 묶어 TraceId로 만들었다.

public class TraceId {
    private String id;
    private int level;

    public TraceId() {
        this.id = createId();
        this.level = 0;
    }

    private TraceId(String id, int level) {
        this.id = id;
        this.level = level;
    }

    private String createId() {
        return UUID.randomUUID().toString().substring(0, 8);
    }

    public TraceId createNextId() {
        return new TraceId(id, level + 1);
    }

    public TraceId createPreviousId() {
        return new TraceId(id, level - 1);
    }

    public boolean isFirstLevel() {
        return level == 0;
    }

    // getter 생략

메서드 이름(message), 메서드 호출 시각은 해당 메서드 호출 시/호출 완료시 따로 전달해주지 않아도 되고, 해당 메서드 시작 시점에 만들어 종료시 활용한다. 로그의 상태 정보를 나타내는 TraceStatus 메서드를 만들었다.

public class TraceStatus {

    private TraceId traceId;
    private Long startTimeMs;
    private String message;

    public TraceStatus(TraceId traceId, Long startTimeMs, String message) {
        this.traceId = traceId;
        this.startTimeMs = startTimeMs;
        this.message = message;
    }
    // getter 생략

메서드 호출 시 단순히 로그를 시작하고, 메서드 마지막에 단순히 로그를 종료하여 로그를 남길 수 있도록 유틸 객체(로그 추적기)를 만들자.

@Slf4j
@Component
public class HelloTraceV1 {

    private static final String START_PREFIX = "-->";
    private static final String COMPLETE_PREFIX = "<--";
    private static final String EX_PREFIX = "<X-";

    public TraceStatus begin(String message) {
        TraceId traceId = new TraceId();
        Long startTimeMs = System.currentTimeMillis();
        log.info("[{}] {}{}", traceId.getId(), addSpace(START_PREFIX, traceId.getLevel()), message);
        return new TraceStatus(traceId, startTimeMs, message);
    }

    public void end(TraceStatus status) {
        complete(status, null);
    }

    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());
        }
    }

    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();
    }
}

상태를 가지고 있지 않고, 싱글톤으로 관리하기 위해 스프링 빈으로 등록했다.
로그 추적기에서 사용되는 공개 메서드는 다음 3가지이다.

  • TraceStatus begin(String message)
    • 로그를 시작한다.
    • 로그 메시지를 파라미터로 받아서 시작 로그를 출력한다.
    • 응답 결과로 현재 로그의 상태인 TraceStatus 를 반환한다.
  • void end(TraceStatus status)
    • 로그를 정상 종료한다.
    • 파라미터로 시작 로그의 상태(TraceStatus)를 전달 받는다. 이 값을 활용해서 실행 시간을 계산하고, 종료시에도 시작할 때와 동일한 로그 메시지를 출력할 수 있다.
    • 정상 흐름에서 호출한다.
  • void exception(TraceStatus status, Exception e)
    • 로그를 예외 상황으로 종료한다.
    • TraceStatus, Exception 정보를 함께 전달 받아서 실행시간, 예외 정보를 포함한 결과 로그를 출력한다.
    • 예외가 발생했을 때 호출한다.

물론 아직 프로토타입이라 요구사항을 모두 만족하지 못한다. 코드를 보면 알겠지만, TraceId 정보를 메서드 호출/메서드 종료 시에 넘겨줘야 하는데 그 부분이 없다.

로그 추적기 V1 - 적용

@RestController
@RequiredArgsConstructor
public class OrderControllerV1 {

    private final OrderServiceV1 orderService;
    private final HelloTraceV1 trace;

    @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; //예외를 꼭 다시 던져주어야 한다. 먹어버리면 Application 흐름에 영향을 준다
        }
    }
}
  • 단순하게 trace.begin(), trace.end() 코드 두 줄만 적용하면 될 줄 알았지만, 실상은 그렇지 않다. trace.exception()으로 예외까지 처리해야 하므로 지저분한 try, catch 코드가 추가된다.
  • begin()의 결과 값으로 받은 TraceStatus status 값을 end(), exception()에 넘겨야 한다. 결국 try, catch 블록 모두에 이 값을 넘겨야한다. 따라서 try 상위에 TraceStatus status 코드를 선언해야 한다.

마찬가지로 Service, Repository에 적용해주자.
실행하면 예상했던 대로, TraceId를 전달해주는 부분이 없어서 id와 깊이가 제대로 나타나지 않는다.

[11111111] OrderController.request()
[22222222] OrderService.orderItem()
[33333333] OrderRepository.save()
[33333333] OrderRepository.save() time=1000ms
[22222222] OrderService.orderItem() time=1001ms
[11111111] OrderController.request() time=1001ms

현재 남은 요구사항은 다음과 같다.

  • 메서드 호출의 깊이 표현
  • HTTP 요청을 구분
    • HTTP 요청 단위로 특정 ID를 남겨서 어떤 HTTP 요청에서 시작된 것인지 명확하게 구분이 가능해야 함
    • 트랜잭션 ID (DB 트랜잭션X)

로그 추적기 V2 - 파라미터로 동기화 개발

트랜잭션ID와 메서드 호출의 깊이를 표현하는 하는 가장 단순한 방법은 첫 로그에서 사용한 트랜잭션ID와 level을 다음 로그에 넘겨주면 된다. TraceId 를 다음 로그에 넘겨줄 수 있도록 로그 추적기를 수정해보자.

@Slf4j
@Component
public class HelloTraceV2 {

    private static final String START_PREFIX = "-->";
    private static final String COMPLETE_PREFIX = "<--";
    private static final String EX_PREFIX = "<X-";

    //V2에서 추가
    public TraceStatus beginSync(TraceId beforeTraceId, String message) {
        TraceId nextId = beforeTraceId.createNextId();
        Long startTimeMs = System.currentTimeMillis();
        log.info("[{}] {}{}", nextId.getId(), addSpace(START_PREFIX, nextId.getLevel()), message);
        return new TraceStatus(nextId, startTimeMs, message);
    }
    // 나머지는 기존과 동일

새롭게 요청이 시작되어 로그를 시작하는 경우 begin()을, 이후에는 beginSync()를 사용한다.

beginSync(..)

  • 기존 TraceId 에서 createNextId()를 통해 다음 ID를 구한다.
  • createNextId()의 TraceId 생성 로직은 다음과 같다.
    • 트랜잭션ID는 기존과 같이 유지한다.
    • 깊이를 표현하는 Level은 하나 증가한다. (0 1)

로그 추적기 V2 - 적용

현재 로그의 상태 정보인 트랜잭션ID와 level이 다음으로 전달되어야 한다.
이 정보는 TraceStatus.traceId 에 담겨있다. 따라서 traceId를 컨트롤러에서 서비스를 호출할 때 넘겨주면 된다.

@RestController
@RequiredArgsConstructor
public class OrderControllerV2 {

    private final OrderServiceV2 orderService;
    private final HelloTraceV2 trace;

    @GetMapping("/v2/request")
    public String request(String itemId) {
        TraceStatus status = null;
        try {
            status = trace.begin("OrderController.request()");
            orderService.orderItem(status.getTraceId(), itemId);
            trace.end(status);
            return "ok";
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;
        }
    }
}

첫 번째 동작 수행 trace.begin() 호출

@Service
@RequiredArgsConstructor
public class OrderServiceV2 {

    private final OrderRepositoryV2 orderRepository;
    private final HelloTraceV2 trace;

    public void orderItem(TraceId traceId, String itemId) {
        TraceStatus status = null;
        try {
            status = trace.beginSync(traceId, "OrderService.orderItem()");
            orderRepository.save(status.getTraceId(), itemId);
            trace.end(status);
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;
        }
    }
}

첫 depth 이후에는 trace.beginSync() 사용

리포지토리까지 코드를 작성하고 수행하면 우리가 원하는 모양의 로그가 출력될 것이다.

[c80f5dbb] OrderController.request()
[c80f5dbb] |-->OrderService.orderItem()
[c80f5dbb] |   |-->OrderRepository.save()
[c80f5dbb] |   |<--OrderRepository.save() time=1005ms
[c80f5dbb] |<--OrderService.orderItem() time=1014ms
[c80f5dbb] OrderController.request() time=1017ms

남은 문제점

  • TraceId 의 동기화를 위해서 관련 메서드의 모든 파라미터를 수정해야 한다.
    • 만약 인터페이스가 있다면 인터페이스까지 모두 고쳐야 하는 상황이다.
  • 로그를 처음 시작할 때는 begin() 을 호출하고, 처음이 아닐때는 beginSync()를 호출해야 한다.
    • 만약에 컨트롤러를 통해서 서비스를 호출하는 것이 아니라, 다른 곳에서 서비스를 처음으로 호출하는 상황이라면 파라미터로 넘길 TraceId 가 없다.

대안이 없을까? 파라미터를 통해 객체를 전달하지 않고도 HTTP 트랜잭션(하나의 스레드) 내에서 값을 유지할 수 있는 기능이 있으면 좋을 것 같다.

일단, 로그 추적기 기능에 대한 인터페이스를 만들고 구현체를 개선해가보자.

public interface LogTrace {

    TraceStatus begin(String message);
    void end(TraceStatus status);
    void exception(TraceStatus status, Exception e);
}

필드 동기화

이제 파라미터를 넘기지 않고 TraceId를 동기화 할 수 있는 FieldLogTrace 구현체를 만들어보자.

FieldLogTrace: 로그 추적기 필드에서 TraceId 동기화

@Slf4j
public class FieldLogTrace 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 동기화, 동시성 이슈 발생

    @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);
    }

    @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() {
        if (traceIdHolder == null) {
            traceIdHolder = new TraceId();
        } else {
            traceIdHolder = traceIdHolder.createNextId();
        }
    }

    private void releaseTraceId() {
        if (traceIdHolder.isFirstLevel()) {
            traceIdHolder = null; //destroy
        } else {
            traceIdHolder = traceIdHolder.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();
    }
}

핵심 아이디어는 이제 로그 추적기가 TraceId 상태를 관리한다는 점이다. 제 직전 로그의 TraceId는 파라미터로 전달되는 것이 아니라 FieldLogTrace의 필드인 traceIdHolder 에 저장된다.

  • syncTraceId()
    • TraceId 를 새로 만들거나 앞선 로그의 TraceId를 참고해서 동기화하고, level도 증가한다.
    • 최초 호출이면 TraceId를 새로 만든다.
    • 직전 로그가 있으면 해당 로그의 TraceId를 참고해서 동기화하고, level도 하나 증가한다.
    • 결과를 traceIdHolder 에 보관한다.
  • releaseTraceId()
    • 메서드를 추가로 호출할 때는 level이 하나 증가해야 하지만, 메서드 호출이 끝나면 level이 하나 감소해야 한다.
    • releaseTraceId()는 level 을 하나 감소한다.
    • 만약 최초 호출(level==0)이면 내부에서 관리하는 traceId를 제거한다.

구현체 스프링 빈으로 등록

@Configuration
public class LogTraceConfig {

    @Bean
    public LogTrace logTrace() {
        return new FieldLogTrace();
    }
}

Controller에 적용

@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, Repository에 적용해보자

실행 결과

[f8477cfc] OrderController.request()
[f8477cfc] |-->OrderService.orderItem()
[f8477cfc] | |-->OrderRepository.save()
[f8477cfc] | |<--OrderRepository.save() time=1004ms
[f8477cfc] |<--OrderService.orderItem() time=1006ms
[f8477cfc] OrderController.request() time=1007ms

이제 모든 문제를 해결한 것 같지만, 이 방식에는 큰 문제가 존재한다. (다음 소주제에서)

필드 동기화 - 동시성 문제

직전에 만든 FieldLogTrace 는 심각한 동시성 문제를 가지고 있다.

[nio-8080-exec-3] [aaaaaaaa] OrderController.request()
[nio-8080-exec-3] [aaaaaaaa] |-->OrderService.orderItem()
[nio-8080-exec-3] [aaaaaaaa] | |-->OrderRepository.save()
[nio-8080-exec-4] [aaaaaaaa] | | |-->OrderController.request()
[nio-8080-exec-4] [aaaaaaaa] | | | |-->OrderService.orderItem()
[nio-8080-exec-4] [aaaaaaaa] | | | | |-->OrderRepository.save()
[nio-8080-exec-3] [aaaaaaaa] | |<--OrderRepository.save() time=1005ms
[nio-8080-exec-3] [aaaaaaaa] |<--OrderService.orderItem() time=1005ms
[nio-8080-exec-3] [aaaaaaaa] OrderController.request() time=1005ms
[nio-8080-exec-4] [aaaaaaaa] | | | | |<--OrderRepository.save() time=1005ms
[nio-8080-exec-4] [aaaaaaaa] | | | |<--OrderService.orderItem() time=1005ms
[nio-8080-exec-4] [aaaaaaaa] | | |<--OrderController.request() time=1005ms

로그 추적기는 싱글톤으로 관리되는데, 여러 스레드에서 동시에 접근한다고 하면, TraceId 필드에서 동시성 문제가 터질 것이다.

동시성 문제

FieldLogTrace에서 왜 동시성 문제가 발생하는지 모르겠는 분은, 영한님의 자바 강의 고급 1편이나 자바의 정석 뒤쪽 챕터를 참고하는 걸 추천한다.

ThreadLocal - 소개

지금처럼 싱글톤 객체의 필드를 사용하면서 동시성 문제를 해결하려면 어떻게 해야할까?
이전 챕터를 마무리하면서, “파라미터를 통해 객체를 전달하지 않고도 HTTP 트랜잭션(하나의 스레드) 내에서 값을 유지할 수 있는 기능이 있으면 좋을 것 같다”라고 생각을 했다. 이럴 때 사용하는 것이 바로 쓰레드 로컬이다.

ThreadLocal은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다. 쉽게 이야기해서 물건 보관 창구를 떠올리면 된다. 여러 사람이 같은 물건 보관 창구를 사용하더라도 창구 직원은 사용자를 인식해서 사용자별로 확실하게 물건을 구분해준다.

자바는 언어차원에서 쓰레드 로컬을 지원하기 위한 java.lang.ThreadLocal 클래스를 제공한다.

ThreadLocal - 예제 코드

@Slf4j
public class ThreadLocalService {

    private ThreadLocal<String> nameStore = new ThreadLocal<>();

    public String logic(String name) {
        log.info("저장 name={} -> nameStore={}", name, nameStore.get());
        nameStore.set(name);
        sleep(1000);
        log.info("조회 nameStore={}", nameStore.get());
        return nameStore.get();
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ThreadLocal 사용법

  • 값 저장: ThreadLocal.set(xxx)
  • 값 조회: ThreadLocal.get()
  • 값 제거: ThreadLocal.remove()

주의

해당 쓰레드가 쓰레드 로컬을 모두 사용하고 나면 ThreadLocal.remove()를 호출해서 쓰레드 로컬에 저장된 값을 제거해주어야 한다.

클라이언트 코드(사용 예제)

@Slf4j
public class ThreadLocalServiceTest {
    private ThreadLocalService service = new ThreadLocalService();

    @Test
    void threadLocal() {
        log.info("main start");
        Runnable userA = () -> {
            service.logic("userA");
        };
        Runnable userB = () -> {
            service.logic("userB");
        };

        Thread threadA = new Thread(userA);
        threadA.setName("thread-A");
        Thread threadB = new Thread(userB);
        threadB.setName("thread-B");

        threadA.start();
        sleep(100);
        threadB.start();

        sleep(2000); // 메인 스레드 종료 방지
        log.info("main exit");
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

쓰레드 로컬 덕분에 쓰레드 마다 각각 별도의 데이터 저장소를 가지게 되었고, A가 로직 수행 시 “userA”가 조회되고, B가 조회 시 “userB”가 조회된다.

쓰레드 로컬 동기화 - 개발

FieldLogTrace 에서 발생했던 동시성 문제를 ThreadLocal로 해결해보자.
필드 대신에 쓰레드 로컬을 사용해서 데이터를 동기화하는 ThreadLocalLogTrace 를 새로 만들자.

@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();
    }
}

ThreadLocal로 변경해주고, 값 조회/저장 시에 get(), set() 메서드를 사용하는 부분만 수정해주면 된다.
기존 클라이언트 코드는 인터페이스에 의존하므로 코드를 수정할 필요가 없다.

ThreadLocal.remove()
추가로 쓰레드 로컬을 모두 사용하고 나면 꼭 ThreadLocal.remove()를 호출해서 쓰레드 로컬에 저장된 값을 제거해주어야 한다.

스프링 빈: 로그 추적기 빈 변경

@Configuration
public class LogTraceConfig {

    @Bean
    public LogTrace logTrace() {
        //return new FieldLogTrace();
        return new ThreadLocalLogTrace();
    }
}

실행해보면, 원하는 데로 정상적으로 동작한다.

쓰레드 로컬 - 주의사항

쓰레드 로컬의 값을 사용 후 제거하지 않고 그냥 두면 WAS(톰캣)처럼 쓰레드 풀을 사용하는 경우에 심각한 문제가 발생할 수 있다.
서블릿 컨테이너가 쓰레드 풀을 관리할 때, 사용자 요청이 완료되면 해당 쓰레드를 쓰레드 풀에 반납하고 재사용한다.
사용자A 저장 요청 종료

이 때 사용자B의 요청을 thread-A가 사용한다면?

쓰레드 로컬은 thread-A 전용 보관소에 있는 사용자A 값을 반환한다.

  • 사용자B는 사용자A의 정보를 조회하게 된다!!!

이런 문제를 예방하려면 사용자A의 요청이 끝날 때 쓰레드 로컬의 값을 ThreadLocal.remove()를 통해서 꼭 제거해야 한다.

Ref) 스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런

'Spring > AOP' 카테고리의 다른 글

[Spring AOP] 빈 후처리기  (0) 2025.07.06
[Spring AOP] 스프링이 지원하는 프록시  (0) 2025.07.06
[Spring AOP] 동적 프록시 기술  (0) 2025.07.06
[Spring AOP] 프록시 패턴과 데코레이터 패턴  (0) 2025.07.06
[Spring AOP] 템플릿 메서드 패턴과 콜백 패턴  (0) 2025.07.06
'Spring/AOP' 카테고리의 다른 글
  • [Spring AOP] 스프링이 지원하는 프록시
  • [Spring AOP] 동적 프록시 기술
  • [Spring AOP] 프록시 패턴과 데코레이터 패턴
  • [Spring AOP] 템플릿 메서드 패턴과 콜백 패턴
lumana
lumana
배움을 나누는 공간 https://github.com/bebeis
  • lumana
    Brute force Study
    lumana
  • 전체
    오늘
    어제
    • 분류 전체보기 (465) N
      • 개발 일지 (28)
        • Performance (0)
        • TroubleShooting (1)
        • Refactoring (0)
        • Code Style, Convetion (0)
        • Architecture (1)
      • Software Engineering (36) N
        • Test (8)
        • 이론 (18)
        • Clean Code (10) N
      • Java (72)
        • Basic (5)
        • Core (21)
        • Collection (7)
        • 멀티스레드&동시성 (13)
        • IO, Network (8)
        • Reflection, Annotation (3)
        • Modern Java(8~) (13)
        • JVM (2)
      • Spring (53)
        • Framework (12)
        • MVC (23)
        • Transaction (3)
        • AOP (11)
        • Boot (0)
        • AI (0)
      • DB Access (16)
        • Jdbc (1)
        • JdbcTemplate (0)
        • JPA (14)
        • Spring Data JPA (0)
        • QueryDSL (0)
      • Computer Science (130)
        • Data Structure (27)
        • OS (14)
        • Database (10)
        • Network (21)
        • 컴퓨터구조 (6)
        • 시스템 프로그래밍 (23)
        • Algorithm (29)
      • HTTP (8)
      • Infra (1)
        • Docker (1)
      • 프로그래밍언어론 (15)
      • Programming Language(Sub) (77)
        • Kotlin (1)
        • Python (25)
        • C++ (51)
        • JavaScript (0)
      • FE (11)
        • HTML (1)
        • CSS (9)
        • React (0)
        • Application (1)
      • Unix_Linux (0)
        • Common (0)
      • PS (13)
        • BOJ (7)
        • Tip (3)
        • 프로그래머스 (0)
        • CodeForce (0)
      • Book Review (4)
      • Math (3)
        • Linear Algebra (3)
      • AI (7)
        • DL (0)
        • ML (0)
        • DA (0)
        • Concepts (7)
      • 프리코스 (4)
      • Project Review (6)
      • LegacyPosts (11)
      • 모니터 (0)
      • Diary (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
lumana
[Spring AOP] 쓰레드 로컬 - Thread Local
상단으로

티스토리툴바