Unit 2.1 — 두 가지 동시성 문제 구분

Psj·2026년 5월 26일

F-lab

목록 보기
159/240

Unit 2.1 — 두 가지 동시성 문제 구분

F-LAB JAVA · 5주차 · Phase 2 · 동시성 안전 도구 3종 비교
🚀 Phase 2 시작 — 가시성과 원자성


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 동시성 문제가 정확히 2가지 인 이유는?
  • 가시성 (Visibility) 문제 의 정의는?
  • 원자성/동시 접근 (Atomicity) 문제 의 정의는?
  • 두 문제를 분리해서 보는 것 이 왜 중요한가?
  • 가시성만 깨지는 경우 는?
  • 원자성만 깨지는 경우 는?
  • 둘 다 깨지는 경우 는?
  • 도구마다 해결 범위가 다른 이유는?
  • 3종 도구 비교의 토대 는?

🎯 핵심 한 문장

동시성 문제는 정확히 가시성 (Visibility) 과 원자성 (Atomicity) 두 가지이며, 가시성은 "한 스레드의 변경이 다른 스레드에 보이지 않는 문제", 원자성은 "동시 수정이 서로를 덮어쓰는 문제" 로, 이 둘을 분리해서 봐야 도구를 정확히 선택할 수 있다.
가시성 문제 는 CPU 캐시 때문에 한 스레드가 변경한 값 (예: runFlag = false) 이 다른 스레드의 캐시에 반영되지 않아 발생하며, 대표 증상은 무한 루프다.
원자성 문제count++ 같은 복합 연산 (읽기-수정-쓰기) 이 중간에 다른 스레드의 개입으로 분리되어 일부 갱신이 손실되는 (lost update) 문제다.
이 둘을 분리해서 보는 것이 핵심인 이유는 도구마다 해결 범위가 다르기 때문이다 — volatile 은 가시성만, synchronized·Atomic 은 가시성과 원자성을 모두 해결한다.
count++ 는 가시성과 원자성을 둘 다 깨는 대표 사례이고, 단순 플래그 (boolean) 는 가시성만 문제가 되는 사례다.

비유 — 공유 화이트보드의 두 문제

동시성 문제 = 공유 화이트보드:

가시성 문제 (안 보임):
  - A 가 보드에 "회의 취소" 적음
  - 하지만 B 는 자기 수첩만 봄 (캐시)
  - B 는 "회의 있음" 으로 착각
  → 변경이 안 보임

원자성 문제 (덮어씀):
  - 보드에 "참석자: 5명"
  - A 와 B 가 동시에 +1 하려 함
  - A: 5 읽음 → 6 적음
  - B: 5 읽음 (A 와 동시) → 6 적음
  - 결과 6 (7 이어야 하는데!)
  → 동시 수정이 덮어씀

분리해서 보기:
  - 둘은 다른 문제
  - 해결책도 다름
    - 가시성: 항상 보드 직접 확인 (volatile)
    - 원자성: 한 명만 수정 (synchronized/Atomic)

→ 동시성 문제 = 가시성 (안 보임) + 원자성 (덮어씀), 분리해야 도구 선택 정확.


🧭 9개 섹션 로드맵

1. 동시성 문제는 2가지
2. 가시성 문제
3. 원자성 문제
4. 분리해서 보기의 중요성
5. 가시성만 깨지는 경우
6. 원자성만 깨지는 경우
7. 둘 다 깨지는 경우 (count++)
8. 도구별 해결 범위
9. 면접 + 자기 점검

1️⃣ 동시성 문제는 2가지

1.1 정확히 2가지

동시성 문제 = 정확히 2가지:

1. 가시성 (Visibility)
   - 변경이 안 보임

2. 원자성 (Atomicity)
   - 동시 수정 덮어씀

이 둘로 분류

1.2 표

문제정의증상
가시성변경이 안 보임무한 루프
원자성동시 수정 덮어씀값 손실

1.3 왜 2가지로

