본문 바로가기
Language/Java

[JAVA] 가시성 문제의 해결사, volatile 키워드의 완벽 이해와 실무 활용

by Papa Martino V 2026. 1. 21.
728x90

volatile
volatile

 

자바 멀티쓰레드 환경에서 가장 다루기 까다로운 버그 중 하나는 바로 "데이터 불일치"입니다. 분명히 한 쓰레드에서 값을 변경했는데, 다른 쓰레드에서는 변경 전의 값을 계속 읽어 들이는 기이한 현상을 경험해 보셨나요? 이는 CPU 캐시와 메인 메모리 사이의 가시성(Visibility) 문제 때문에 발생합니다. 자바는 이러한 문제를 해결하기 위해 volatile이라는 특별한 키워드를 제공합니다.

본 포스팅에서는 단순히 "가시성을 보장한다"는 정의를 넘어, 하드웨어 아키텍처 관점에서의 동작 원리와 synchronized와의 차이점, 그리고 실무에서 이 키워드를 언제 사용해야 하는지 전문가의 시각으로 심도 있게 파헤쳐 보겠습니다.

1. volatile 키워드의 핵심 정의

자바에서 volatile 키워드는 변수를 '메인 메모리(Main Memory)에 저장하겠다'고 명시하는 것입니다. 일반적인 변수는 성능 향상을 위해 CPU 캐시에 복사되어 사용되지만, volatile이 붙은 변수는 읽기와 쓰기 작업이 모두 메인 메모리에서 직접 수행됩니다.


2. 가시성(Visibility) vs 원자성(Atomicity)

많은 개발자가 혼동하는 부분입니다. volatile가시성은 보장하지만, 원자성은 보장하지 않습니다. 이 차이를 명확히 아는 것이 멀티쓰레드 프로그래밍의 성패를 결정합니다.

비교 항목 volatile synchronized / Atomic Class
가시성 보장 O (항상 최신 값을 읽음) O
원자성 보장 X (복합 연산 시 데이터 유실 가능) O
차단(Blocking) X (Non-blocking으로 성능 우위) O (쓰레드 대기 발생 가능)
사용 권장 상황 하나의 쓰레드가 쓰고, 여러 쓰레드가 읽을 때 여러 쓰레드가 쓰고 읽을 때

3. volatile이 필요한 결정적 이유: CPU 캐시

현대 CPU는 성능을 위해 각 코어마다 전용 L1, L2 캐시를 가집니다. 특정 쓰레드가 공유 변수를 수정하면 해당 코어의 캐시 값만 바뀌고 메인 메모리에는 즉시 반영되지 않을 수 있습니다. 이때 다른 코어에서 실행 중인 쓰레드는 메인 메모리의 옛날 값을 읽어오게 되는데, volatile은 이 통로를 직접 메인 메모리로 고정하여 모든 쓰레드가 항상 일관된 데이터를 보게 만듭니다.


4. 실무 예제 (Sample Example)

아래는 플래그 변수를 이용해 쓰레드를 종료시키는 전형적인 예제입니다. volatile이 없다면 running 변수가 CPU 캐시에 머물러 루프가 영원히 끝나지 않을 수 있습니다.

public class VolatileExample extends Thread {
    // volatile이 없다면 메인 쓰레드에서 값을 바꿔도 이 쓰레드는 모를 수 있음
    private volatile boolean running = true;

    public void run() {
        while (running) {
            // 작업 수행
        }
        System.out.println("쓰레드 안전하게 종료");
    }

    public void stopRunning() {
        running = false;
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileExample t1 = new VolatileExample();
        t1.start();
        
        Thread.sleep(1000);
        t1.stopRunning(); // 상태 변경 시 즉시 반영됨
    }
}

5. volatile 사용 시 주의사항

  • 증가 연산자(++) 주의: count++는 읽기-수정-쓰기의 3단계 작업이므로 volatile만으로는 안전하지 않습니다. 이 경우 AtomicInteger를 사용해야 합니다.
  • 성능 오버헤드: 메인 메모리에 직접 접근하므로 CPU 캐시를 이용하는 것보다 성능이 낮습니다. 꼭 필요한 공유 변수에만 사용하세요.
  • Happens-Before 관계: 자바 메모리 모델(JMM)에 따라 volatile 변수에 대한 쓰기는 이후의 모든 읽기 작업보다 먼저 발생함을 보장합니다.

내용 출처 및 참고 문헌

  • Java Language Specification (JLS) Chapter 17. Threads and Locks
  • Oracle Java Documentation: Volatile Variables
  • Brian Goetz, "Java Concurrency in Practice"
  • Doug Lea's JSR-133 Cookbook for Compiler Writers
728x90