📑 목차
레이스 컨디션이 "가끔만" 터지는 이유
레이스 컨디션은 같은 코드가 같은 입력을 받아도 실행할 때마다 결과가 달라지는 상황을, "확인→완화→복구→검증" 순서로 정리하는 데서 출발한다.
겉으로는 "가끔"이지만, 내부에서는 스레드/프로세스의 실행 순서가 매번 달라지는 구조가 반복된다.
CPU 코어 수, OS 스케줄러, GC 타이밍, I/O 지연, 캐시 히트/미스 같은 작은 차이가 타이밍 창을 열고 닫는다.
테스트 환경에서는 타이밍 창이 잘 안 열리다가, 운영에서 트래픽과 병렬성이 늘어나면 갑자기 열리는 경우가 흔하다.
레이스 컨디션을 만드는 구조: 공유 상태와 인터리빙
레이스 컨디션은 "공유되는 변경 가능한 상태(shared mutable state)"를 여러 실행 흐름이 동시에 건드릴 때, 접근 순서(인터리빙)가 우연히 맞아떨어져 잘못된 결과가 나오는 구조다.
보통 다음 3가지가 동시에 성립할 때 발생한다.
- 공유 상태가 존재한다(카운터, 잔액, 재고, 캐시 엔트리, 전역 맵 등).
- 동시 접근이 일어난다(스레드/코루틴/요청 핸들러가 병렬 실행된다).
- 동기화가 부족하다(락/원자 연산/단일 스레드 처리 등 보호 장치가 없다).
문제의 핵심은 "한 줄짜리 연산처럼 보이는 동작"도 내부적으로는 여러 단계(읽기→계산→쓰기)로 쪼개진다는 점이다.
이 단계들이 서로 다른 스레드의 단계와 섞이는 순간, 값이 덮어써지거나 일부 업데이트가 사라지는 현상이 생긴다.