2가지로 보는 이유:

  - 원인 다름
    - 가시성: 캐시
    - 원자성: 연산 분리

  - 해결 다름
    - 도구마다 범위 다름

→ 정확한 진단 + 처방

1.4 ILIC 의 맥락

@Service
public class TwoConcurrencyProblems {
    
    // 가시성 문제 후보 — 플래그
    private boolean running = true;   // 가시성 (캐시)
    
    // 원자성 문제 후보 — 카운터
    private int processedCount = 0;   // 원자성 (count++)
    
    public void worker() {
        while (running) {   // 가시성: running 변경 안 보일 수 있음
            processedCount++;   // 원자성: 동시 증가 손실
        }
    }
    
    public void stop() {
        running = false;   // 다른 스레드에 안 보일 수 있음 (가시성)
    }
    // 두 문제가 별개로 존재
}

1.5 자기 점검 답변

동시성 문제가 정확히 2가지인 이유는?

:
1. 2가지:

  • 가시성
  • 원자성
  1. :

    • 가시성: 안 보임
    • 원자성: 덮어씀
  2. 이유:

    • 원인/해결 다름
  3. 목적:

    • 정확한 진단/처방

2️⃣ 가시성 문제

2.1 가시성 문제

가시성 (Visibility) 문제:

  한 스레드의 변경이
  다른 스레드에 보이지 않음.

원인:
  - CPU 캐시 (코어별)
  - 변경이 캐시에만

2.2 대표 증상

// 가시성 문제 — 무한 루프
private boolean runFlag = true;

// 스레드 1
while (runFlag) {   // 캐시의 true 만 봄
    work();
}
// runFlag = false 변경을 못 볼 수 있음 → 무한 루프

// 스레드 2
runFlag = false;   // 변경 (스레드 1 이 못 봄)

2.3 원인 — 캐시

가시성 원인 — CPU 캐시:

  각 코어 자기 캐시:
    - 스레드 1: 코어 1 캐시 (true)
    - 스레드 2: 코어 2 에서 false 변경
    - 코어 1 캐시 미반영
    - 스레드 1 은 true 계속

2.4 해결

가시성 해결:

  - volatile (메인 메모리 직접)
  - synchronized (동기화)
  - Atomic (내부 volatile)

→ 가시성 보장

2.5 ILIC 의 맥락

@Service
public class VisibilityProblem {
    
    // ❌ 가시성 문제
    private boolean shutdownRequested = false;
    
    public void worker() {
        while (!shutdownRequested) {   // 변경 안 보일 수 있음
            processNext();
        }
    }
    
    public void requestShutdown() {
        shutdownRequested = true;   // worker 가 못 볼 수 있음
    }
    
    // ✓ 해결 — volatile
    private volatile boolean shutdownSafe = false;
    
    public void workerSafe() {
        while (!shutdownSafe) {   // 항상 메인 메모리 (보임)
            processNext();
        }
    }
    
    private void processNext() { }
}

2.6 자기 점검 답변

가시성 문제의 정의는?

:
1. 정의:

  • 변경이 안 보임
  • 다른 스레드
  1. 증상:

    • 무한 루프
  2. 원인:

    • CPU 캐시
  3. 해결:

    • volatile 등

3️⃣ 원자성 문제

3.1 원자성 문제

원자성 (Atomicity) 문제:

  동시 수정이 서로를 덮어씀.
  - 복합 연산 분리
  - 일부 갱신 손실 (lost update)

3.2 대표 증상

// 원자성 문제 — count++ 손실
private int count = 0;

// 여러 스레드가 동시에
count++;   // read → +1 → write (3단계)

// 기대: N번 증가 = N
// 실제: < N (손실)

3.3 원인 — 연산 분리

원자성 원인 — 연산 분리:

  count++ 는 3단계:
    1. count 읽기
    2. +1
    3. count 쓰기

  두 스레드:
    - 둘 다 같은 값 읽음
    - 둘 다 +1
    - 하나 덮어씀 (손실)

