우당탕탕

Java 멀티스레드에서 synchronized와 ReentrantLock 차이점 직접 비교해봤어요 본문

언어/Java

Java 멀티스레드에서 synchronized와 ReentrantLock 차이점 직접 비교해봤어요

모찌모찝 2026. 5. 13. 18:18

Java 멀티스레드 프로그래밍 하다가 synchronized랑 ReentrantLock 중에 뭘 써야 할지 한참 고민했어요. 저도 처음엔 둘 다 비슷한 동기화 수단이라고 생각했거든요. 그런데 실제로 둘을 쓰면서 겪은 문제 때문에 완전히 생각이 달라졌습니다.

이 글에서 synchronized와 ReentrantLock의 차이점, 쓸 때 주의점, 직접 써보면서 겪은 삽질까지 모두 풀어볼게요. 이거 읽고 나면 Java 멀티스레드 동기화 고민은 싹 해결될 거예요.

개발 환경 / 버전 정보

Java 17, Eclipse IDE, OpenJDK 17 환경에서 테스트했어요. synchronized는 자바 기본 키워드고, ReentrantLockjava.util.concurrent.locks 패키지에서 가져왔습니다.

synchronized와 ReentrantLock 이렇게 다릅니다

사실 이 부분이 가장 헷갈렸는데요, 크게 보면 두 가지 차이가 있어요.

  • 사용법과 유연성: synchronized는 키워드라 문법적으로 간단하지만, 락 획득과 해제가 자동으로 처리돼요. ReentrantLock은 명시적으로 락을 lock()하고 unlock() 해야 합니다.
  • 추가 기능: ReentrantLock은 락 획득 시도 시간 제한, 인터럽트 가능 락, 조건 변수(Condition) 같은 부가 기능이 있는 반면 synchronized는 이런 기능이 없어요.

synchronized 기본 코드

public class SyncExample {
    private int count = 0;

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

    public synchronized int getCount() {
        return count;
    }
}

synchronized 키워드만 붙이면 내가 원하는 메서드, 혹은 코드 블록 전체에 락이 걸려서 다른 스레드는 진입 못 해요. 너무 직관적이고 간단하죠?

ReentrantLock 기본 코드

import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock은 꼭 try-finally로 해제 처리를 해줘야 안전하더라고요. 안 그럼 락이 풀리지 않아 데드락 걸릴 수 있어요.

직접 써보면서 겪었던 삽질과 해결책

저는 처음에 synchronized로 동기화만 하면 끝이라고 생각해서 코드에 이것만 썼는데, 특정 상황에서 락이 제대로 풀리지 않는 문제가 생겼어요. 직접 디버깅해보니, synchronized는 락 획득과 해제가 JVM 내부에서 자동으로 처리되지만, 단순히 메서드가 끝나기 전 예외가 나면 스레드가 블록을 유지하는 경우가 있더라고요.

그래서 ReentrantLock으로 고쳤는데, 한 가지 실수로 unlock()을 빼먹고 쓰다 보니 데드락 상황이 발생했습니다. 이런 부분이 제일 조심해야 하는 점이에요.

public void increment() {
    lock.lock();
    // do something
    // unlock() 호출 안 해서 다른 스레드가 락 못 풀림
}

이걸 방지하려면 항상 try-finally 안에 넣어야 하는 거예요. 안 그러면 서버가 멈춰서 얼마나 민망한지 몰라요.

이건 이렇게 하면 좋아요

추가로 ReentrantLock의 장점 중 하나가 잠금 시도 시간 제한인데요, 이렇게 하면 무한정 대기하지 않고 일정 시간만 기다렸다가 실패할 수 있어서 서버가 멈추는 걸 예방할 수 있어요.

import java.util.concurrent.TimeUnit;

public void incrementWithTimeout() {
    try {
        if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {  // 0.5초 대기
            try {
                // 작업 수행
                count++;
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("락 획득 실패, 작업 건너뜀");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        System.out.println("스레드 인터럽트로 작업 취소");
    }
}

이 부분 덕분에 저희 서비스가 극단적인 부하 상황에서도 멈추지 않고 견디는 걸 직접 확인했어요.

자주 물어보시는 것들

Q. synchronized가 ReentrantLock보다 무조건 느리지 않나요?

A. 꼭 그렇지 않아요. JVM이 synchronized에 대해 여러 최적화를 해오고 있어서 간단한 동기화에는 오히려 synchronized가 더 빠를 때도 있어요. 하지만 복잡한 락 전략이 필요하면 ReentrantLock이 유리합니다.

Q. ReentrantLock을 꼭 unlock 해야 하는 이유가 뭔가요?

A. unlock을 빼먹으면 락이 해제되지 않고 다른 스레드는 계속 대기하게 돼요. 이걸 데드락(deadlock)이라고 하는데 서버 다운 원인 중 하나입니다. 그래서 항상 try-finally에서 해제를 보장해줘야 해요.

Q. synchronized에도 인터럽트 기능이나 시간 제한 같은 게 있나요?

A. 기본적으로 synchronized는 이런 기능 없어요. 그래서 복잡한 상황(시간 제한, 조건 대기 등)이 필요하면 ReentrantLock을 써야 합니다.

직접 코드 써보고 경험하면서 synchronized와 ReentrantLock의 차이가 이해가 확실히 됐어요. 단순히 문법 차이만 보는 게 아니라, 쓰는 용도와 상황에 따라 적절히 선택하는 게 중요하더라고요. 멀티스레드 환경에서 안정적이고 유연한 동기화를 원하면 ReentrantLock을 적극 추천합니다.

Comments