간단한 락 정리 표
| 잠금 방식 | 특징 | 적합한 환경 |
| Synchronized / ReentrantLock | Java 코드 레벨에서 스레드를 제어 | 단일 서버, 단일 JVM |
| 낙관적 락 (Optimistic Lock) | DB에 버전(Version) 컬럼을 두어 데이터 정합성 검증 | 단일 DB, 데이터 충돌이 드물 때 |
| 비관적 락 (Pessimistic Lock) | DB 트랜잭션 동안 Row에 배타적 락(Lock) 설정 | 단일 DB, 데이터 충돌이 빈번할 때 |
| 네임드 락 (Named Lock) | DB 연결 기반으로 특정 이름의 락을 획득 | 단일 DB, MySQL 환경 |
| 분산 락 (Distributed Lock) | Redis 등 외부 시스템을 통해 락을 관리 | 다중 서버, 분산 환경 |
### JVM 잠금 (Synchronized / ReentrantLock)
- 방법: Java 언어 자체에서 제공하는 기능으로, 하나의 서버 프로세스(JVM) 내에서 코드 블록이나 메서드에 대한 동시 접근을 제어합니다. synchronized 키워드를 사용하거나, 더 세밀한 제어가 가능한 java.util.concurrent.locks.ReentrantLock 객체를 사용해 구현합니다. ReentrantLock 활용 시 여러 메서드나 서비스 간에 락을 명시적으로 공유할 수 있습니다.
- 장점: 외부 시스템이나 라이브러리 없이 구현이 매우 간단하고 빠릅니다. 단일 서버 환경에서 간단한 동시성 문제를 해결하기에 적합합니다.
- 단점: 서버가 두 대 이상으로 늘어나는 분산 환경에서는 전혀 동작하지 않습니다. 서버 A에서 획득한 락은 서버 B에 아무런 영향을 주지 못해, 결국 여러 서버에서 동시에 정원을 초과하여 등록되는 문제가 다시 발생합니다.
### 데이터베이스를 이용한 잠금 (비관적 락, Pessimistic Lock)
- 방법: 한 스레드가 수강 인원을 확인하고 등록하는 동안, 다른 스레드가 해당 데이터에 접근하지 못하도록 데이터베이스의 Row에 배타적 잠금(Exclusive Lock)을 거는 방식입니다. JPA에서는 @Lock(LockModeType.PESSIMISTIC_WRITE) 어노테이션으로 쉽게 구현할 수 있습니다.
- 장점: 구현이 비교적 간단하고, 데이터 정합성을 확실하게 보장합니다.
- 단점: 잠금을 얻지 못한 나머지 스레드들은 앞선 스레드의 트랜잭션이 끝날 때까지 계속 대기해야 합니다. 동시 요청이 매우 많은 상황에서는 이 대기 시간 때문에 DB 커넥션 풀이 빠르게 고갈되고 전체 시스템의 성능이 급격히 저하될 수 있습니다.
### 데이터베이스를 이용한 잠금 (낙관적 락, Optimistic Lock)
- 방법: 실제로 데이터를 잠그지 않고, "데이터 쓰기 작업에 의한 충돌이 거의 없을 것"이라고 낙관적으로 가정하는 방식입니다. 테이블에 version 컬럼을 추가하고, 데이터를 수정할 때 처음에 읽었던 version과 현재 DB의 version이 일치하는지 확인합니다. 만약 version이 다르다면 다른 스레드가 먼저 수정한 것이므로, 현재 트랜잭션을 실패시키고 재시도를 유도합니다. JPA에서는 @Version 어노테이션으로 구현합니다.
- 장점: 잠금을 사용하지 않아 대기가 없으므로, 충돌이 거의 발생하지 않는 환경에서는 비관적 락보다 훨씬 높은 성능을 보여줍니다.
- 단점: 수강 신청처럼 충돌이 매우 빈번하게 발생하는 시나리오에는 최악의 선택입니다. 1000개의 동시 요청이 발생하면 1개를 제외한 999개가 실패하고 재시도를 반복하게 되어, 엄청난 수의 예외를 발생시키고 리소스를 낭비하게 됩니다.
### 데이터베이스를 이용한 잠금 (네임드 락, Named Lock)
- 방법: MySQL 등 일부 데이터베이스에서 제공하는 기능으로, 테이블이나 Row가 아닌 사용자가 지정한 '이름'에 대해 락을 획득하는 방식입니다. GET_LOCK('lecture_1_enroll')과 같은 함수를 호출하여 락을 획득하고, 처리가 끝나면 RELEASE_LOCK()으로 해제합니다.
- 장점: 특정 데이터 Row와 상관없이 여러 로직을 하나의 락으로 묶어서 제어할 수 있어 유연성이 높습니다.
- 단점: 특정 데이터베이스에 종속적이라 다른 DB로 교체하기 어렵습니다. 락을 획득하고 해제하는 로직을 코드에서 명시적으로 관리해야 하므로 번거롭고, 해제를 누락하면 시스템 전체가 멈추는 데드락이 발생할 수 있습니다.
### Redis 분산 락 (Distributed Lock)
- 방법: 애플리케이션 서버 외부의 Redis 같은 중앙화된 시스템을 사용하여 락을 관리하는 방식입니다. 모든 서버는 이 Redis에 특정 키(예: lock:lecture:1)에 대한 락을 요청하고, 락을 획득한 서버만 임계 영역(critical section) 로직을 실행할 수 있습니다. Java에서는 Redisson 라이브러리를 사용하면 복잡한 로직을 손쉽게 구현할 수 있습니다.
- 장점: 서버가 여러 대인 분산 환경에서 동시성을 제어할 수 있는 해결책입니다. DB에 직접 부하를 주지 않아 성능이 뛰어나고, 스케일 아웃에 유연하게 대처할 수 있습니다.
- 단점: Redis라는 외부 시스템에 대한 의존성이 생깁니다. Redis 장애 시 락을 획득할 수 없어 서비스에 영향을 줄 수 있습니다. DB 락에 비해 구현이 다소 복잡합니다.
#### 구체적인 Redis 분산 락 종류 (더 많음)
Lettuce 락
- Redis를 사용하여 비교적 간단한 동시성 제어를 원하는 경우
- 빠른 동기화가 필요한 경우
- 분산 환경일 경우 (여러 개의 DB, 서버)
- 스핀 락 방식
Redisson 락
- 분산 시스템에서 효율적으로 락을 관리하고, 부하를 최소화해야 하는 경우
- 동시 접근이 많은 시스템에서 성능과 동기화 문제를 모두 해결하고자 할 경우
- 분산 환경일 경우 (여러 개의 DB, 서버)
- pub/sub 방식 가능
Lettuce의 스핀 락을 적극 활용하게 된다면 지속적으로 재시도를 요청하므로 Redis에 부하를 주게 됩니다.
이후, 계속해서 대기 상태에 빠지면 전체 서비스에 문제가 생길 수 있습니다.
조금 고민해 보자면,
재시도가 필요하지 않은 Lock의 경우 비교적 간단한 Lettuce를 활용해 볼 수 있을 것 같습니다 !
이외의 경우, 조금 더 많은 고민이 필요하겠지만 의존성이 발생하더라도 Redisson 락을 사용할 것 같습니다 ..
'SSA > Back' 카테고리의 다른 글
| [SSA] 특강 개설 시 선착순 동시성 이슈 해결 (2) | 2025.09.10 |
|---|---|
| [SSA] 수강 로직에 대한 간단한 고민 .. (0) | 2025.09.09 |
| [SSA] Kafka Consumer가 멱등성을 가질 수 있도록 설계 (0) | 2025.09.09 |
| [SSA] 수강 기능, 알림 기능을 이벤트 기반으로 분리 (0) | 2025.09.09 |
| [SSA] 이벤트 발행에 트랜잭셔널 아웃박스 패턴을 적용 (1) | 2025.09.08 |