본문 바로가기
Java

자바 스레드 (4) - 동기화와 잠금

by gentle-tiger 2025. 6. 10.

프로젝트 중 느낀 자바 기초의 부족함을 보완하고자 핵심 주제를 순차적으로 복습할 예정입니다.
자바 코어 → 자바 스레드 → 자바 OOP → 자바의 예외 → 자바의 컬렉션 → 최대 절전 모드 순으로 정리할 예정입니다. 

 

 

동기화와 잠금

  1. Java에서 잠금 또는 잠금 목적은 무엇입니까?
  2. 얼마나 많은 방법으로 Java에서 동기화를 수행할 수 있습니까?                                            .
  3. 동기화 방법이란 무엇입니까?
  4. Java에서 동기화된 메서드는 언제 사용합니까? 
  5. Java에서 동기화된 블록이란 무엇입니까?
  6. 동기화 블록은 언제 사용하며 동기화 블록을 사용하면 어떤 이점이 있습니까?
  7. 클래스 레벨 잠금이란 무엇입니까?
  8. Java에서 정적 메소드를 동기화할 수 있습니까?
  9. 프리미티브에 동기화된 블록을 사용할 수 있습니까?

 

 

Java에서 잠금 또는 잠금의 목적은 무엇입니까?

자바에서 잠금(Lock) 은 여러 스레드가 공유 자원에 동시에 접근할 때 데이터의 일관성과 무결성을 보장하기 위한 제어 수단입니다. 멀티스레드 환경에서는 두 개 이상의 스레드가 같은 변수나 객체에 접근할 수 있는데, 이 과정에서 동시 수정이 발생하면 경쟁 조건(Race Condition), 데이터 손상(Data Corruption) 등의 문제가 발생할 수 있습니다. 이를 방지하기 위해 자바는 동기화(Synchronization) 기능을 통해 잠금 메커니즘을 제공합니다.

가장 기본적인 잠금 메커니즘은 JVM 수준에서 제공되는 모니터 락(Monitor Lock) 으로, synchronized 키워드를 통해 객체 단위의 배타적 잠금을 구현합니다. 더 정교한 제어를 원할 경우 java.util.concurrent.locks 패키지의 Lock, ReentrantLock, ReentrantReadWriteLock 등을 사용하여 명시적인 잠금 및 재진입, 읽기/쓰기 분리, 공정성 제어 등의 기능을 사용할 수 있습니다.

하지만 Java 애플리케이션에서는 JVM 내부 동기화 외에도, 외부 리소스에 대한 동시성 제어가 필요합니다. 대표적으로 데이터베이스에 대한 잠금은 JPA의 @Lock(PESSIMISTIC_WRITE), @Version 등의 어노테이션을 통해 비관적/낙관적 락을 구현하며, 이는 DB 트랜잭션 수준의 S/X 락으로 이어집니다. 또한, 멀티 인스턴스 또는 클러스터 환경에서는 Redis, Zookeeper 등 외부 시스템을 활용한 분산 락이 필요합니다. 이들은 JVM 수준의 락과 달리 애플리케이션 간 경합을 제어합니다.

즉, 잠금은 동시성 환경에서 자원의 일관성과 안정성을 확보하기 위한 핵심 기법으로, 상황에 따라 적절한 수준과 방식의 잠금 전략을 선택해 적용하는 것이 중요합니다.

 

 

Java에서 동기화를 수행하는 방법은 몇 가지가 있습니까?

자바에서 동기화는 크게 두 가지 방식으로 수행할 수 있습니다. 첫 번째는 synchronized 키워드를 사용하는 방법이고, 두 번째는 java.util.concurrent.locks 패키지에서 제공하는 명시적 락 API(Lock 인터페이스 기반) 를 사용하는 방법입니다. 이 외에도 AtomicInteger, ConcurrentHashMap 등 락-프리 동기화(Non-blocking Synchronization) 를 지원하는 클래스도 존재합니다.

synchronized는 간단하고 직관적이지만, 재진입, 공정성, 인터럽트 제어 등에서 유연성이 떨어지는 반면, ReentrantLock과 같은 명시적 락은 공정성 설정, 명시적 잠금 해제, tryLock 사용 등 더 정교한 제어가 가능합니다. 락 없이 원자성을 보장하는 AtomicXXX 시리즈는 CAS(Compare-And-Swap) 기반으로 성능이 우수하나 적용 범위가 제한적입니다.

 

 

동기화 방법이란 무엇입니까?

동기화 방법(Synchronized Method)은 synchronized 키워드를 메서드 선언부에 붙여 해당 메서드 전체에 대해 락을 거는 방식입니다. 인스턴스 메서드에 적용할 경우 해당 객체에 대한 인스턴스 모니터 락, 정적 메서드에 적용할 경우 클래스 객체(Class object)에 대한 락이 걸리게 됩니다. 한 번에 하나의 스레드만 해당 메서드를 실행할 수 있도록 제한합니다.

이 방식은 구현이 간단하다는 장점이 있지만, 메서드 전체에 락이 걸리므로 불필요하게 긴 임계 구역(Critical Section)이 생길 수 있다는 단점이 있습니다. 결과적으로 성능 저하가 발생할 수 있으므로, 실제로는 동기화 블록을 사용해 임계 구역을 최소화하는 것이 더 효율적인 경우가 많습니다.

// 전체 메서드에 synchronized -> 임계 구역이 불필요하게 큼
public synchronized void increment() {
	...
}
public void increment() {
    // 필요한 부분만 동기화
    synchronized (lock) {
        count++;  // 임계 구역
    }
}

 

 

Java에서 동기화된 메서드는 언제 사용합니까?

