반응형
본 내용은 인프런의 김영한 님의 강의 "스프링 핵심 원리 - 고급편" 내용을 바탕으로 정리한 내용입니다.
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 |