읽기/쓰기 잠금을 활용한 효율적인 캐시 구현 방법

1. 이미 존재하는 동기화 기법(모니터)으로 충분하지 않나요? 왜 읽기/쓰기 잠금이 필요한가요?

모든 동시성 문제를 해결할 수 있는 것은 아닙니다. 특히 읽기 작업이 많고 쓰기 작업이 적은 시나리오에서는 읽기/쓰기 잠금이 성능 향상에 큰 도움이 됩니다. 이 경우, 여러 스레드가 동시에 읽기를 수행할 수 있어 처리량이 크게 증가합니다.

2. 읽기/쓰기 잠금은 자바 전용인가요? 핵심 원칙은 무엇인가요?

읽기/쓰기 잠금은 자바에 국한된 개념이 아닙니다. 일반적인 동시성 제어 기술입니다. 주요 원칙은 다음과 같습니다:

  • 동시에 여러 스레드가 읽기 작업을 수행할 수 있습니다.
  • 단일 시간점에서 오직 하나의 스레드만 쓰기 작업이 가능합니다.
  • 쓰기 중인 동안 다른 스레드는 읽기 조차 불가능합니다 (쓰기 중에는 읽기가 차단됨).

3. 자바에서 읽기/쓰기 잠금은 어떻게 구현되며, 사용 방법은?

ReadWriteLock 인터페이스는 추상적인 정의이며, 그 구현체로는 ReentrantReadWriteLock가 있습니다. 사용 시 각각 readLock()writeLock() 메서드를 호출하여 잠금을 획득합니다.

4. 캐시 구현에 적합하다고 했으니, 간단한 캐시 클래스를 어떻게 만들 수 있나요?

키와 값으로 구성된 기본 캐시 클래스를 정의하고, get()에서는 읽기 잠금, put()에서는 쓰기 잠금을 사용합니다.

class Cache<K, V> {
    private final Map<K, V> cacheMap = new HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    V get(K key) {
        readLock.lock();
        try {
            return cacheMap.get(key);
        } finally {
            readLock.unlock();
        }
    }

    V put(K key, V value) {
        writeLock.lock();
        try {
            return cacheMap.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
}

5. 초기 데이터 로딩은 어떻게 처리해야 하나요?

데이터가 적은 경우: 애플리케이션 시작 시 일괄적으로 캐시에 로드하면 간단하고 빠릅니다.
데이터가 많은 경우: 요청이 들어올 때마다 필요한 데이터를 지연 로딩(Lazy Loading) 방식으로 캐시에 추가합니다.

6. 지연 로딩 방식의 코드 구현은 어떻게 되나요?

읽기 시 캐시에 없으면 쓰기 잠금을 획득해 데이터를 조회하고 캐시에 저장합니다. 이 과정에서 다중 스레드 간 경쟁 상태를 방지하기 위해 두 번째 검사가 필요합니다.

V get(K key) {
    V value = null;

    // ① 읽기 잠금 획득
    readLock.lock();
    try {
        value = cacheMap.get(key); // ② 캐시 조회
    } finally {
        readLock.unlock(); // ③ 잠금 해제
    }

    // ④ 캐시에 존재하면 바로 반환
    if (value != null) return value;

    // ⑤ 쓰기 잠금 획득 (데이터 생성)
    writeLock.lock();
    try {
        // ⑥ 재확인: 다른 스레드가 이미 데이터를 로드했을 수 있음
        value = cacheMap.get(key);
        if (value == null) {
            // ⑦ 데이터베이스 조회 (예시)
            value = fetchFromDatabase(key);
            cacheMap.put(key, value);
        }
    } finally {
        writeLock.unlock();
    }

    return value;
}

7. 왜 두 번째 확인이 필요한가요?

여러 스레드가 동시에 같은 키에 대한 get() 요청을 보낼 수 있습니다. 첫 번째 읽기 잠금 후 캐시에 없지만, 다른 스레드가 이미 데이터를 조회해서 캐시에 넣었을 가능성이 있기 때문에, 쓰기 잠금 내에서 다시 확인하는 것이 중요합니다. 이를 통해 중복 조회 및 저장을 방지합니다.

참고: Java의 ReentrantReadWriteLock잠금 업그레이드(읽기 → 쓰기)를 지원하지 않습니다. 이는 deadlock을 유발할 수 있기 때문입니다. 반대로 내림차순(쓰기 → 읽기)은 허용됩니다.

태그: java ReadWriteLock 캐시 동시성 제어 동시 접근

6월 15일 19:32에 게시됨