이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Race Condition과 Critical Section에서 붙잡아야 할 핵심 판단 기준은 무엇인가?
  • 결과가 실행 순서에 의존하면 위험하다라는 기준은 어떤 JVM/OS 신호로 확인되는가?
  • Java/Spring 코드에서 Race Condition과 Critical Section 판단 기준은 어떤 장애 신호로 드러나는가?

개요

이 문서의 주제는 Race Condition과 Critical Section이다. 공유 상태 변경에서 race condition이 생기는 원리와 critical section을 설명한다.

먼저 붙잡을 기준은 다음 문장이다. 결과가 실행 순서에 의존하면 위험하다. 같은 요청을 같은 순서로 보낸 것처럼 보여도 실제 서버에서는 여러 worker thread가 singleton bean, cache, counter, 컬렉션을 동시에 건드릴 수 있다.

핵심 모델

  • 판단 기준: 결과가 실행 순서에 의존하면 위험하다.
  • Race condition은 공유 상태를 읽고 쓰는 순서가 달라질 때 결과가 바뀌는 문제다.
  • Critical section은 동시에 실행되면 불변식이 깨지는 코드 구간이다.
  • Check-then-act, read-modify-write, get-then-put은 한 줄이어도 race 후보가 된다.
  • 예외 로그가 없어도 잘못된 수량, 중복 발급, 누락 같은 데이터 오류로 드러날 수 있다.
  • 해결책은 lock 추가뿐 아니라 불변 객체, DB constraint, queue 직렬화처럼 경계를 바꾸는 것까지 포함한다.

문서별 핵심 구분

  • race condition은 실행 순서에 따라 결과가 달라지는 문제다.
  • critical section은 동시에 실행되면 안 되는 공유 상태 구간이다.
  • 테스트 통과가 동시성 안전을 증명하지 않는다.

원리

Race condition은 두 개 이상의 실행 흐름이 같은 상태를 읽고 쓰는 순서에 따라 결과가 달라지는 문제다. 가장 흔한 형태는 read -> modify -> write가 원자적으로 실행되지 않는 경우다.

Critical section은 여러 thread가 동시에 지나가면 공유 상태의 불변식이 깨지는 구간이다. 중요한 점은 코드 줄 수가 아니라 “함께 실행되면 안 되는 상태 전이”를 찾는 것이다.

Spring singleton bean의 field, static cache, in-memory counter, 재사용되는 mutable DTO는 모두 critical section 후보가 된다. 반대로 method local variable처럼 요청마다 새로 만들어지는 상태는 보통 공유되지 않는다.

Race condition은 로그에 예외를 남기지 않고 데이터 값만 틀리게 만들 수 있다. 따라서 “에러 로그가 없다”가 안전을 뜻하지 않는다.

Java/Spring 연결

  • Spring singleton service의 mutable field는 모든 요청 thread가 공유한다.
  • @Transactional은 DB transaction 경계이지 Java heap 객체의 race를 자동으로 막지 않는다.
  • Controller나 service에서 누적 counter, 임시 상태, 마지막 요청값을 field에 저장하면 race 후보가 된다.
  • Race는 thread dump에 BLOCKED로 나타나지 않을 수 있어 코드 구조와 부하 재현을 함께 본다.
  • 단일 JVM에서 재현되지 않아도 다중 인스턴스에서는 DB row나 Redis key 경합으로 확대될 수 있다.
  • Spring 코드 리뷰에서는 Race Condition과 Critical Section 판단 기준이 어떤 thread, pool, timeout, resource 설정으로 드러나는지 확인한다.

코드와 설정 예시

private int issuedCouponCount;
 
public void issue() {
    if (issuedCouponCount < limit) {
        issuedCouponCount++;
    }
}

두 요청 thread가 동시에 issuedCouponCount를 읽으면 둘 다 limit 아래라고 판단할 수 있다. ++는 읽기, 증가, 쓰기의 복합 연산이므로 critical section을 보호하거나 DB constraint, atomic 연산, queue 같은 다른 경계로 옮겨야 한다.

관찰 명령

jcmd <pid> Thread.print
grep -R "static .*List\|static .*Map" -n src 2>/dev/null | head
curl -s localhost:8080/actuator/metrics/http.server.requests

Race Condition과 Critical Section 관찰 명령은 결론이 아니라 가설 확인 도구다. 운영 중에는 반복 간격, 대상 PID, 수집 시각을 함께 남기고 출력이 큰 명령은 범위를 제한한다.

지표 해석

