동시성 버그는 코드가 틀려서 생기는 게 아니라,
특정 타이밍이 ‘우연히’ 겹칠 때만 모습을 드러난다.
1️⃣ 왜 “재현” 문제가 아닌가
일반 버그
- 입력이 같으면
- 결과도 같음
- 재현 가능
- 디버거로 잡힘
동시성 버그
- 입력 같아도
- 스레드 스케줄링 순서가 매번 다름
- 결과가 달라짐
- 같은 코드, 같은 데이터인데 결과가 랜덤
👉 이게 핵심입니다.
2️⃣ 동시성 버그의 진짜 원인
JVM/OS 관점
- 스레드 실행 순서 = OS 스케줄러 결정
- CPU 코어 상태
- 컨텍스트 스위칭 타이밍
- JIT 최적화
- 캐시 일관성(Memory Model)
👉 개발자가 통제 불가
3️⃣ 그래서 “운”이라는 말이 나온다
동시성 버그는 보통 이렇게 터집니다:
Thread A: if (x == null) { Thread B: x = new Object(); Thread A: x.doSomething(); // NPE
- 이 순서가 딱 이 타이밍에만 발생
- 로그 찍으면 사라짐
- 디버거 걸면 안 터짐
- 서버 늘리면 더 잘 터짐
👉 타이밍 복권
4️⃣ 왜 로그/디버거로 잡기 어려운가
로그 찍으면?
- I/O 발생
- 스레드 타이밍 변경
- 버그 사라짐 (Heisenbug)
디버거 걸면?
- 스레드 멈춤
- 실행 순서 강제
- 재현 실패
👉 관측 자체가 상태를 바꿈
5️⃣ 실무에서 자주 터지는 동시성 버그 유형
① 체크 후 실행 (Check-Then-Act)
if (!map.containsKey(k)) { map.put(k, v); }
② 공유 객체에 대한 무보호 접근
- static 필드
- 싱글톤 내부 상태
- 캐시 Map
③ 잘못된 Double-Checked Locking
if (instance == null) { synchronized(...) { if (instance == null) { instance = new Instance(); } } }
(volatile 없으면 깨짐)
6️⃣ 그래서 결론이 이렇게 된다
동시성 버그는 “고치기”보다
“처음부터 안 만들기”의 문제다
- 재현해서 잡는 건 거의 불가능
- 테스트로도 100% 못 잡음
- 운 좋으면 안 터질 뿐
7️⃣ 실무자가 취하는 올바른 태도
❌ 잘못된 접근
- “테스트에서 안 터졌으니 괜찮다”
- “운영에서 한 번도 안 터졌다”
- “락 걸면 되겠지”
✅ 올바른 접근
- 공유 상태 최소화
- 불변 객체 사용
- 명시적 동시성 도구 사용
- ConcurrentHashMap
- Atomic*
- synchronized/Lock
- 설계 단계에서 차단
8️⃣ 이 문장의 진짜 의미 (핵심)
동시성 버그는 실력이 아니라 확률의 문제다.
실력 있는 개발자는 “확률이 0이 되게 설계”한다.
9️⃣ 실무 한 줄 요약
- 재현 안 된다고 안전한 게 아님
- 재현되는 동시성 버그는 이미 하급
- 진짜 위험한 건 “아직 안 터진 것”
LIST
'Spring & Backend' 카테고리의 다른 글
| AOP는 문제 해결이 아니라 문제 은닉 도구가 될 수 있다 (0) | 2026.02.02 |
|---|---|
| 좋은 자바 코드는 JVM을 신뢰하되, 감시한다 (0) | 2026.02.02 |
| 추상화는 힘이다. 하지만 과하면 미래의 디버깅 비용이다 (0) | 2026.02.02 |
| JVM 이해 (0) | 2026.02.02 |
| GC를 모르면 성능을 논하지 말고, GC를 알면 성능을 과신하지 마라 (0) | 2026.02.02 |
