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을 유발할 수 있기 때문입니다. 반대로 내림차순(쓰기 → 읽기)은 허용됩니다.