본문 바로가기
Spring/Advanced

쓰레드 로컬 - ThreadLocal

by JHyun0302 2023. 8. 12.
728x90

※문제 : TraceId 를 파라미터로 넘기지 않는 방법?

 

 

 

FieldLogTrace

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

 

 

syncTraceId()

  • TraceId 를 새로 만들거나 앞선 로그의 TraceId 를 참고해서 동기화하고, level 도 증가한다.
  • 최초 호출이면 TraceId 를 새로 만든다.
  • 직전 로그가 있으면 해당 로그의 TraceId 를 참고해서 동기화하고, level 도 하나 증가한다.
  • 결과를 traceIdHolder 에 보관한다.

 

releaseTraceId()

  • 메서드를 추가로 호출할 때는 level 이 하나 증가해야 하지만, 메서드 호출이 끝나면 level 이 하나 감소해야 한다.
  • releaseTraceId() 는 level 을 하나 감소한다.
  • 만약 최초 호출( level==0 )이면 내부에서 관리하는 traceId 를 제거한다.

 

 

begin_end_level2() - 실행 결과

[ed72b67d] hello1
[ed72b67d] |-->hello2
[ed72b67d] |<--hello2 time=2ms
[ed72b67d] hello1 time=6ms

begin_exception_level2() - 실행 결과
[59770788] hello
[59770788] |-->hello2
[59770788] |<X-hello2 time=3ms 
ex=java.lang.IllegalStateException
[59770788] hello time=8ms 
ex=java.lang.IllegalStateException

 

 

 

 

 

 

 

 


필드 동기화 - 동시성 문제

 

 

기대하는 결과 - 로그 분리해서 확인하기

[52808e46]
[nio-8080-exec-3] [52808e46] OrderController.request()
[nio-8080-exec-3] [52808e46] |-->OrderService.orderItem()
[nio-8080-exec-3] [52808e46] |   |-->OrderRepository.save()
[nio-8080-exec-3] [52808e46] |   |<--OrderRepository.save() time=1001ms
[nio-8080-exec-3] [52808e46] |<--OrderService.orderItem() time=1001ms
[nio-8080-exec-3] [52808e46] OrderController.request() time=1003ms

[4568423c]
[nio-8080-exec-4] [4568423c] OrderController.request()
[nio-8080-exec-4] [4568423c] |-->OrderService.orderItem()
[nio-8080-exec-4] [4568423c] |   |-->OrderRepository.save()
[nio-8080-exec-4] [4568423c] |   |<--OrderRepository.save() time=1000ms
[nio-8080-exec-4] [4568423c] |<--OrderService.orderItem() time=1001ms
[nio-8080-exec-4] [4568423c] OrderController.request() time=1001ms

 

 

 

 

 

 

실제 결과 - 로그 분리해서 확인하기

