👉 예시 :
- A의 계좌에 15만원이 있을 때, 두 명이 동시에 1만원씩 입금하면 정상 결과는 17만원이어야 한다.
- 하지만 두 스레드가 동시에 15만원을 읽어 각각 1만원을 더해 쓰면 최종 결과가 16만원으로 잘못 저장될 수 있다.
1. 상호 배제(Mutual Exclusion) : 한 스레드가 임계 구역을 실행 중이면 다른 스레드는 진입할 수 없어야 한다.
2. 진행(Progress) : 임계 구역에 실행 중인 스레드가 없을 때는, 대기 중인 스레드 중 하나가 반드시 진입할 수 있어야 한다.
3. 한정 대기(Bounded Waiting) : 특정 스레드가 무한정 대기하지 않도록 보장해야 한다. (기아 상태 방지)
⭐️ 1. 동기화 전략 (Lock 기반)
락을 걸어 순서를 보장하는 전통적인 방식
스레드가 임계 구역에 순차적으로 진입하도록 제어한다.
대표적인 해결 방법:
synchronized 블록이나 메서드가 대표적인 구현체2. 원자적 연산 사용 (Lock-free)
AtomicInteger.incrementAndGet()3. Thread-Safe 자료 구조 사용
ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedQueue💡 상황에 따라 성능과 안정성을 고려해 적절한 방법을 선택해야 한다.
- 락 기반 방식은 직관적이지만 성능 저하가 있을 수 있다.
- 원자적 연산은 빠르지만 복잡한 로직에는 한계가 있다.
- 복잡한 데이터 구조는 Thread-safe 자료구조를 사용하는 것이 더 안전하다.
ThreadLocal 기반 컨텍스트(MDC, SecurityContextHolder)는 스레드가 바뀌면 자동 전파되지 않는다. (사라짐)
ThreadLocal은 스레드의 로컬 컨텍스트 변수로 스레드가 살아있는 동안 계속해서 유지되는 변수를 말한다. (스레드의 글로벌 변수)
TaskDecorator는 스레드풀에서 실행되는 Runnable을 감싸 전·후 처리를 할 수 있게 해주는 인터페이스이다.ThreadLocal 기반 컨텍스트를 안전하게 전파할 수 있다.1. TaskDecorator 구현
public class ContextCopyingTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable task) {
// 1) Capture
Map<String, String> callerMdc = MDC.getCopyOfContextMap();
SecurityContext callerSec = SecurityContextHolder.getContext();
return () -> {
try {
// 2) Restore
if (callerMdc != null) MDC.setContextMap(callerMdc);
SecurityContextHolder.setContext(callerSec);
// 3) Run
task.run();
} finally {
// 4) Clear
MDC.clear();
SecurityContextHolder.clearContext();
}
};
}
}
2. 스레드풀에 등록 + @Async 메서드에서 활용
@EnableAsync
@Configuration
public class AsyncConfig {
@Bean(name = "appExecutor")
public ThreadPoolTaskExecutor appExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setTaskDecorator(new ContextCopyingTaskDecorator());
executor.initialize();
return executor;
}
}
@Service
public class MyService {
@Async("appExecutor")
public void doAsyncWork() {
// 여기서 MDC와 SecurityContext 정상 동작
}
}
🔁 스레드 간 컨텍스트 전달 원칙
- 캡처(Capture): 현재 스레드의 컨텍스트 저장
- 래핑(Wrap): 비동기 태스크를 컨텍스트 포함 형태로 감싸기
- 복원(Restore): 실행 스레드에서 컨텍스트 복원
- 정리(Clear): 실행 후 반드시 초기화 (스레드풀 재사용 시 누수 방지)
깔끔하고 정확하게 정리돼있어서 잘봤습니다!