오늘은 ThreadLocal과 MDC 관련해 포스팅해보고자 한다.
우선 ThreadLocal
ThreadLocal은 무엇인가?
정의를 ChatGpt에게 물어봤다.
ThreadLocal은 프로그래밍에서 스레드 간에 데이터를 공유하지 않고, 각 스레드에게 독립적으로 데이터를 제공하기 위한 메커니즘입니다. 자바에서 ThreadLocal은 스레드 수준의 데이터 저장소를 제공하는 클래스입니다.
ThreadLocal을 사용하면 한 스레드에서 저장한 데이터를 다른 스레드에서 직접 접근할 수 없고, 각 스레드에게 독립적인 데이터를 제공할 수 있습니다. 이는 스레드 간의 데이터 공유를 방지하고 스레드 간에 충돌이나 경합 상태를 피할 수 있도록 도와줍니다.
by ChatGpt
즉, 스레드 별로 관리할 수 있는 저장소라고 할 수 있다.
가령 다음과 같이 Thread별 저장소를 관리할 수 있다.
public class Example {
// 각 스레드에게 독립적인 ThreadLocal 변수를 생성
private static ThreadLocal<String> threadLocalVariable = new ThreadLocal<>();
public static void main(String[] args) {
// 첫 번째 스레드에서 데이터 설정
Thread thread1 = new Thread(() -> {
threadLocalVariable.set("Data from Thread 1");
// 다른 코드 수행
});
// 두 번째 스레드에서 데이터 설정
Thread thread2 = new Thread(() -> {
threadLocalVariable.set("Data from Thread 2");
// 다른 코드 수행
});
thread1.start();
String dataFromThread1 = threadLocalVariable.get(); // 이 시점에서는 "Data from Thread 1" 반환
thread2.start();
String dataFromThread2 = threadLocalVariable.get(); // 이 시점에서는 "Data from Thread 2" 반환
// 다른 코드 수행
}
}
ThreadLocal은 스프링 MVC에서 보다 강력한 힘을 발휘하는데, 스프링 MVC는 대게 구현 시 1 request 1thread의 형식을 취하기 때문에 각 요청마다 다른 저장소를 사용하고 싶을 때 많이 쓰인다.
가령, 스프링의 TransactionSyncronizationManager가 같은 트랜잭션(커넥션)을 공유하기 위해 ThreadLocal을 사용한다. (관련 개념은 보다 복잡한데, 이는 나중에 글로 한번 정리해봐야겠다.)
그럼 다음과 같이 질문이 생각날 수도 있다.
‘Async로 다른 스레드에서 동작한다면?’
당연히 다른 스레드에서 동작하므로 다른 ThreadLocal 저장소를 사용하게 되고, 다른 커넥션을 사용하게 되어 조심해야한다.
가령, 다른 클래스의 @Transactional과 @Async 메서드를 for문으로 부르면 커넥션 풀에서 엄청나게 많은 커넥션을 가져다가 쓰게 되기 때문에 DB 커넥션이 부족해지는 문제가 발생할 수 있다.
MDC
MDC란? 위에서 열심히 설명한 ThreadLocal 이라는 도구를 활용해 로깅과 트레이싱이라는 요구사항 에 맞게 추상화한 도구가 바로 MDC(Mapped Diagnostic Context)이다.
로깅이 필요할 때 요청 별 ID (보통 TraceId라고 한다.)를 만들어 ThreadLocal에 저장하고 필요할 때 꺼내쓰는 역할을 하는 것이 MDC이다.
logback 혹은 slf4j 같은 로깅 라이브러리가 내부적으로 MDC 를 사용해서 요청마다 TraceID 를 생성하고 관리해주고 있어 우리는 편하게 이를 사용하면 된다.
MDC가 아니었다면 매번 로깅이 필요할 때 마다 ThreadLocal에서 이 정보를 넣었다 뺐다 했어야 할것이다.
이어서 위에서 했던 질문과 같은 질문을 해보자.
‘비동기로 다른 스레드에서 로깅을 남기면 실제로 TraceId가 다른 값이 들어가는가?’
답은 맞다.이다.
(MDCAdapter 구현체에 따라 아닐 수도 있다. slf4J에서 제공하는 MDCAdapter 구현체를 사용한다면 InheritableThreadLocal을 사용해 아니다. 근데 InheritableThreadLocal을 사용하면 사이드 이펙트가 많다고 하는데,, 스레드 풀에서 가져다가 만들다보니 새로운 비동기 처리를 할 때 지정한 스레드가 상속받아 만든 스레드가 아니라 다른 스레드가 배정될 때가 있을 수 있을 것 같다. 자세한건 아직 조사를 안해봐서 패스..)
다른 스레드에는 이 스레드 로컬에 저장된 context 정보가 전달이 되지 않고 새로 만들어지기 때문에 값이 올바르게 전달되지 않을 것이다.
‘엥? 그러면 어떻게 하지? 비동기나 코루틴에서는 log를 못찍는건가?’ 라고 생각할 수 있는데 역시.. 다 해결방법은 있었다.
비동기 시 MDC가 정상동작하게 하는 방법
Spring 4.3 이상부터 제공되는 TaskDecorator 를 이용해서 비동기처리하는 taskExecutor 생성시 커스터마이징이 가능하다.
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(20);
taskExecutor.setMaxPoolSize(50);
taskExecutor.setQueueCapacity(200);
taskExecutor.setMaxPoolSize(50);
// 데코레이터 설정
taskExecutor.setTaskDecorator(new TaskDecoratorImpl());
taskExecutor.setThreadNamePrefix("async");
taskExecutor.setThreadGroupName("async-group");
return taskExecutor;
}
}
public class TaskDecoratorImpl implements TaskDecorator {
@Override
public Runnable decorate(Runnable task) {
// 디음 스레드가 시작하기 전 이전 스레드로컬의 context 정보를 가져옴
Map<String, String> callerThreadContext = MDC.getCopyOfContextMap();
return () -> {
// 다음 스레드로컬에 그대로 전달
MDC.setContextMap(callerThreadContext);
task.run();
};
}
}
매우 간단하게 해결 가능하다.
코틀린 코루틴에서 해결하기
코루틴 스코프를 기본 사용하던 스레드에서 그대로 사용하면 당연히 정상 동작하겠지만, Dispatchers.Default 등 다른 디스패처를 사용하면 해당 디스패처는 별도의 스레드 풀을 관리하니 로그가 남지 않는 것은 이제 이해가 됐을 것이다.
그럼 스프링 MVC 환경에서는 기존 사용하던 스레드만 그대로 사용해야하는 것인가? 여러 스레드를 사용할 수는 없을까?
다행히도 코틀린은 이를 위해 MDCContext라는 것을 이미 만들어 놓았다. 다음과 같이 사용하면 된다.
launch(MDCContext()) {
logger.info { "..." } // The MDC context contains the mapping here
}
runBlocking{
}
그럼 도대체 어떻게 MDCContext는 여러 스레드 풀을 사용할 수 있는 코루틴에서 이를 가능하게 하는걸까?
궁금하니 코드를 한번 까보자
생각보다 매우 간단한데, 인스턴스가 생성되는 시점에 MDC에서 context 정보를 가져오고, 코루틴 내부에서 스레드 전환시에 updateThreadContext() 메서드가 호출되어 새로운 스레드의 MDC의 contextMap 변수를 교체해준다.
결과적으로 MDC에 관한 정보는 MDCContext 내부에 저장되고 이를 관리하여 코루틴 내부에서 스레드가 전환되더라도 MDC가 정상동작할 수 있다.
마무리
ThreadLocal은 매우 간편해보이지만, 위험한 부분이 많이 존재한다. 예를들어 스레드 풀 환경에서 스레드는 사용되고 삭제되는 것이 아니라 재사용되어 ThreadLocal의 정보가 그대로 남아있게 되어 메모리에 문제가 생기거나 다른 스레드에서 이전 요청의 정보를 볼 수 있다거나 하는 부분이 존재한다. 또 virtual thread 같은 기술과 함께 쓸 때 너무 많은 메모리를 사용하는 문제가 발생할 수도 있어 조심해야한다.
'서버 > 스프링' 카테고리의 다른 글
@SpringBootTest의 webEnvironment를 통한 통합테스트 환경 구축 및 비교하기 (0) | 2024.11.03 |
---|---|
[스프링 AOP] 3. 스프링 AOP 스프링에서 빈이 프록시로 변환되는 과정 (0) | 2024.11.03 |
[스프링 AOP] 2. 스프링 AOP 프록시 생성 원리 (0) | 2024.10.02 |
[스프링 AOP] 1. 스프링 AOP와 Proxy 패턴 (6) | 2024.09.28 |
[JPA] 기본 키 매핑 전략 정리 (0) | 2022.10.30 |