인터리빙은 "어떤 순서로 끼어드는가"의 문제이므로, 재현이 어려울수록 레이스 컨디션일 가능성이 커진다.
반대로 매번 100% 재현되는 버그는 동시성 문제라기보다, 단순 로직 오류나 입력 검증 실패일 가능성이 더 높다.
원자성과 가시성: 예방 방법이 갈리는 지점
레이스 컨디션 예방 방법은 결국 두 축으로 갈린다.
첫째는 연산을 쪼개지지 않게 만드는 "원자성(Atomicity)"이고, 둘째는 다른 실행 흐름이 변경을 제때 볼 수 있게 만드는 "가시성(Visibility)"이다.
원자성: 읽기-계산-쓰기를 한 덩어리로 만들기
원자성은 중간 단계가 관측되지 않도록, 연산이 한 번에 일어난 것처럼 보이게 만드는 성질이다.
특징은 "업데이트 손실(lost update)" 같은 결과 오류가 줄어든다는 점이다.
주의점은 "원자적으로 보이는 연산"이 언어/런타임마다 다르다는 점이며, 단순 증가 연산도 원자적이지 않은 경우가 많다.
아래는 레이스 컨디션이 숨어들기 쉬운 전형적 형태를 보여준다.
// 의도: counter를 1 증가
// 현실: read(counter) → +1 → write(counter) 로 쪼개질 수 있다.
counter = counter + 1
위 형태가 여러 실행 흐름에서 동시에 수행되면, 증가가 2번 일어났는데도 결과는 1만 증가하는 식의 손실이 발생할 수 있다.
가시성: 한쪽의 변경이 다른 쪽에 늦게 보이는 문제
가시성은 한 실행 흐름이 바꾼 값을 다른 실행 흐름이 "언제" 보게 되는가에 관한 성질이다.
특징은 값이 틀리게 계산되기보다는, 오래된 값(stale value)을 기반으로 잘못된 판단을 하는 형태로 드러나는 경우가 많다는 점이다.
주의점은 단순히 락을 걸지 않는 것뿐 아니라, 메모리 재정렬이나 캐시 동기화 같은 낮은 층의 동작까지 영향을 받을 수 있다는 점이다.
예방 방법 비교표: 락, 원자 연산, 단일 스레드화
레이스 컨디션을 막는 방법은 "공유 상태를 보호하느냐, 공유 자체를 줄이느냐"로 정리할 수 있다.
| 방법 | 설명 |
|---|---|
| 뮤텍스/락 | 뮤텍스/락으로 임계 구역을 보호하고, 적용 범위가 넓고 직관적이며, 원자성과 가시성을 함께 확보하기 쉽다. 다만 락 범위가 커지면 병목이 생기고, 잘못 쓰면 데드락/우선순위 역전이 생길 수 있다. |
| 원자 연산 | 원자 연산(Atomic)이나 CAS로 단일 변수 업데이트를 안전하게 만들고, 락보다 오버헤드가 낮고, 간단한 카운터/플래그에 유리하다. 다만 여러 변수를 함께 갱신하는 복합 불변식은 원자 연산만으로 깨끗하게 해결하기 어렵다. |
| 단일 스레드화 | 단일 스레드 처리(큐/이벤트 루프)로 공유 상태의 동시 접근을 제거하고, 레이스 컨디션 자체가 구조적으로 사라지며, 설계가 단순해진다. 다만 큐 지연이 곧 처리 지연이 되므로, 처리량/지연 목표를 기준으로 설계해야 한다. |
어떤 방법이든 "공유 상태의 범위를 좁히고, 임계 구역을 짧게 만들며, 불변식을 문서화한다"는 원칙이 따라온다.
확인-완화-수정-검증 순서로 점검하는 절차
레이스 컨디션은 한 번의 증상만으로 단정하기 어렵다.
따라서 확인 단계에서 "레이스가 맞는가"를 먼저 가르는 체크가 필요하다.
YES/NO 판단 체크리스트
- YES/NO: 동일 입력인데 결과가 가끔 달라지는가(비결정성)?
- YES/NO: 병렬성(스레드 수/동시 요청 수)을 올리면 재현 확률이 올라가는가?
- YES/NO: 로그에 "순서가 뒤집힌 흔적"이 남는가(예: 상태가 되돌아감, 값이 건너뜀)?
- YES/NO: 공유 상태를 보호하는 락/원자 연산/큐가 코드에 명확히 보이는가?
- YES/NO: 임계 구역 안에서 I/O(네트워크/DB/파일)를 수행하는가?