3.4 시각화

원자성 문제:

스레드 A    스레드 B    count
읽기(0)                  0
            읽기(0)      0  ← 둘 다 0
+1 (1)
            +1 (1)
쓰기(1)                  1
            쓰기(1)      1  ← A의 1 덮어씀

  2번 증가했는데 1 (손실)

3.5 해결

원자성 해결:

  - synchronized (한 스레드만)
  - Atomic (CAS)
  - Lock

  volatile 은 X (원자성 보장 안 됨)

3.6 ILIC 의 맥락

@Service
public class AtomicityProblem {
    
    // ❌ 원자성 문제
    private int processedCount = 0;
    
    public void process(Shipment shipment) {
        doProcess(shipment);
        processedCount++;   // 동시 증가 손실
    }
    
    // ✓ 해결 — AtomicInteger
    private final AtomicInteger safeCount = new AtomicInteger();
    
    public void processSafe(Shipment shipment) {
        doProcess(shipment);
        safeCount.incrementAndGet();   // 원자적 (손실 X)
    }
    
    private void doProcess(Shipment s) { }
}

3.7 자기 점검 답변

원자성/동시 접근 문제의 정의는?

:
1. 정의:

  • 동시 수정 덮어씀
  • lost update
  1. 증상:

    • count++ 손실
  2. 원인:

    • 연산 분리 (3단계)
  3. 해결:

    • synchronized/Atomic
    • volatile X

4️⃣ 분리해서 보기의 중요성

4.1 왜 분리

분리해서 보는 이유:

  도구마다 해결 범위 다름:
    - volatile: 가시성만
    - synchronized: 둘 다
    - Atomic: 둘 다

  → 문제 진단 → 도구 선택

4.2 잘못된 선택 방지

잘못된 선택 방지:

  분리 안 하면:
    - 원자성 문제에 volatile (틀림)
    - count++ 에 volatile (여전히 손실)

  분리하면:
    - 가시성만 → volatile
    - 원자성 → Atomic/synchronized

4.3 진단 흐름

진단 흐름:

  문제 발생
    ↓
  가시성? (변경 안 보임)
    → volatile / synchronized
    ↓
  원자성? (값 손실)
    → Atomic / synchronized
    ↓
  둘 다? (count++)
    → Atomic / synchronized

4.4 도구 선택 표

문제적합 도구
가시성만volatile
원자성만(드묾) Atomic
둘 다Atomic, synchronized

4.5 ILIC 의 맥락

@Service
public class SeparateAnalysis {
    
    // 가시성만 → volatile
    private volatile boolean running = true;
    public void stop() { running = false; }   // 단일 쓰기
    
    // 원자성 (+가시성) → Atomic
    private final AtomicInteger count = new AtomicInteger();
    public void increment() { count.incrementAndGet(); }   // 복합
    
    // 복잡한 복합 연산 → synchronized
    private int balance = 0;
    private final List<String> history = new ArrayList<>();
    public synchronized void transfer(int amount) {
        balance += amount;            // 여러 변수 일관성
        history.add("transfer: " + amount);
    }
    
    // 진단: 가시성? 원자성? 복합?
    // → 적절한 도구 선택
}

4.6 자기 점검 답변

두 문제를 분리해서 보는 것이 왜 중요한가?

:
1. 이유:

  • 도구 해결 범위 다름
  1. 잘못된 선택 방지:

    • count++ 에 volatile (틀림)
  2. 진단 흐름:

    • 가시성? 원자성?
  3. 선택:

    • 가시성: volatile
    • 원자성: Atomic/synchronized

5️⃣ 가시성만 깨지는 경우

5.1 단순 플래그

가시성만 깨지는 경우:

  단순 boolean 플래그:
    - 한 스레드만 쓰기
    - 여러 스레드 읽기

  단일 쓰기 = 원자성 OK
  but 가시성 X

5.2 예시

