본문 바로가기
Back-end & 알고리즘

자바 동기화 synchronized와 ReentrantLock 언제 어떤 것을 써야 할까

by CodeByJin 2026. 8. 6.
반응형

Java 동기화의 갈림길: synchronized와 ReentrantLock 무엇을 골라야 할까

동시성 제어를 구현할 때 백엔드 엔지니어가 가장 마주하기 쉬운 고민은 언어 내장 키워드인 synchronized와 표준 라이브러리의 명시적 락인 ReentrantLock 사이의 선택이다. 두 도구 모두 임계 영역을 보호하는 목적은 같지만, 락 획득 실패 시의 대기 방식, 타임아웃, 공정성 설정, 그리고 최근 자바 환경에서 화두가 되는 Virtual Thread와의 궁합에서 극명한 차이를 보인다.

 

이 글은 단순히 문법적 차이를 나열하는 대신, 실무 프로젝트에서 어떤 기준으로 두 동기화 방식을 선택해야 하는지, 운영 관점에서 어떤 Trade-off가 발생하는지, 그리고 유지보수와 장애 대응에 미치는 영향을 시니어 개발자의 시선으로 분석한다.

자바 동기화 락
자바 동기화 락

두 방식의 구조적 차이와 운영 관점의 의미

synchronized는 자바 런타임 자체에 최적화가 깊게 반영된 내장 키워드다. 블록을 벗어나는 순간 JVM이 예외 상황까지 고려해 락을 안전하게 해제하므로 개발자가 락 해제를 누락할 위험이 없다. 반면 ReentrantLock은 객체 인스턴스로 존재하며 lock()unlock()을 명시적으로 호출해야 한다.

 

이 차이는 단순한 코딩 스타일의 문제를 넘어선다. ReentrantLock은 try-finally 블록을 누락할 경우 영구적인 데드락이나 스레드 기아 상태를 유발할 수 있는 휴먼 에러 요소를 내포한다. 그럼에도 불구하고 이를 선택하는 이유는 통제력 때문이다.

제어력과 유연성의 Trade-off

ReentrantLock이 제공하는 tryLock(long timeout, TimeUnit unit)이나 lockInterruptibly() 같은 기능은 단순한 편의 기능을 넘어선다. 대규모 트래픽 환경에서 데이터베이스 커넥션 풀이나 외부 API 호출 대기열이 포화 상태일 때, 무한정 락 획득을 대기하다가 전체 스레드 풀이 고갈되는 장애를 막기 위한 필수 방어선이 될 수 있다.

 

반면 synchronized는 이러한 타임아웃 제어가 불가능하다. 대기 스레드가 늘어날수록 시스템 메모리와 스레드 컨텍스트 스위칭 비용이 증가하며, 병목 지점이 어디인지 모니터링하기 까다로워진다. 다만, 임계 영역 내부의 로직이 매우 짧고 단순하다면 synchronized의 JVM 레벨 최적화(Biased Locking, Lightweight Locking 등) 덕분에 불필요한 객체 생성 비용 없이 뛰어난 성능을 보여준다.

반응형

Virtual Thread 환경에서의 치명적인 포인트

JDK 21 이후 Virtual Thread를 도입하는 프로젝트가 늘어나면서 동기화 메커니즘의 선택 기준도 변하고 있다. synchronized 블록 내부에서 I/O 작업이나 긴 블로킹 연산이 발생할 경우, 해당 Virtual Thread를 실행하던 Carrier Thread(OS Thread)가 고정(Pinning)되는 현상이 발생한다.

 

Carrier Thread가 핀ning되면 ForkJoinPool의 가용 스레드가 부족해져 전체 시스템의 처리량이 급감한다. 반면 ReentrantLock은 내부적으로 AQS(AbstractQueuedSynchronizer) 기반으로 구현되어 있어 Virtual Thread가 락을 대기할 때 Carrier Thread를 차단하지 않고 마운트를 해제(Unmount)할 수 있다.

실무 엔지니어 코멘트: 프로젝트에서 Virtual Thread를 전면 도입할 계획이라면, 기존에 작성된 방대한 synchronized 블록 중 I/O나 외부 통신이 포함된 구간이 있는지 전수 조사가 필요하다. 단순히 문법이 편하다는 이유로 synchronized를 고집했다가 대규모 트래픽에서 원인 불명의 스레드 고갈 장애를 겪을 수 있다.

상황별 선택 가이드와 비교

비교 항목 synchronized ReentrantLock
구현 복잡도 낮음 (키워드 선언만 필요) 높음 (try-finally 필수)
타임아웃 제어 지원 안 함 (무한 대기) tryLock으로 지원
공정성 설정 비공정성만 지원 공정/비공정 선택 가능
조건 변수 관리 단일 wait/notify 다중 Condition 객체 분리 가능
Virtual Thread 궁합 블로킹 시 Pinning 유발 가능 효율적인 언마운트 지원

표에서 볼 수 있듯, 비즈니스 로직이 단순한 인메모리 카운터 조작이나 짧은 상태 변경이라면 synchronized가 유지보수 측면에서 압도적으로 유리하다. 불필요한 보일러플레이트 코드를 줄이고 락 해제 누락이라는 치명적인 버그 원인을 원천 차단하기 때문이다.