실전 점검 절차: 확인-완화-수정-검증
- 확인: 로그에 "값의 불연속"을 남긴다(요청 단위로 before/after를 함께 기록한다).
- 확인: 병렬성을 인위적으로 올려 재현율을 올린다(부하 테스트, 스레드 수 증가, 반복 실행).
- 완화: 공유 상태 접근을 임시로 직렬화한다(락 범위 확대 또는 단일 큐 처리)로 증상이 사라지는지 본다.
- 수정: 최종 구조를 선택한다(락/원자 연산/단일 스레드화 중 하나로 불변식을 지키는 형태로 재설계한다).
- 검증: 경쟁 조건을 노린 테스트를 만든다(동시 요청, 반복 실행, 타이밍 삽입)로 회귀를 막는다.
- 검증: 운영에서 관측 지표를 둔다(오류율뿐 아니라 "값 불일치 카운트" 같은 도메인 지표를 둔다).
메뉴/경로 기준으로 바로 실행하는 점검 포인트
- GitHub Actions: Actions → (워크플로우 선택) → Run workflow에서 동시성 테스트를 반복 실행한다.
- IntelliJ IDEA: Run → Edit Configurations → (해당 실행) → Modify options에서 테스트 반복/병렬 옵션을 추가한다.
- VS Code: Terminal → New Terminal에서 레이스 탐지 옵션이 있는 테스트 커맨드를 반복 실행한다.
실전 예시: 합계가 "가끔만" 1씩 틀리는 버그
주문 처리에서 총수량을 누적하는 로직이 있고, 동시 요청이 들어오면 합계가 가끔 1씩 덜 올라가는 증상이 발생한다고 가정한다.
운영 로그에 다음 같은 조각이 남는다.
WARN total_mismatch expected=120 actual=119 orderBatch=2025-12-26T21:10:14Z
이 경우 "누적 변수 업데이트가 원자적이지 않다"는 가설을 먼저 세운다.
임시 완화로 누적 구간을 락으로 감싸거나, 누적 요청을 큐로 모아 단일 실행 흐름에서 처리해 증상이 사라지는지 확인한다.
증상이 사라진다면 최종 수정은 불변식 단위로 결정한다(단일 카운터면 원자 연산, 복합 상태면 락/트랜잭션/단일 스레드화).
검증 단계에서는 동시 요청을 집중시키는 테스트를 추가하고, 값 불일치 카운트를 지표로 남겨 재발을 조기에 탐지한다.
결론
레이스 컨디션은 "공유 상태 + 동시 접근 + 부족한 보호"가 겹칠 때, 실행 순서(인터리빙)가 바뀌면서 가끔만 드러나는 구조다.
예방 방법은 원자성과 가시성을 어떤 방식으로 확보할지의 선택이며, 락, 원자 연산, 단일 스레드화는 각각 적용 범위와 비용이 다르다.
현장에서는 확인-완화-수정-검증 순서로 접근하고, 증상 완화가 되면 구조를 바꿔 재발을 막는 쪽으로 마무리한다.
연계 주제로는 데드락과 라이브락의 차이, 동시성 테스트 설계(재현율 올리기), 분산 환경에서의 일관성 문제가 이어진다.
FAQ
Q1. 레이스 컨디션과 데드락은 무엇이 다른가
레이스 컨디션은 실행 순서가 섞이면서 결과가 틀어지는 문제다.
데드락은 서로 락을 잡고 기다리며 진행이 멈추는 문제다.
레이스는 "가끔 틀림", 데드락은 "멈춤"으로 관측되는 경우가 많다.
Q2. 락을 걸면 레이스 컨디션이 항상 해결되는가
락이 임계 구역을 정확히 감싸고, 공유 상태의 모든 접근 경로가 같은 락 규칙을 따르면 해결 가능하다.
락 범위가 빠지거나, 다른 락과 섞이거나, 락 밖에서 공유 상태를 건드리면 해결되지 않는다.
성능 병목과 데드락 위험까지 함께 점검해야 한다.
Q3. 테스트에서는 안 나고 운영에서만 터지는 이유는 무엇인가
운영은 동시 요청 수, 코어 수, I/O 지연, GC, 캐시 상태가 더 다양해 타이밍 창이 열릴 확률이 높다.
테스트에서는 실행 순서가 우연히 안정적으로 유지되어 증상이 숨는 경우가 많다.
병렬성을 의도적으로 올리는 테스트와 값 불일치 지표를 함께 두는 방식이 재발 방지에 유리하다.
'전산학' 카테고리의 다른 글
| CAP 정리 쉽게 이해하기: 일관성·가용성·분할 허용 중 무엇을 선택하는가 (0) | 2026.01.02 |
|---|---|
| 분산 시스템에서 ‘일관성’이란 무엇인가: 동시에 업데이트할 때 데이터가 어긋나는 이유 (0) | 2026.01.02 |
| 트래픽을 고르게 나누는 핵심: Consistent Hashing으로 캐시/샤딩 리밸런싱을 줄이는 원리 (0) | 2026.01.01 |
| 포스트모템이 문화가 되면 장애가 준다: blame-free 리뷰와 재발 방지 액션아이템 작성법 (0) | 2026.01.01 |
| SLA·SLO·SLI를 숫자로 읽는 법: 에러 버짓으로 ‘개발 속도 vs 안정성’ 균형 잡기 (0) | 2026.01.01 |