// 가시성만 문제 (원자성 OK)
private boolean shutdown = false;

// 스레드 1 (유일한 쓰기)
shutdown = true;   // 단일 쓰기 (원자적)

// 스레드 2~N (읽기만)
while (!shutdown) {   // 가시성 X (안 보일 수 있음)
    work();
}
// 단일 쓰기라 원자성 OK
// 하지만 가시성 X → volatile 필요

5.3 왜 원자성 OK

왜 원자성 OK:

  boolean 단일 쓰기:
    - true 또는 false
    - 중간 상태 없음
    - 한 번에 (원자적)

  복합 연산 아님:
    - count++ 같은 게 아님
    - 단순 대입

5.4 해결 — volatile

// volatile 로 가시성 해결
private volatile boolean shutdown = false;

shutdown = true;   // 단일 쓰기 + 가시성
// 원자성 (단일 쓰기) + 가시성 (volatile)
// → volatile 만으로 충분

5.5 ILIC 의 맥락

@Service
public class VisibilityOnly {
    
    // 가시성만 문제 → volatile 충분
    private volatile boolean active = true;
    private volatile String currentStatus = "IDLE";
    private volatile long lastUpdateTime;
    
    // 모두 단일 쓰기 (원자성 OK) + 가시성 필요
    public void deactivate() {
        active = false;   // 단일 쓰기 (volatile)
    }
    
    public void updateStatus(String status) {
        currentStatus = status;   // 단일 쓰기
        lastUpdateTime = System.currentTimeMillis();   // 단일 쓰기
    }
    
    // 여러 스레드가 읽기 → volatile 로 가시성 보장
}

5.6 자기 점검 답변

가시성만 깨지는 경우는?

:
1. 경우:

  • 단순 플래그
  • 한 쓰기, 여러 읽기
  1. 원자성 OK:

    • 단일 쓰기
    • 중간 상태 없음
  2. 가시성 X:

    • 캐시
    • 안 보임
  3. 해결:

    • volatile 충분

6️⃣ 원자성만 깨지는 경우

6.1 드문 경우

원자성만 깨지는 경우:

  실제로는 드묾:
    - 원자성 깨지면
    - 보통 가시성도 관련

  이론적:
    - synchronized 안에서 가시성 보장
    - 하지만 동기화 부족 시 원자성

6.2 단일 변수 원자성

단일 변수 원자성:

  long/double 쓰기:
    - 32비트 JVM 에서 비원자 (word tearing)
    - 64비트를 32비트씩 쓰기

  volatile long:
    - 원자적 쓰기 보장
    - + 가시성

6.3 word tearing

// long 쓰기 (32비트 JVM 에서 비원자)
private long value;   // 64비트

value = 0x1234567890ABCDEFL;
// 32비트 JVM: 상위 32 + 하위 32 (2번)
// 다른 스레드가 중간에 읽으면 깨진 값

// volatile 로 원자적
private volatile long valueSafe;

6.4 거의 둘 다

실무에서:

  원자성 문제 = 보통 가시성도
  - count++ : 둘 다
  - 복합 연산: 둘 다

  순수 원자성만:
    - long/double word tearing (드묾)

→ 보통 함께 고려

6.5 ILIC 의 맥락

@Service
public class AtomicityOnly {
    
    // long word tearing (32비트 JVM, 드묾)
    private volatile long totalWeight;   // volatile 로 원자적 쓰기
    
    // 대부분: 원자성 = 가시성도 (count++)
    private final AtomicLong count = new AtomicLong();
    
    public void addWeight(long weight) {
        // long 누적 (원자성 + 가시성 필요)
        // AtomicLong 으로 둘 다
        count.addAndGet(weight);
    }
    
    // 순수 원자성만 깨지는 경우는 드묾
    // 보통 가시성과 함께
}

6.6 자기 점검 답변

원자성만 깨지는 경우는?