실무에서 자주 하는 실수와 안티패턴

ReentrantLock을 도입할 때 주니어 개발자들이 가장 많이 범하는 실수는 lock() 호출을 try 블록 내부에서 수행하는 것이다.

private final ReentrantLock lock = new ReentrantLock();

public void badExample() {
    try {
        lock.lock(); // 여기서 예외가 발생하면 unlock()이 호출되어 IllegalMonitorStateException 발생
        doSomething();
    } finally {
        lock.unlock();
    }
}

위와 같이 작성할 경우, lock() 자체에서 메모리 부족이나 기타 원인으로 예외가 발생하면 락을 획득하지도 않았는데 unlock()이 실행되어 정상적인 예외 흐름이 덮어씌워진다. 올바른 패턴은 다음과 같이 lock()try 바깥 상단에 배치하는 것이다.

private final ReentrantLock lock = new ReentrantLock();

public void goodExample() {
    lock.lock();
    try {
        doSomething();
    } finally {
        lock.unlock();
    }
}

사소해 보이는 차이지만, 운영 서버에서 예외 추적을 방해하는 원인이 되며 코드 리뷰 시 반드시 검수해야 하는 포인트다.

공정 락(Fairness)에 대한 오해와 비용

ReentrantLock을 생성할 때 new ReentrantLock(true)를 통해 공정 락을 설정할 수 있다. 먼저 대기한 스레드가 락을 먼저 획득하도록 보장하므로 특정 스레드가 무한정 대기하는 기아 상태를 방지할 수 있어 매력적으로 보인다.

 

하지만 공정 락은 스레드를 대기 큐에 순서대로 줄 세우고 관리하는 과정에서 컨텍스트 스위칭 비용이 증가한다. 결과적으로 시스템 전체의 Throughput(처리량)이 심각하게 떨어지는 Trade-off가 발생한다. 실무에서 기아 상태가 비즈니스 관점에서 치명적인 문제가 되는 경우가 아니라면, 기본값인 비공정 락을 사용하는 것이 성능 측면에서 훨씬 유리하다.

핵심 요약

  • 단순하고 짧은 임계 영역 보호는 가독성과 안전성이 검증된 synchronized로 시작하는 것이 정답이다.
  • 타임아웃 제어, 인터럽트 반응, 다중 조건 변수 분리가 필요할 때만 ReentrantLock을 도입한다.
  • Virtual Thread를 적극적으로 활용하는 아키텍처라면 핀ning 현상을 방지하기 위해 락 메커니즘 전수 검토가 필수적이다.
  • 공정 락은 기아 상태 해결의 이점이 있지만 처리량 저하라는 비용을 수반하므로 신중하게 적용해야 한다.

실무 체크리스트

  • 추천하는 경우: 외부 시스템 연동 대기열 관리, 복잡한 생산자-소비자 패턴, 타임아웃 기반의 락 제어가 필요한 경우
  • 추천하지 않는 경우: 단순 메모리 변수 동기화, 짧은 메서드 단위 보호에 불필요하게 ReentrantLock을 도입하는 경우
  • 운영 전 확인 사항: try-finally 패턴의 올바른 적용 여부, Virtual Thread 환경에서의 Pinning 발생 가능성
  • 유지보수 관점: 팀원들이 명시적 락 해제 패턴에 익숙한지, 락 대기 시간 모니터링 체계가 갖추어져 있는지 확인

Q. 성능만 보면 synchronized가 항상 더 빠른가요?

최근 JVM 버전에서는 synchronized의 성능이 ReentrantLock과 거의 차이가 없거나 짧은 구간에서는 오히려 더 우수한 경우도 많다. 성능 차이보다는 제어 기능의 필요성에 따라 선택해야 한다.

Q. 분산 환경에서도 ReentrantLock이 유효한가요?

아니다. synchronized와 ReentrantLock은 모두 단일 JVM 내부의 스레드 동기화만 보장한다. 다중화된 서버 인스턴스 환경에서는 Redis나 ZooKeeper 등을 활용한 분산 락을 구축해야 한다.

Q. 기존 synchronized 코드를 모두 ReentrantLock으로 마이그레이션해야 할까요?

문제가 없는 안정적인 코드라면 굳이 바꿀 필요가 없다. 마이그레이션은 타임아웃 요구사항이 생기거나 Virtual Thread 도입으로 인한 Pinning 이슈가 실제로 측정될 때 검토하는 것이 올바른 순서다.

실무에서는 결국 아키텍처의 요구 수준과 유지보수 비용을 저울질해 도구를 골라야 한다. 굳이 복잡한 제어가 필요 없다면 단순함을 지키는 것이 가장 현명한 설계다.

 

 

파일 업로드, 서버에 그냥 저장하면 안 되는 이유: 실무자가 반드시 챙겨야 할 5가지

파일 업로드 기능은 백엔드 개발에서 가장 기초적이면서도, 운영 환경에서는 가장 골치 아픈 '블랙홀'과 같습니다. 단순한 폼 전송부터 대용량 파일 스트리밍까지 구현 방법은 다양하지만, 무작

byteandbit.tistory.com

 

반응형