1. Race Condition (경쟁 상태)
Race Condition이란?
두 개 이상의 스레드가 공유 자원에 동시에 접근할 때, 실행 순서에 따라 결과가 달라지는 문제입니다.
재고: 1개
스레드 A: 재고 조회 → 1개 확인 → (아직 차감 전)
스레드 B: 재고 조회 → 1개 확인 → (아직 차감 전)
스레드 A: 재고 차감 → 0개
스레드 B: 재고 차감 → -1개 ← 문제 발생!
해결 전략 1 - synchronized (Java 내장)
JVM 레벨에서 하나의 스레드만 해당 블록에 진입하도록 잠금을 겁니다.
public class StockService {
private int stock = 1;
public synchronized void decreaseStock() {
if (stock <= 0) throw new RuntimeException("재고 없음");
stock--;
}
}
장점: 별도 의존성 없이 간단하게 적용
단점: 서버가 여러 대이면 효과 없음 (JVM 단위 잠금이라 다른 서버는 영향 없음)
해결 전략 2 - DB 비관적 락 (Pessimistic Lock)
조회 시점에 DB 레벨에서 락을 걸어 다른 트랜잭션이 접근하지 못하게 합니다.
public interface StockRepository extends JpaRepository<Stock, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT s FROM Stock s WHERE s.id = :id")
Optional<Stock> findByIdWithLock(@Param("id") Long id);
}
@Service
@Transactional
public class StockService {
public void decreaseStock(Long id) {
Stock stock = stockRepository.findByIdWithLock(id)
.orElseThrow(); // SELECT ... FOR UPDATE → 다른 트랜잭션 대기
stock.decrease(); // 안전하게 차감
}
}
장점: 충돌이 잦은 환경에서 데이터 정합성 확실히 보장
단점: 락 대기로 인한 성능 저하, 데드락 위험
해결 전략 3 - DB 낙관적 락 (Optimistic Lock)
락을 걸지 않고, 버전 번호로 충돌을 감지합니다. 충돌 시 재시도 로직이 필요합니다.
@Entity
public class Stock {
@Id
private Long id;
private int quantity;
@Version // 버전 필드 추가
private Long version;
}
// 충돌 시 OptimisticLockException 발생 → 재시도
@Service
public class StockFacade {
public void decreaseStock(Long id) throws InterruptedException {
while (true) {
try {
stockService.decrease(id);
break;
} catch (OptimisticLockingFailureException e) {
Thread.sleep(50); // 잠시 대기 후 재시도
}
}
}
}
장점: 락을 걸지 않아 충돌이 적은 환경에서 성능 우수
단점: 충돌이 잦으면 재시도 오버헤드 발생
해결 전략 4 - Redis 분산 락 (Distributed Lock)
서버가 여러 대인 환경에서 Redis를 통해 전체 서버에 잠금을 겁니다.
// Redisson 라이브러리 사용
@Service
public class StockService {
private final RedissonClient redissonClient;
public void decreaseStock(Long id) {
RLock lock = redissonClient.getLock("stock:lock:" + id);
try {
// 락 획득 시도 (최대 5초 대기, 3초 후 자동 해제)
if (!lock.tryLock(5, 3, TimeUnit.SECONDS)) {
throw new RuntimeException("락 획득 실패");
}
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease();
stockRepository.save(stock);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 반드시 해제
}
}
}
장점: 멀티 서버 환경에서도 정합성 보장, 유연한 만료 시간 설정
단점: Redis 서버 의존성 추가, 네트워크 비용 발생
해결 전략 5 - DB Unique 제약 + 멱등성 설계
동일한 요청이 중복 처리되지 않도록 멱등 키를 사용합니다.
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = "idempotencyKey"))
public class Order {
@Column(unique = true)
private String idempotencyKey; // 클라이언트가 생성한 고유 키
}
// 동일한 idempotencyKey로 중복 요청 시 DB Unique 제약 위반 → 무시
전략 비교 정리
전략 적합한 환경 성능 복잡도
| synchronized | 단일 서버, 단순 케이스 | 중간 | 낮음 |
| 비관적 락 | 충돌 빈번, 정합성 최우선 | 낮음 | 낮음 |
| 낙관적 락 | 충돌 드문 환경 | 높음 | 중간 |
| Redis 분산 락 | 멀티 서버 환경 | 중간 | 높음 |
| 멱등성 설계 | 중복 요청 방지 | 높음 | 중간 |
2. 비동기 환경에서 Context 전달
문제 상황
Spring의 MDC와 SecurityContext는 ThreadLocal 기반입니다.
메인 스레드: MDC에 requestId="abc" 저장, SecurityContext에 사용자 정보 저장
@Async 실행 → 새로운 스레드 생성
새 스레드: MDC 비어있음, SecurityContext 비어있음 ← 정보 전달이 안 됨
@Service
public class AsyncService {
@Async
public void doAsync() {
String requestId = MDC.get("requestId"); // null ← 문제
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // null
}
}
MDC 전달 방법
방법 1 - TaskDecorator 구현
@Component
public class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 호출 스레드(메인)의 MDC 복사
Map<String, String> mdcContext = MDC.getCopyOfContextMap();
return () -> {
try {
// 새 스레드에 MDC 설정
if (mdcContext != null) {
MDC.setContextMap(mdcContext);
}
runnable.run();
} finally {
MDC.clear(); // 스레드 풀 재사용 시 오염 방지
}
};
}
}
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor asyncExecutor(MdcTaskDecorator decorator) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setTaskDecorator(decorator); // 여기에 적용
executor.initialize();
return executor;
}
}
SecurityContext 전달 방법
방법 1 - SecurityContextHolder 전략 변경
// 애플리케이션 시작 시 전략 변경
@PostConstruct
public void init() {
// MODE_INHERITABLETHREADLOCAL: 자식 스레드에 자동 상속
SecurityContextHolder.setStrategyName(
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
);
}
⚠️ 스레드 풀 환경에서는 스레드가 재사용되므로 이전 컨텍스트가 남아있을 수 있습니다.
방법 2 - TaskDecorator에서 SecurityContext도 함께 전달 (권장)
@Component
public class ContextCopyTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 호출 스레드의 컨텍스트 캡처
Map<String, String> mdcContext = MDC.getCopyOfContextMap();
SecurityContext securityContext = SecurityContextHolder.getContext();
return () -> {
try {
// MDC 복원
if (mdcContext != null) {
MDC.setContextMap(mdcContext);
}
// SecurityContext 복원
SecurityContextHolder.setContext(securityContext);
runnable.run();
} finally {
MDC.clear();
SecurityContextHolder.clearContext(); // 반드시 클리어
}
};
}
}
CompletableFuture 환경에서 MDC 전달
@Async 없이 직접 CompletableFuture를 사용하는 경우입니다.
@Service
public class AsyncService {
public CompletableFuture<String> doAsync() {
Map<String, String> mdcContext = MDC.getCopyOfContextMap();
return CompletableFuture.supplyAsync(() -> {
// MDC 수동 복원
if (mdcContext != null) MDC.setContextMap(mdcContext);
try {
return someLogic();
} finally {
MDC.clear();
}
}, asyncExecutor);
}
}
WebFlux (Reactor) 환경에서 Context 전달
Reactor는 ThreadLocal을 사용할 수 없습니다. Reactor Context를 사용합니다.
// Reactor Context에 저장
Mono.deferContextual(ctx -> {
String requestId = ctx.get("requestId");
MDC.put("requestId", requestId);
return Mono.just(processRequest());
})
.contextWrite(Context.of("requestId", "abc123"));
정리
환경 MDC 전달 방법 SecurityContext 전달 방법
| @Async | TaskDecorator | TaskDecorator 또는 INHERITABLETHREADLOCAL |
| CompletableFuture | 수동 캡처 후 복원 | 수동 캡처 후 복원 |
| WebFlux | Reactor Context | ReactiveSecurityContextHolder |
핵심 원칙
- ThreadLocal은 스레드 경계를 넘지 못함
- 새 스레드 시작 전에 컨텍스트를 캡처하고, 새 스레드에서 복원 후 작업
- 스레드 풀 재사용 시 반드시 finally 블록에서 클리어
'Weekly Paper' 카테고리의 다른 글
| Weekly Paper #14 (0) | 2026.06.09 |
|---|---|
| Weekly Paper #13 (0) | 2026.06.09 |
| Weekly Paper #12 (0) | 2026.05.11 |
| Weekly Paper #10 (0) | 2026.04.01 |
| Weekly Paper #9 (0) | 2026.03.23 |