:
1. 드묾:

  • 보통 가시성도 관련
  1. 단일 변수:

    • long/double word tearing
    • 32비트 JVM
  2. word tearing:

    • 64비트를 32비트씩
  3. 실무:

    • 보통 둘 다 함께

7️⃣ 둘 다 깨지는 경우 (count++)

7.1 count++ 대표 사례

count++ — 둘 다 깨짐:

  가시성:
    - count 변경 안 보일 수 있음

  원자성:
    - read-modify-write 분리
    - 손실

7.2 두 문제 동시

private int count = 0;

count++;
// 1. read count (가시성: 옛 값 읽을 수 있음)
// 2. +1
// 3. write count (가시성: 안 보일 수 있음 + 원자성: 덮어씀)

// 가시성 + 원자성 둘 다

7.3 volatile 로 부족

// volatile 로 가시성 OK, 원자성 X
private volatile int count = 0;

count++;
// 가시성: O (메인 메모리)
// 원자성: X (여전히 3단계 분리)
// → 손실

// → volatile 부족
// → Atomic 또는 synchronized

7.4 올바른 해결

// ✓ AtomicInteger (둘 다)
private final AtomicInteger count = new AtomicInteger();
count.incrementAndGet();   // 가시성 + 원자성

// ✓ synchronized (둘 다)
private int count = 0;
public synchronized void inc() { count++; }

7.5 복합 연산 일반

복합 연산 일반:

  count++ 외에도:
    - count += n
    - count = count * 2
    - if (x == 0) x = 1 (check-then-act)
    - list.add (확인 + 추가)

  모두 둘 다 문제
  → Atomic / synchronized

7.6 ILIC 의 맥락

@Service
public class BothProblems {
    
    // ❌ count++ (둘 다 문제)
    private int wrongCount = 0;
    public void wrong() { wrongCount++; }
    
    // ❌ volatile 도 부족 (원자성 X)
    private volatile int volatileCount = 0;
    public void stillWrong() { volatileCount++; }   // 손실
    
    // ✓ AtomicInteger (둘 다 해결)
    private final AtomicInteger correctCount = new AtomicInteger();
    public void correct() { correctCount.incrementAndGet(); }
    
    // ✓ synchronized (둘 다, 복잡한 경우)
    private int balance = 0;
    public synchronized void transfer(int amount) {
        balance += amount;   // 복합 + 일관성
    }
}

7.7 자기 점검 답변

둘 다 깨지는 경우는?

:
1. count++:

  • 가시성 (변경 안 보임)
  • 원자성 (RMW 분리)
  1. volatile 부족:

    • 가시성 O, 원자성 X
  2. 해결:

    • AtomicInteger
    • synchronized
  3. 복합 연산:

    • +=, *=, check-then-act

8️⃣ 도구별 해결 범위

8.1 3종 비교 (예고)

도구가시성원자성방식
synchronized락 (블로킹)
volatile메모리 동기화
AtomicCAS (논블로킹)

8.2 volatile

volatile:

  ✅ 가시성
  ❌ 원자성

  적합:
    - 단순 플래그
    - 단일 쓰기

8.3 synchronized

synchronized:

  ✅ 가시성
  ✅ 원자성
  - 락 (느림)

  적합:
    - 복잡한 복합 연산
    - 여러 변수

8.4 Atomic

Atomic:

  ✅ 가시성
  ✅ 원자성
  - CAS (논블로킹, 빠름)

  적합:
    - 단일 변수 원자 연산
    - count++

8.5 선택 가이드

선택 가이드:

가시성만 (플래그):
  → volatile

원자성 (단일 변수):
  → Atomic

복잡한 복합 (여러 변수):
  → synchronized

→ 문제 진단 후 선택

8.6 ILIC 의 맥락

@Service
public class ToolScopeGuide {
    
    // 가시성만 → volatile
    private volatile boolean running = true;
    
    // 원자성 (단일) → Atomic
    private final AtomicInteger count = new AtomicInteger();
    