동기화된 메서드는 인스턴스 단위의 공유 자원에 대한 일괄적 보호가 필요할 때 사용합니다. 예를 들어, 멀티스레드 환경에서 클래스의 상태를 변경하는 모든 작업을 원자적으로 처리해야 한다면, 해당 작업을 synchronized 메서드로 구현하는 것이 간단하고 안정적입니다.

다만, 메서드 전체에 락이 걸리기 때문에 성능에 민감한 경우에는 적절하지 않을 수 있습니다. 이 경우에는 정확히 필요한 범위에만 락을 걸 수 있도록 동기화 블록(synchronized block)을 사용하는 것이 더 적합합니다. 동기화 메서드는 단순한 동기화 요구에는 적합하지만, 락 경쟁이 많은 환경에서는 병목이 발생할 가능성이 있습니다.

 

 

Java에서 동기화된 블록이란 무엇입니까?

동기화된 블록(Synchronized Block)은 코드의 특정 부분에만 락을 적용하여 동기화 처리를 하는 방식입니다. synchronized(obj) 형태로 사용되며, obj는 모니터 락을 획득할 대상 객체입니다. 이렇게 하면 임계 구역(Critical Section) 을 명확하게 제어할 수 있고, 메서드 전체를 잠그는 것보다 더 세밀한 동기화 제어가 가능합니다.

예를 들어, 여러 스레드가 공유하는 리스트에 데이터를 추가할 때만 동기화를 적용하고, 읽기에는 동기화를 하지 않도록 할 수 있습니다. 이런 방식은 성능 최적화와 병행성 제어의 균형을 맞출 수 있는 실용적인 방법입니다. 필요 이상으로 넓은 범위에 락을 걸면 스레드 경쟁이 증가하고 처리량이 감소하므로, 반드시 동기화 범위를 최소화하는 것이 좋습니다.

 

읽기 쓰리를 분리하여 제어

public void add(String item) {
    synchronized(lock) {
        list.add(item);  // 쓰기만 동기화
    }
}

public String get(int index) {
    return list.get(index);  // 동기화 없음
}

 

 

동기화 블록은 언제 사용하며 어떤 이점이 있습니까?

동기화 블록은 임계 구역이 메서드의 일부분에 불과한 경우, 즉 전체 메서드를 동기화할 필요가 없을 때 사용합니다. 예를 들어, 특정 조건이 만족될 때만 공유 자원을 수정하는 코드가 있다면, 해당 부분만 synchronized 블록으로 감싸는 것이 적절합니다. 이를 통해 락 경합(Lock Contention) 을 줄이고 전체적인 처리량(Throughput) 을 향상시킬 수 있습니다.

이점으로는 첫째, 동기화 범위 축소로 인한 성능 향상, 둘째, 락 객체 선택의 유연성, 셋째, 가독성과 의도의 명확화 등이 있습니다. 특히 synchronized(this) 대신 별도의 락 객체를 정의하여 제어할 수 있으므로, 의도하지 않은 락 충돌을 방지하는 데에도 유리합니다.

 

 

클래스 레벨 잠금이란 무엇입니까?

클래스 레벨 잠금은 클래스의 정적 자원(static resource)에 대한 동기화 제어를 의미합니다. 이는 정적 메서드에 synchronized 키워드를 붙이거나, synchronized(ClassName.class) 형태로 사용됩니다. 해당 락은 클래스 객체(Class object)에 걸리며, JVM은 클래스당 하나의 클래스 객체를 생성하므로 해당 클래스의 모든 스레드 간 공유됩니다.

이 방식은 전역 상태나 공용 캐시 등 클래스 단위의 동기화가 필요한 경우에 적합합니다. 다만, 클래스 레벨 락은 스코프가 넓기 때문에 동시성 제한이 커질 수 있고, 병목 현상이 발생할 가능성도 높아 반드시 필요한 경우에만 사용해야 합니다.

 

 

Java에서 정적 메서드를 동기화할 수 있습니까?

예, 자바에서는 정적 메서드에도 synchronized 키워드를 사용할 수 있습니다. 이 경우, 락은 인스턴스 객체가 아닌 해당 클래스의 클래스 객체(Class 객체) 에 대해 획득됩니다. 즉, 하나의 JVM 내에서 해당 클래스에 속한 모든 정적 메서드는 동일한 클래스 락을 공유하게 됩니다.

정적 메서드의 동기화는 클래스 수준에서의 자원 공유 제어가 필요한 경우에 유용하지만, 모든 정적 메서드가 동시에 실행되지 못할 수 있으므로 주의가 필요합니다. 클래스 수준의 락은 스레드 경합이 심할 경우 전체 시스템 성능에 영향을 줄 수 있으므로 가능한 한 락 범위를 좁히는 구조를 고려하는 것이 좋습니다.

 

 

프리미티브에 동기화된 블록을 사용할 수 있습니까?

자바에서 synchronized 키워드의 대상은 Object 타입만 가능하며, 프리미티브 타입(boolean, int, long 등)에는 직접 사용할 수 없습니다. 프리미티브 타입은 객체가 아니기 때문에 모니터 락을 설정하거나 획득할 수 없습니다. 따라서 동기화가 필요한 경우에는 프리미티브를 래핑한 객체(Integer, Long 등) 나 별도의 락 객체(Object 인스턴스 등) 를 사용해야 합니다.

예를 들어, 다음과 같이 해야 합니다.

Object lock = new Object(); int counter = 0; synchronized (lock) { counter++; }

만약 동시성 처리를 원한다면, AtomicInteger, AtomicLong 등 원자 연산을 지원하는 클래스를 사용하는 것도 좋은 대안입니다. 이는 락 없이도 스레드 안전하게 동작하며 성능 측면에서도 유리합니다.