[nio-8080-exec-3]
[nio-8080-exec-3] [aaaaaaaa] OrderController.request()
[nio-8080-exec-3] [aaaaaaaa] |-->OrderService.orderItem()
[nio-8080-exec-3] [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]
[nio-8080-exec-4] [aaaaaaaa] |    |   |-->OrderController.request()
[nio-8080-exec-4] [aaaaaaaa] |    |   |   |-->OrderService.orderItem()
[nio-8080-exec-4] [aaaaaaaa] |    |   |   |   |-->OrderRepository.save()
[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

 

 

 

 

 

 

 

동시성 문제

 

  • FieldLogTrace 는 싱글톤으로 등록된 스프링 빈이다. 이 객체의 인스턴스가 애플리케이션에 딱 1개만 존재한다는 뜻이다.
  • 하나만 있는 인스턴스의 FieldLogTrace.traceIdHolder 필드를 여러 쓰레드가 동시에 접근하기 때문에 문제가 발생

 

 

 

 

순차 진행 1(정상)

 

1. Thread-A 는 userA 를 nameStore 에 저장했다.

2. Thread-A 는 userA 를 nameStore 에서 조회했다.

 

 

순차 진행 2(정상)

 

3. Thread-B userB nameStore 에 저장했다.

4. Thread-B userB nameStore 에서 조회했다.

 

 

 

 

 


동시성 문제 발생

 

 

동시성 문제 발생 1

 

1. thread-A userA 값을 nameStore 에 보관한다.

2. 0.1초 이후에 thread-B userB 의 값을 nameStore 에 보관.  userA 값은 제거되고 userB 값이 저장된다.

 

 

 

동시성 문제 발생 2

 

3. thread-A 의 호출이 끝나고 기대했던 userA를 반환받는 대신 userB 의 값 반환받는다.

4. thread-B 의 호출이 끝나면서 nameStore 의 결과인 userB 를 반환받는다.

 

 

 

 

 

 

☆ 정리

  1. Thread-A 는 userA 를 nameStore 에 저장했다.
  2. Thread-B 는 userB 를 nameStore 에 저장했다.
  3. Thread-A 는 userB 를 nameStore 에서 조회했다.
  4. Thread-B 는 userB 를 nameStore 에서 조회했다.

 

 

 

 

동시성 문제

  • 결과적으로 Thread-A 입장에서는 저장한 데이터와 조회한 데이터가 다른 문제가 발생한다.
  • 여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라 한다.

 

 

 

 

참고

  • 동시성 문제는 지역 변수에서는 발생하지 않는다. 지역 변수는 쓰레드마다 각각 다른 메모리 영역이 할당된다.
  • 같은 인스턴스의 필드(주로 싱글톤에서 자주 발생), 또는 static 같은 공용 필드에 접근할 때 동시성 문제가 발생한다.
  •  동시성 문제는 값을 읽기만 하면 발생하지 않는다. 값을 변경하기 때문에 발생한다.

 

 

 

 

 

 

 


ThreadLocal - 소개

 

 

쓰레드 로컬 작동 원리

 

  • thread-A 가 조회하면 쓰레드 로컬은 thread-A 전용 보관소에서 userA 데이터를 반환한다.
  • thread-B 가 조회하면 thread-B 전용 보관소에서 userB 데이터를 반환한다.

 

 

 

 

 

 


쓰레드 로컬 동기화 - 개발

 

 

 

 

 

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) { }
    
    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());
        }
    }
    
    // addSpace(String prefix, int level) { }
}

 

 

ThreadLocal.remove()

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

 

 

 

 

 

 

로그 분리해서 확인하기

[nio-8080-exec-3]
[nio-8080-exec-3] [52808e46] OrderController.request()
[nio-8080-exec-3] [52808e46] |-->OrderService.orderItem()
[nio-8080-exec-3] [52808e46] |   |-->OrderRepository.save()
[nio-8080-exec-3] [52808e46] |   |<--OrderRepository.save() time=1001ms
[nio-8080-exec-3] [52808e46] |<--OrderService.orderItem() time=1001ms
[nio-8080-exec-3] [52808e46] OrderController.request() time=1003ms
[nio-8080-exec-4]
[nio-8080-exec-4] [4568423c] OrderController.request()
[nio-8080-exec-4] [4568423c] |-->OrderService.orderItem()
[nio-8080-exec-4] [4568423c] |   |-->OrderRepository.save()
[nio-8080-exec-4] [4568423c] |   |<--OrderRepository.save() time=1000ms
[nio-8080-exec-4] [4568423c] |<--OrderService.orderItem() time=1001ms
[nio-8080-exec-4] [4568423c] OrderController.request() time=1001ms

 

 

 

 

 

 

 


쓰레드 로컬 - 주의사항

 

 

 

 

 

 

사용자A 저장 요청 종료

 

 

 

 

 

 

 

사용자B 조회 요청

 

 

 

 

즉, 사용자B는 사용자A의 데이터를 확인하게 되는 심각한 문제가 발생하게 된다.
이런 문제를 예방하려면 사용자A의 요청이 끝날 때 쓰레드 로컬의 값을 ThreadLocal.remove() 를 통해서 꼭 제거해야 한다.

 

 

 

 

반응형

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

스프링이 지원하는 프록시  (0) 2023.08.17
동적 프록시 기술  (0) 2023.08.16
프록시 패턴과 데코레이터 패턴  (0) 2023.08.14
템플릿 메서드 패턴과 콜백 패턴  (0) 2023.08.13
예제 만들기  (0) 2023.08.12