티스토리 뷰
멀티스레드 환경에서 발생하는 대표적인 문제 중 하나인 경쟁 상태(Race Condition)에 대해 설명하고, 이를 해결하기 위한 다양한 전략을 설명해보세요.

경쟁 상태(Race Condition)란 무엇인가?
둘 이상의 프로세스나 스레드가 공유 자원에 동시에 접근하여 조작할 때, 그 실행 결과가 접근이 발생한 순서에 따라 달라지는 현상
단순히 속도가 느려지는 문제가 아니라, 계산 결과가 틀리거나 프로그램이 비정상적으로 동작할 수 있는 논리적 오류
왜 발생하는가?
아래와 같은 발생 조건이 존재함 이 중 하나라도 해당하지 않으면 경쟁 상태는 일어나지 않는다.
공유 자원의 존재: 여러 스레드가 동시에 접근 가능한 메모리, 변수, 파일 등이 존재해야 함.
동시성(Concurrency): 둘 이상의 스레드가 해당 자원을 이용하려고 함.
하나 이상의 쓰기(Write): 단순히 읽기만 하는 것이 아니라, 최소한 하나 이상의 스레드가 값을 변경하려고 시도함.
해결 전략
1. synchronized 키워드
자바의 가장 기초적인 동기화 도구. 특정 객체에 자물쇠(Lock)를 걸어 다른 스레드의 접근을 막는다
2. 원자적(Atomic) 클래스
하드웨어 수준의 CAS(Compare-And-Swap) 알고리즘을 사용하여, 스레드를 멈추게 하지 않고(Non-blocking) 원자적인 연산을 보장
3. 스레드 안전한(Thread-Safe) 컬렉션
멀티스레드 환경에서 안전하게 사용할 수 있도록 자바가 이미 구현해 둔 자료구조
ConcurrentHashMap, CopyOnWriteArrayList 등
비동기 환경에서 MDC(Logback Mapped Diagnostic Context)나 SecurityContext 같은 컨텍스트 정보를 스레드 간에 전달해야 할 경우, 처리하는 방법에 대해 설명하세요.
비동기 환경에서 MDC나 SecurityContext가 유지되지 않는 이유는 이들이 내부적으로 ThreadLocal을 사용하기 때문
ThreadLocal은 말 그대로 "스레드 단위의 로컬 저장소"이므로, 새로운 스레드가 생성되거나 스레드 풀의 다른 스레드로 작업이 넘어가면 기존 데이터에 접근할 수 없다.
왜 컨텍스트가 유실될까?
일반적인 웹 요청은 하나의 스레드에서 처리되지만, @Async나 CompletableFuture, 또는 ThreadPoolExecutor를 사용하는 순간 작업의 주체가 다른 스레드로 바뀝
- MDC: 로그에 추적 ID(Trace ID) 등을 남기기 위해 사용. 스레드가 바뀌면 ID가 비어있게 됨.
- SecurityContext: Spring Security의 인증 정보(Authentication)를 보유. 스레드가 바뀌면 "인증되지 않은 사용자"로 간주됨.
컨텍스트 손실은 어떤 문제를 발생시킬까?
- 보안 컨텍스트 손실 : 현재 사용자의 인증 정보와 권한 정보를 비동기 메서드에서 사용할 수 없다.
- 추적성 저하 : 로그 분석 시 요청을 추적하기 어려워진다. 특히 분산 추적(distributed tracing)에서 trace ID나 span ID가 전파되지 않는다.
- 트랜잭션 컨텍스트 손실 : 현재 트랜잭션의 상태나 설정 정보가 비동기 메서드로 전파되지 않는다.
- 커스텀 컨텍스트 손실 : 애플리케이션에서 정의한 사용자 정의 컨텍스트 정보들이 손실된다.
처리 방법

1. Spring의 TaskDecorator (가장 추천하는 방식)
Spring 프레임워크에서 제공하는 인터페이스로, 스레드 풀에서 스레드를 실행하기 직전에 '데코레이션'' 작업을 할 수 있게 함.
즉, 스레드 간 데이터 배달원 역할
동작 방식:
복사 : 메인 스레드(A)에서 데이터를 복사한다(Copy).
전달 : 새로운 스레드(B)로 데이터를 넘겨준다(Set).
제거 : 스레드(B)의 작업이 끝나면 데이터를 깨끗이 지운다(Clear).
장점: 로직과 설정이 분리되어 코드가 깔끔하며, MDC와 보안 컨텍스트를 동시에 처리하기에 가장 적합
2. Java의 InheritableThreadLocal (주의 필요)
동작 방식:
ThreadLocal 대신 부모 스레드의 값을 자식 스레드가 물려받는 InheritableThreadLocal을 사용
주의 사항 : 이 방식은 스레드 풀(Thread Pool) 환경에서 위험함. 스레드 풀은 스레드를 재사용하기 때문에, 이전에 사용했던 데이터가 지워지지 않고 남아있어 다른 사용자의 정보가 섞이는 심각한 보안 사고가 날 수 있다
고로 권장하지는 않음
3. Spring Security의 DelegatingSecurityContextAsyncTaskExecutor
동작 방식:
Wrapping(감싸기): 기존에 사용하던 스레드 실행기(AsyncTaskExecutor)를 이 클래스로 한 번 감싼다
자동 복사: 비동기 작업이 제출되는 시점에, 현재 스레드가 가진 SecurityContext(인증 정보)를 가로채서 보관
Context 주입: 새로운 스레드에서 작업이 실행될 때, 보관했던 인증 정보를 해당 스레드의 SecurityContextHolder에 자동으로 넣어줌
장점: 보안 컨텍스트를 넘기기 위해 별도의 복사 로직을 직접 코딩할 필요가 없어 설정이 매우 간편
