반응형

본 내용은 인프런의 김영한 님의 강의 "스프링 핵심 원리 - 고급편" 내용을 바탕으로 정리한 내용입니다.

ThreadLocal 이란?

  • ThreadLocal은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다.
  • 이것은 멀티스레드 환경에서 쓰레드 간에 데이터를 공유하지 않고도 각각의 쓰레드에서 데이터를 사용하고 유지할 수 있는 방법을 제공한다. 따라서 같은 인스턴스의 쓰레드 로컬 필드에 접근해도 문제없다. ex) 창고에서 사용자A, 사용자B 모두 창구 직원을 통해서 물건을 보관하고, 꺼내지만 창구 지원이 사용자에 따라 보관한 물건을 구분해주는 것이다.
  • 쓰레드 로컬을 모두 사용하면 remove() 로 저장된 값을 제거해야 한다.

ThreadLocal 구현 예시

ThreadLocalService.class

  • 쓰레드 로컬 사용
package hello.advanced.trace.threadlocal.code;  

import lombok.extern.slf4j.Slf4j;  

@Slf4j  
public class ThreadLocalService { {  

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

    /**  
     * name 출력 후 1초동안 쓰레드를 멈추고 그 뒤에 nameStore 값을 출력  
     * @param name  
     * @return  
     */  
    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();  
    }  

    /**  
     *  쓰레드를 멈춘다.  
     * @param millis  
     */  
    private void sleep(int millis) {  
        try {  
            Thread.sleep(millis);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}
}

FieldServiceTest.class

  • 테스트 실행 코드
package hello.advanced.trace.threadlocal;  

import hello.advanced.trace.threadlocal.code.FieldService;  
import lombok.extern.slf4j.Slf4j;  
import org.junit.jupiter.api.Test;  

@Slf4j  
public class FieldServiceTest {  

    private FieldService fieldService = new FieldService();  

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

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

        threadA.start();  
//        sleep(2000); //동시성 문제 발생X  
        sleep(100); //동시성 문제 발생O  
        threadB.start();  

        sleep(3000); //메인 쓰레드 종료 대기  
        log.info("main exit");  
    }  

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

테스트 결과

main start
저장 name=userA -> nameStore=null
저장 name=userB -> nameStore=null
조회 nameStore=userA
조회 nameStore=userB
main exit
  • 원래는 테스트 코드 실행 시 threadA와 threadB의 동시성 문제가 발생하지만 쓰레드 로컬을 사용하여 두 쓰레드가 개별적으로 출력된다.

쓰레드 로컬을 이용한 로그 추적기 구현

LogTrace.interface

  • 로그 trace 인터페이스
package hello.advanced.trace.logtrace;  

import hello.advanced.trace.TraceStatus;  

public interface LogTrace {  

    TraceStatus begin(String message);  

    void end(TraceStatus status);  

    void exception(TraceStatus status, Exception e);  
}

ThreadLocalLogTrace.class

  • 쓰레드 로컬을 활용한 로그 trace 구현
package hello.advanced.trace.logtrace;  

import hello.advanced.trace.TraceId;  
import hello.advanced.trace.TraceStatus;  
import lombok.extern.slf4j.Slf4j;  

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

    private void syncTraceId() {  
        TraceId traceId = traceIdHolder.get();  
        if (traceId == null) {  
            traceIdHolder.set(new TraceId());  
        } else {  
            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.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 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();  
    }  
}

ThreadLocalLogTraceTest.class

  • 테스트 실행 코드
package hello.advanced.trace.logtrace;  

import hello.advanced.trace.TraceStatus;  
import org.junit.jupiter.api.Test;  

public class ThreadLocalLogTraceTest {  

    ThreadLocalLogTrace trace = new ThreadLocalLogTrace();  

    @Test  
    void begin_end_level() {  
        TraceStatus status1 = trace.begin("hello1");  
        TraceStatus status2 = trace.begin("hello2");  
        trace.end(status2);  
        trace.end(status1);  
    }  

    @Test  
    void begin_exception_level() {  
        TraceStatus status1 = trace.begin("hello1");  
        TraceStatus status2 = trace.begin("hello2");  
        trace.exception(status2, new IllegalStateException());  
        trace.exception(status1, new IllegalStateException());  
    }  
}

테스트 결과

[7d4ce94c] hello1
[7d4ce94c] |-->hello2
[7d4ce94c] |<--hello2 time=0ms
[7d4ce94c] hello1 time=15ms

[306ab10c] hello1
[306ab10c] |-->hello2
[306ab10c] |<X-hello2 time=0ms ex=java.lang.IllegalStateException
[306ab10c] hello1 time=0ms ex=java.lang.IllegalStateException
  • begin_end_level 메소드 확인 결과 두 쓰레드 의 로그가 오류없이 로그가 정상적으로 출력된다.
  • begin_exception_level 메서드 확인 결과 exception 을 발생해서 예외와 함께 로그가 출력된다.

ThreadLocal 사용시 주의 사항

  • 쓰레드 로컬의 값을 사용 후 제거하지 않으면 WAS 처럼 쓰레드 풀을 사용하는 경우 심각한 문제가 발생할 수 있다.
  • 이전에 조회한 쓰레드 로컬에 담긴 값을 다른 쓰레드에서 접근할 수 있는 경우가 생길 수 있기에 사용 후 반드시 제거해야 한다.
반응형

'Spring' 카테고리의 다른 글

[스프링] 프록시란?  (0) 2024.05.11
[스프링] 템플릿 메서드 패턴  (0) 2024.05.04
[스프링] 로그 추적기  (0) 2024.05.04
[스프링] 빈 스코프란?  (0) 2023.07.23
[스프링] 스프링 빈의 생명주기 콜백  (0) 2023.07.23

+ Recent posts