Java의 동시성 제어

SIHA·2026년 9월 25일

동시성 제어

목록 보기
4/5

앞서 살펴본 낙관적 락과 비관적 락은 DB에서의 동시성 제어 방법이다. Java 애플리케이션에서도 여러 Thread가 하나의 자원을 동시에 접근하면서 동시성 문제가 발생할 수 있는데, Java에서는 synchronized 키워드를 통해 이러한 동시성 문제를 제어할 수 있다.

synchronized란

멀티스레드 환경에서 여러 Thread가 공유하는 자원에 동시에 접근하는 것을 제어하기 위한 Java의 키워드이다.
즉, 여러 Thread가 하나의 공유 자원을 동시에 건드려서 문제가 생기는 것을 막기 위해 사용하는 것이다.

왜 synchronized가 필요한가

프로그램을 실행하면 하나의 작업만 순서대로 처리하는 것이 아니라 여러 Thread가 동시에 동작할 수 있다.
문제는 여러 Thread가 하나의 공유 자원을 함께 사용할 때 발생한다.
특히 여러 Thread가 같은 객체의 상태를 읽고 변경하는 경우, 실행 순서에 따라 결과가 달라질 수 있다.

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized void decrement() {
        counter--;
    }

    public synchronized int value() {
        return counter;
    }
}

위의 코드에서, increment()와 decrement() 함수는 값을 읽고, 증가 또는 감소시키고, 다시 저장하는 여러 과정을 거친다.

그런데 만약 Thread A와 Thread B가 increment()을 동시에 수행한다면,

Thread A: stock 읽기
Thread B: stock 읽기
Thread A: stock 증가
Thread B: stock 증가
Thread A: stock 저장
Thread B: stock 저장

처럼 서로의 실행이 끼어들 수 있다.
이렇게 여러 Thread의 실행이 서로 끼어들어 실행되는 것을 interleaving이라고 한다.

결국 여러 Thread가 공유 자원에 동시에 접근하면서 예상하지 못한 결과가 발생할 수 있고, 이런 문제가 발생할 수 있는 구간을 Critical Section(임계 영역)이라고 한다.

따라서 Critical Section에 여러 Thread가 동시에 접근하지 못하도록 동시성 제어가 필요하다.
Java에서는 이를 위해 synchronized를 사용할 수 있다.

synchronized method

synchronized를 메서드에 붙이면 해당 메서드는 synchronized method가 된다.

public synchronized void decrement() {
    stock--;
}

이 메서드를 여러 Thread가 동시에 호출한다고 해서 여러 Thread가 동시에 메서드 내부를 실행할 수 있는 것은 아니다.

같은 객체의 synchronized 메서드에 접근하려는 Thread 중 하나가 먼저 락을 획득하면 해당 Thread가 메서드를 실행하고, 다른 Thread들은 락을 획득할 때까지 기다린다.

즉,

Thread A → Lock 획득 → 메서드 실행 → Lock 해제
Thread B →            대기
                         ↓
                     Lock 획득 → 메서드 실행

과 같은 식으로 동작한다.

synchronized statement

메서드 전체가 아니라 특정 코드 영역에만 동기화를 적용하고 싶다면 synchronized statement를 사용할 수 있다.

public void decrease() {
    synchronized (this) {
        stock--;
    }
}

여기서 this는 현재 객체를 의미하며, synchronized 뒤에 있는 객체를 기준으로 락을 획득한다.

즉 synchronized는 단순히 "메서드에 락을 건다"라기보다는 특정 객체를 기준으로 동기화를 수행한다고 이해하는 것이 더 정확하다.

Monitor Lock

그렇다면 여기서 한 가지 의문이 생긴다.

Thread가 어떻게 서로 기다리고, 누가 먼저 실행할지를 어떻게 정하는 것일까?

synchronized는 JVM이 관리하는 Intrinsic Lock(내재적 락) 또는 Monitor Lock을 이용한다.