    // 복잡한 복합 → synchronized
    private int balance = 0;
    private final List<String> log = new ArrayList<>();
    
    public void stop() { running = false; }              // volatile
    public void increment() { count.incrementAndGet(); } // Atomic
    
    public synchronized void transfer(int amount) {       // synchronized
        balance += amount;
        log.add("transfer");
    }
    
    // 문제 유형 → 도구
    // (Phase 2 의 나머지 Unit 에서 상세)
}

8.7 자기 점검 답변

도구마다 해결 범위가 다른 이유는?

:
1. 3종:

  • synchronized: 둘 다
  • volatile: 가시성만
  • Atomic: 둘 다
  1. volatile:

    • 가시성만
  2. Atomic:

    • CAS (논블로킹)
  3. 선택:

    • 문제 진단 후

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
동시성 문제 2가지?가시성, 원자성
가시성?변경 안 보임
원자성?동시 수정 덮어씀
분리 이유?도구 범위 다름
가시성만?단순 플래그
원자성만?long word tearing (드묾)
둘 다?count++
volatile 범위?가시성만
synchronized 범위?둘 다
Atomic 범위?둘 다 (CAS)

9.2 자기 점검 체크리스트

2가지 문제

  • 가시성
  • 원자성

가시성

  • 정의
  • 캐시

원자성

  • 정의
  • 연산 분리

분리

  • 중요성

경우

  • 가시성만
  • 원자성만
  • 둘 다

도구 범위

  • 3종 비교

9.3 추가 심화 질문

Q1: 가시성과 원자성을 둘 다 깨는 사례?

답:

  • count++ (대표)
  • 여러 스레드 공유 카운터
  • read-modify-write
  • 가시성 (캐시) + 원자성 (3단계)

Q2: 가시성만 깨지는 사례?

답:

  • volatile 없는 boolean 플래그
  • 단일 쓰기 (원자성 OK)
  • 여러 읽기
  • 캐시로 안 보임

Q3: happens-before 와 가시성?

답:

  • JMM 규칙
  • A happens-before B → A 결과 B 에 보임
  • volatile, synchronized 가 보장
  • 가시성의 이론적 기반

Q4: 메모리 배리어?

답:

  • CPU 명령 재정렬/캐시 제어
  • volatile 이 삽입
  • 가시성 + 순서 보장
  • 하드웨어 수준

Q5: 순서 (ordering) 문제?

답:

  • 가시성/원자성 외 재정렬
  • 컴파일러/CPU 최적화
  • DCL 싱글톤 문제
  • volatile 로 방지

🎯 핵심 요약 — 3줄 정리

1. 두 가지 문제

  • 가시성: 변경이 안 보임 (캐시)
  • 원자성: 동시 수정 덮어씀 (lost update)

2. 분리해서 보기

  • 도구마다 해결 범위 다름
  • 가시성만: volatile, 원자성: Atomic/synchronized

3. 사례

  • 둘 다: count++ (가장 흔함)
  • 가시성만: 단순 플래그
  • → 진단 후 도구 선택

📚 다음으로...

Unit 2.2 — synchronized: 둘 다 해결, 대신 느림

이번 Unit에서 두 문제를 구분했다면, 다음은 synchronized (둘 다 해결).

  • 가시성 + 원자성 보장
  • BLOCKED 비용
  • 가장 안전하지만 가장 비쌈

Phase 2 진행 상황

🚀 Phase 2 — 동시성 안전 도구 3종 비교
  ✅ Unit 2.1 두 가지 동시성 문제 구분 ← 여기
  ⏭ Unit 2.2 synchronized
  ⏭ Unit 2.3 volatile
  ⏭ Unit 2.4 Atomic + CAS (★ 마스터)

5주차 누적 진행

✅ Phase 1 — 스레드 풀 필요성 (3 Unit)
🚀 Phase 2 — 동시성 안전 도구 (1/4 진행)

총: 4/26 Unit
profile
Software Developer

0개의 댓글