본문 바로가기
Language/Java

[JAVA] 쓰레드 간의 효율적인 대화 : wait(), notify(), notifyAll() 완벽 가이드

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

wait(), notify(), notifyAll()
wait(), notify(), notifyAll()

 

자바 멀티쓰레딩 프로그래밍에서 여러 쓰레드가 협력하여 하나의 목적을 달성해야 할 때, 우리는 단순히 '동기화(Synchronization)'를 넘어 쓰레드 간의 '통신(Communication)'이 필요하게 됩니다. 자바의 Object 클래스는 이를 위해 wait(), notify(), notifyAll()이라는 세 가지 핵심 메서드를 제공합니다. 이 메서드들은 쓰레드가 자원을 낭비하며 무한 루프를 도는 'Busy Waiting' 상태를 방지하고, 시스템 리소스를 극도로 효율적으로 사용할 수 있게 돕습니다. 본 글에서는 이 메서드들의 정확한 용도와 동작 메커니즘, 그리고 실무에서 주의해야 할 핵심 포인트를 상세히 설명하겠습니다.

1. 메서드별 역할 및 기능 정의

이 메서드들은 Thread 클래스가 아닌 Object 클래스에 정의되어 있습니다. 이는 자바의 모든 객체가 고유의 모니터(Monitor)를 가지고 있으며, 쓰레드 간의 통신이 이 모니터 락을 기반으로 이루어지기 때문입니다.

메서드 주요 용도 쓰레드 상태 변화
wait() 락을 반납하고, 다른 쓰레드가 통지할 때까지 대기한다. RUNNABLE → WAITING
notify() 대기 중인 쓰레드 중 임의의 하나를 깨운다. WAITING → BLOCKED (락 획득 대기)
notifyAll() 대기 중인 모든 쓰레드를 깨운다. (권장되는 방식) WAITING → BLOCKED (락 획득 대기)

2. 반드시 지켜야 할 사용 규칙

이 메서드들을 사용할 때는 JVM이 강제하는 두 가지 중요한 규칙이 있습니다.

  • synchronized 블록 내에서만 호출: 호출하는 쓰레드는 반드시 해당 객체의 모니터 락을 소유하고 있어야 합니다. 그렇지 않으면 IllegalMonitorStateException이 발생합니다.
  • 루프(while) 내에서 wait 사용: 쓰레드가 깨어났을 때 조건이 여전히 유효한지 다시 확인해야 합니다. 이를 'Spurious Wakeup(허위 깨어남)' 방지라고 합니다.

3. 실무 예제: 생산자-소비자 패턴 (Sample Example)

공유 자원인 큐(Queue)를 두고 데이터를 생성하는 쓰레드와 소비하는 쓰레드가 어떻게 협력하는지 보여주는 예제입니다.

class DataQueue {
    private String data;
    private boolean empty = true;

    // 소비자용 메서드
    public synchronized String consume() {
        while (empty) { // 데이터가 없으면 대기
            try { wait(); } catch (InterruptedException e) {}
        }
        empty = true;
        notifyAll(); // 생산자에게 데이터 비었음을 알림
        return data;
    }

    // 생산자용 메서드
    public synchronized void produce(String newData) {
        while (!empty) { // 데이터가 아직 있으면 대기
            try { wait(); } catch (InterruptedException e) {}
        }
        empty = false;
        this.data = newData;
        notifyAll(); // 소비자에게 데이터 준비됨을 알림
    }
}

4. notify() 보다는 notifyAll()을 사용해야 하는 이유

notify()는 대기 중인 쓰레드 중 하나만 무작위로 깨웁니다. 만약 깨어난 쓰레드가 자신의 조건이 맞지 않아 다시 wait()에 빠지게 되면, 시스템 전체가 아무도 깨워주지 않는 데드락 상태와 유사한 '신호 상실' 현상을 겪을 수 있습니다. 반면 notifyAll()은 모든 쓰레드를 깨워 각자가 조건을 확인하게 하므로 훨씬 안전하고 논리적으로 견고합니다.


5. 현대적인 대안: Condition 객체

JDK 5부터는 java.util.concurrent.locks.Condition 인터페이스를 통해 wait/notify보다 더 세밀한 제어가 가능해졌습니다. 하나의 락에 대해 여러 개의 대기 집합(Wait Set)을 가질 수 있어, 생산자만 깨우거나 소비자만 깨우는 등의 작업이 가능합니다. 대규모 프로젝트에서는 이 방식을 더 선호합니다.


내용 출처 및 참고 자료

  • Oracle Java Docs: Object Class - wait(), notify()
  • Joshua Bloch, "Effective Java 3rd Edition" - Item 81
  • Java Language Specification Chapter 17. Threads and Locks
728x90