개발자가 직접 Monitor 객체를 만들어 사용하는 것은 아니고, Java 객체를 동기화의 대상으로 사용하면 JVM이 그에 필요한 락 상태를 관리한다.

public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}

위 코드에서는 this 객체를 기준으로 Monitor Lock을 획득한다.

한 Thread가 해당 락을 획득하고 Critical Section을 실행하는 동안 다른 Thread가 같은 락을 획득하려고 하면 기다리게 된다.

그리고 먼저 락을 획득한 Thread가 작업을 끝내고 락을 해제하면 다른 Thread가 락을 획득하여 실행할 수 있다.

결국 우리가 synchronized라는 키워드 하나를 사용하면 JVM이 뒤에서 이런 동기화 과정을 처리해주는 것이다.

synchronized가 보장하는 것

여기서 synchronized가 단순히 "한 명씩 실행시킨다"만 보장하는 것은 아니다.

Mutual Exclusion

가장 먼저 생각할 수 있는 것은 상호 배제(Mutual Exclusion)이다.

같은 Monitor Lock을 사용하는 여러 Thread가 있을 때 한 번에 하나의 Thread만 해당 Critical Section을 실행할 수 있다.

따라서 여러 Thread가 공유 자원의 상태를 동시에 변경하는 것을 막을 수 있다.

Happens-Before Relationship

그리고 synchronized는 Thread 간의 메모리 가시성(Visibility)도 보장한다.

한 Thread가 synchronized 영역에서 공유 상태를 변경하고 락을 해제한 뒤, 다른 Thread가 같은 Monitor의 락을 획득하면 이전 Thread의 변경 사항을 볼 수 있도록 보장된다.

이러한 실행 순서와 메모리 가시성에 대한 관계를 Happens-Before Relationship이라고 한다.

즉 synchronized는 단순히 Thread를 줄 세우는 것뿐만 아니라, Thread 간의 메모리 가시성까지 보장하는 역할을 한다.

synchronized의 범위

여기서 중요한 점이 하나 있다.

synchronized는 JVM 내부에서 동작한다.

따라서 하나의 JVM 안에서 여러 Thread가 같은 객체를 공유하는 상황에서는 사용할 수 있지만, 여러 서버가 각각 별도의 JVM으로 실행되는 환경에서는 서버 간의 동시성을 제어할 수 없다.

Server A                         Server B

JVM A                            JVM B
 ├─ Thread A                     ├─ Thread B
 └─ Thread C                     └─ Thread D
       ↓                               ↓
 synchronized                     synchronized
       ↓                               ↓
     서로 다른 JVM이므로 서로의 Lock을 모름

따라서 멀티 서버 환경에서 동일한 DB 데이터를 여러 서버가 동시에 변경하는 상황이라면 synchronized만으로는 충분하지 않다.

이런 경우 DB Lock이나 Distributed Lock과 같은 다른 동시성 제어 방법을 고려할 수 있다.

다른 동시성 제어 방법과의 비교

여기까지 공부하고 나니 낙관적 락, 비관적 락, synchronized, Distributed Lock이 왜 자꾸 같이 등장하는지도 조금 이해할 수 있었다.

결국 모두 동시성 문제를 해결하기 위한 방법이지만, 어디에서 무엇을 보호하는지가 다르다.

  • Optimistic Lock → DB 데이터의 충돌을 감지하고 처리
  • Pessimistic Lock → DB 데이터를 미리 잠그고 충돌을 방지
  • synchronized → 하나의 JVM 안에서 Thread 간 공유 상태를 제어
  • Distributed Lock → 여러 서버/JVM에서 공유되는 작업을 제어

따라서 어떤 락이 무조건 더 좋은 것이 아니라, 현재 어떤 자원을 공유하고 있고 어떤 환경에서 동작하는지를 보고 적절한 방법을 선택해야 한다.

profile
뭐라도 해보자

0개의 댓글