신호먼저 의심할 것다음 확인
중복 발급/누락check-then-act race공유 counter, DB constraint
값이 간헐적으로 틀림read-modify-write racesingleton field, static state
부하 테스트에서만 재현timing 의존 race반복 횟수, thread 수
로그에는 정상 처리예외 없는 데이터 오류결과 검증, invariant

Race Condition과 Critical Section에서는 핵심 신호와 보조 지표를 같은 시간축에서 맞춘다. 먼저 변한 신호와 뒤따라 포화된 신호를 분리해야 원인과 결과를 구분할 수 있다.

장애 상황에서 보는 순서

  1. 깨진 데이터의 불변식이 무엇인지 먼저 적는다.
  2. 그 불변식을 읽고 쓰는 코드 경로와 공유 상태를 찾는다.
  3. Singleton field, static field, cache, counter, get-then-put 패턴을 확인한다.
  4. 반복 부하 테스트로 같은 조건에서 결과가 흔들리는지 본다.
  5. Lock, Atomic, DB constraint, queue 중 어느 경계가 맞는지 결정한다.

이 순서는 Race Condition과 Critical Section 후보를 빠르게 좁히기 위한 기본 루틴이다. 결론을 내리기 전에는 애플리케이션 증거와 JVM/OS 증거를 최소 한 번 이상 맞춘다.

실전 팁

  • Critical section은 “공유 상태를 읽고 쓰며 불변식을 확인하는 구간”으로 찾는다.
  • Race 의심 시 예외 로그보다 잘못된 결과의 발생 조건과 요청 동시성을 먼저 기록한다.
  • 재현 테스트는 thread 수, 반복 횟수, 데이터 초기화 조건을 고정해 여러 번 돌린다.
  • Lock을 넣기 전에 상태를 method local, immutable object, DB constraint로 옮길 수 있는지 본다.
  • 장애 회고에는 어떤 공유 상태가 있었고 어떤 순서에서 값이 깨졌는지 남긴다.

주니어 팁

  • Singleton bean field에 값이 바뀌는 변수가 있으면 먼저 의심한다.
  • count++처럼 한 줄이어도 읽기와 쓰기가 나뉜 복합 연산인지 확인한다.
  • 테스트 한 번 통과보다 반복 부하에서 값이 항상 맞는지가 더 중요하다.

시니어 팁

  • Thread-safe 설계의 기본은 lock 추가가 아니라 공유 mutable state 제거다.
  • 정합성은 Java lock, DB unique constraint, optimistic lock, queue 중 어느 계층이 책임질지 명확히 한다.
  • 다중 인스턴스 배포에서는 JVM 내부 lock이 전체 시스템의 critical section이 아니라는 점을 설계에 반영한다.

Guru급 팁

  • Race Condition과 Critical Section 주제는 단일 snapshot보다 변화 방향이 중요하다. 같은 명령을 간격을 두고 실행해 상태가 증가, 유지, 회복 중 어디인지 본다.
  • 고급 도구는 Race Condition과 Critical Section 가설을 좁힐 때만 사용하고 기본 지표와 모순되는지 먼저 확인한다.
  • Race Condition과 Critical Section 조치 후에는 CPU, memory, socket, disk, pool 중 다음 포화 지점이 어디로 이동했는지 확인한다.

위험 신호!

  • 에러 로그가 없다는 이유로 race 가능성을 배제한다.
  • Singleton bean field를 요청별 임시 저장소처럼 사용한다.
  • ++, check-then-act, get-then-put을 원자 연산으로 오해한다.
  • 단일 서버에서는 맞았다는 이유로 다중 인스턴스 정합성을 검토하지 않는다.
  • Lock만 추가하고 어떤 불변식을 보호하는지 기록하지 않는다.

확인 질문

확인 질문

  • Race condition이 예외 없이 데이터 오류로만 나타날 수 있는 이유는 무엇인가?
    • 여러 thread가 같은 값을 읽고 각자 정상 로직을 수행해도 쓰기 순서에 따라 최종 값만 틀릴 수 있기 때문이다.
  • Critical section을 찾을 때 기준은 무엇인가?
    • 공유 상태를 읽고 쓰며 “동시에 실행되면 깨지는 불변식”이 있는 구간인지 확인해야 한다.
  • Spring singleton bean에서 mutable field가 위험한 이유는 무엇인가?
    • Bean 인스턴스 하나를 여러 요청 thread가 공유하므로 field가 요청 간 공유 상태가 되기 때문이다.

참고 문서