특강 수강신청에 대한 동시성 제어에서 동시성 문제를 해결하는 코드를 구현해 봤습니다.
# Redisson 라이브러리의 분산 락
다음은 k6 분석 결과입니다.



- Redis Lock을 쓰면 요청이 대기하는 시간이 길어지므로 처리 시간이 매우 증가할 터입니다.
- 실제 요청이 성공한 평균 시간은 1.18초입니다.
- http_req_duration 요청을 처리하는 데 걸리는 평균 시간이 16.29초입니다.
- 1855개의 요청이 5xx 계열의 서버 오류를 반환했습니다.
Redis 분산 락을 쓰지 않고 해결해 보기 위해서
- 비관적 락:
- 데이터베이스 테이블의 한 레코드 전체에 물리적으로 접근이 불가능하도록 락을 거는 거라서 위와 같은 수정이 불가합니다.
- 애플리케이션 안에서도 트랜잭션의 시작과 종료를 전부 기다려야 합니다.
- 네임드 락:
- 애플리케이션 레벨에서 만드는 '단순'한 약속일 뿐 실제 물리적 접근은 다른 곳에서라도 충분히 가능합니다.
- 그러면 비관적 락에 비해 더 짧은 속도로 "거의" 동시에 데이터베이스 레벨에서 UPDATE를 수행할 수 있게 됩니다.
에 대해서 테스트를 해 보고 서비스에 적용해 보고자 합니다.
비관적 락을 적용할 경우에 적용할 데이터의 범위를 매우 타이트하게 설정해서 딱 해당하는 부분만 잠금(해당 레코드만)을 걸어야 합니다.
--> 왜냐하면, 수강신청 중인 특강이 우연찮게 ID가 1씩 차이 나는데 거기다가 갭락(Gap Lock)이 걸리게 되면 다른 특강에 대한 수강신청을 막아 버릴 수도 있습니다.
여기서 의문이 드는데,, 만약에 특강을 개설했는데 강사가 실수로 특강 시작 시간이나 종료 시간, 아니면 뭐 강의 내용에 대해서 실수로 잘못 적었다면 어떻게 할까요 ?
--> 다른 사람이 신청하는 도중에라도 강의 내용을 수정할 수 있게 하면 어떨까 생각했습니다. 이때는
- 기술적인 측면:
- 네임드 락을 통해서 '강의 신청 락', '강의 수정 락'의 방법으로 락을 따로따로 지정해서 작업을 수행하도록 할 수 있을 것 같습니다.
- 운영적인 측면:
- 그리고 강의 내용을 수정하게 되면 기존에 설계했던 방식으로 강의 수정 알림을 보내 줍니다.
- 그래야 혹시라도 신청한 강의의 시간이 맞지 않거나, 강의 내용이 마음에 안 들 경우 다시 취소할 수 있습니다.
- 다만, 수강정원에 대한 수정은 고민을 좀 더 해 봐야 할 것 같습니다 .. 바꿀 수 있게 하는 게 맞는지, 아니면 못 바꾸는 게 맞는지
- 수강정원을 수정할 수 있게 한다면 수정하는 내용이 바로 데이터베이스에 적용되도록 해서 수강신청 트랜잭션이 수강정원의 값을 잘못 읽는 일이 없도록 해야 합니다.
- 그러면 이건 비관적 락을 걸어야 하나 ? 고려할 만한 부분입니다.
- 먼저 수강정원을 읽어 간 트랜잭션이 전부 처리된 다음 강의를 수정해야 할 것 같습니다.
- 그러면 다시 비관적 락으로 돌아가는 거 같습니다 ..
- 수강정원을 수정할 수 없게 하려면 애플리케이션에서 수강정원의 수정은 불가하도록 메서드를 구현합니다.
- 이거는 락을 걸 필요가 없을 거 같습니다.
- 강의 시작 시간이나 종료 시간 같은 내용만 바꾼다면 알림도 갈 것이고, 정합성에 대한 중요성이 다소 떨어집니다. 신경 안 써도 될 정도로요.
- 수강정원을 수정할 수 있게 한다면 수정하는 내용이 바로 데이터베이스에 적용되도록 해서 수강신청 트랜잭션이 수강정원의 값을 잘못 읽는 일이 없도록 해야 합니다.
특강 수강신청 중 수강정원 수정을 허용할 경우 문제 발생 가정
| 시간 | 수강 신청 (네임드 락 사용) | 강의 수정 (비관적 락 사용) | DB 상태 (enrolled/capacity) |
| T1 | GET_LOCK('enroll_lock', ...) 성공. 수강 신청 권한(애플리케이션) 획득. | 25 / 30 | |
| T2 | DB에서 enrolled(25), capacity(30) 값을 읽어 옴. | 25 / 30 | |
| T3 | SELECT ... FOR UPDATE 실행. DB는 어떤 비관적 락도 없으므로 즉시 레코드 락 성공. | 25 / 30 | |
| T4 | DB에서 enrolled(25) 값을 읽고, capacity를 25로 수정 후 커밋. 레코드 락 해제. | 25 / 25 | |
| T5 | T2에서 읽었던 이전 데이터를 기반으로 enrolled를 26으로 증가시키고 커밋. | 26 / 25 (문제 발생 지점) |
- 수강신청에 대해 네임드 락을 사용할 예정이라면 강의 수강정원 수정이 불가하도록 해야 합니다.
- 강의 수정에 대해서 낙관적 락을 쓴다면 ?
- 이것도 단순히 락에서 버전 차이만을 읽기 때문에 수강생/수강정원 : 26/25 문제가 발생합니다.
- 강의 수정에 대해서 낙관적 락을 쓴다면 ?
--> 락을 쓸 거라면 모두 같은 계열의 락을 사용해야 할 것 같습니다. 섞어서 쓸 경우 생기는 문제를 핸들링하기 매우 어려워 보입니다 ..
# 방법 1: 모두 비관적 락 사용
수강 신청과 강의 수정 로직 모두가 JPA의 @Lock(LockModeType.PESSIMISTIC_WRITE)를 사용합니다.
- @Transactional 어노테이션과 같이 간단하게 구현할 수 있습니다. 락을 획득하고 해제하는 것을 신경 쓸 필요가 없습니다. 트랜잭션이 끝나면 자동으로 락이 해제되므로 실수가 발생할 여지가 거의 없습니다. 다만, 락 해제에 따라서 레이스 컨디션은 고민해야 합니다.
- "특정 데이터의 접근을 제한한다"라는 의도가 코드에 명시적으로 드러나기 때문에 알아 보기 쉽습니다.
- 모든 잠금 관리와 쓰레드 대기를 데이터베이스가 직접 처리하므로, 자원 경쟁이 심할 경우 DB에 부하가 집중될 수 있습니다.
특정 데이터(하나의 특강 Row)에 대한 정합성을 지키는 것이 무엇보다 중요한 목표일 때 선택할 것 같습니다.
구현이 간단하고 실수가 적어 안정적입니다.
# 방법 2: 모두 네임드 락 사용
수강 신청과 강의 수정 로직 모두가 동일한 이름의 네임드 락(예: GET_LOCK('lecture_lock_123'))을 사용합니다.
- 데이터베이스 부하 감소: 잠금을 기다리는 동안의 부하를 애플리케이션이 나눠 갖게 되므로, 데이터베이스는 실제 쿼리 처리에만 집중할 수 있습니다.
- try-finally 구문을 사용하여 락 해제를 직접 책임져야 합니다. 이건 어떻게 보면 유연한 것이고, 어떻게 보면 실수 유발인 거라고 볼 수 있을 거 같습니다.
- 만약 다른 사람이랑 협업을 할 경우 이 규칙을 모르고 락 획득 없이 DB에 접근하는 코드를 작성하면, 동시성 제어가 깨지게 됩니다.
- 여러 테이블이나 외부 시스템에 걸친 작업을 직접 동기화하는 유연성을 챙길 수 있을 거 같습니다.
- 경험이 많지 않아서 이 경우는 많이 있나 ? 싶으면서 아직 잘 떠오르지 않긴 합니다 ..
여러 테이블에 걸친 복잡한 로직을 동기화하면서도 DB의 부하를 줄이고 싶을 때 사용하면 좋을 것 같습니다.
고민 중인 "하나의 특강에 대한 수강 신청과 수강정원 수정" 문제에 대해서는 모두 비관적 락 사용이 더 적합한 것 같습니다.
--> 특강의 수강정원이 매우 중요한 문제이기 때문입니다.
우선은 해당 내용을 채택하고 특강 신청 중 강의 내용의 세부적인 수정이 성능적인 면에서 더 중요해질 경우 다른 방법을 적용해 보는 게 나을 것 같습니다.
# DB의 비관적 락
다음은 k6 분석 결과입니다.



- 실제 요청이 성공한 평균 시간은 1.03초입니다.
- Redis 락과 큰 차이는 없습니다.
- 그리고 5xx대 응답을 반환하는 것도 2007개입니다.
주목할 점은 다음입니다.


성능 테스트 결과 비교
| 측정 항목 (Metric) | Redis 분산 락 | DB 비관적 락 | 성능 차이 (비관적 락 우위) |
| 평균 (avg) | 16.29s | 9.18s | 7.11초 빠름 |
| 중앙값 (med) | 17.68s | 11.16s | 6.52초 빠름 |
| 최대 (max) | 30.16s | 15.84s | 14.32초 빠름 (약 1.9배) |
| p(90) | 28.72s | 14.68s | 14.04초 빠름 (약 2배) |
| p(95) | 28.99s | 15.20s | 13.79초 빠름 (약 1.9배) |
현재, 100명이 수강정원인 특강에 대해 1만 명이 신청하는 테스트에서
Redis 락에 비해 DB 비관적 락이 더 빠르게 동작하는 것을 확인했습니다.
수강신청 성공 요청은 당연히 먼저 들어간 사람들 100명이 성공하는 것이니
초반부에 부하가 없는 때에는 성공 요청이 큰 차이가 나질 않습니다.
문제는 그 다음이었습니다.
특강에 대한 수강신청이 실패했을 때에도 사용자에게 빠르게 응답할 수 있어야 합니다.
# DB의 네임드 락
다음은 k6 분석 결과입니다.



- 실제 요청이 성공한 평균 시간은 1.18초입니다.
- Redis 락, DB 비관적 락과 큰 차이가 없네요.
- 5xx대 응답을 반환하는 것은 1799개입니다.


정합성도 깨지지 않았습니다.
문제는 역시 실패 응답에 관한 부분입니다.



동시성 제어 방식별 성능 테스트 결과 비교
| 측정 항목 (Metric) | Redis 분산 락 | DB 비관적 락 | DB 네임드 락 |
| 평균 (avg) | 16.29s | 9.18s | 15.4s |
| 중앙값 (med) | 17.68s | 11.16s | 16.22s |
| 최대 (max) | 30.16s | 15.84s | 29.01s |
| p(90) | 28.72s | 14.68s | 26.53s |
| p(95) | 28.99s | 15.20s | 27.88s |
# 그렇다면 Redis 락이나 네임드 락 대신 비관적 락을 써야 하는가 ?
왜 비관적 락이 빠른지 고민해 보자면
| 구분 | DB 비관적 락 (Pessimistic Lock) | DB 네임드 락 (Named Lock) |
| 잠금 위치 | 데이터베이스 엔진 (최적화되어 있음) | 애플리케이션 (최적화 안 되어 있음) |
| 대기 장소 | DB 내부에서 잠금 대기열 | 애플리케이션과 DB를 오가는 과정 |
| 네트워크 비용 | 상대적으로 적음 | 상대적으로 많음 |
생각해 보면 다소 간단한 문제 같기도 합니다.
DB의 비관적 락은 이미 수많은 최적화를 거쳐서 고도화된 작업일 것이고,
Redis 분산 락이나 DB 네임드 락은 만든 지 얼마 안 된 따끈따끈한 작업이라 최적화도 전일 뿐더러 DB 분산 락만큼 고도화할 수 있을지도 의문인 부분입니다.
결국, 네트워크의 오버헤드 비용이 이러한 시간 차이를 만들어 내는 것 같습니다.
Redis 분산 락과 DB 네임드 락의 시간차가 별로 나지 않는 것도 동일한 이유일 테고요.
# 결론
맨 처음에는 Redis 분산 락이 정답이지 ! 하며 구현의 내용을 속단해 버렸습니다.
실제로는 어떤 내용이 더 최적의 선택일지 깊이 있게 고민하고 실제 테스트로까지 이어져서 선택의 결과를 명확히 해야 됐습니다 ..
Redis 분산 락을 구현해 본 건 학습용으로 남겨 두고 추후 활용할 일이 있다면 다시 뒤짚어 가면서 활용해 보겠습니다.
분산 락인 만큼 분산된 DB 환경에서 적용해야 효율을 높일 수 있을 것 같습니다.
- 근데 '특강'이라는 건 '특별한 강의'라는 뜻인데 이 특강이 있는 데이터베이스를 분산시켜야 할 만큼 많은 리소스가 필요할지에 대해서는 회의적인 입장이긴 합니다.
DB 네임드 락은 단순히 DB 테이블의 어떤 레코드에 대한 접근 제한을 적용한다기보다 애플리케이션 전반에 걸쳐서
복잡한 락의 로직을 비즈니스 로직에 도입해야 할 때 써 볼 만할 것 같습니다.
## 그런데 과연 이게 최선일까요 ?
아예 락을 쓰지 않고 메시지 큐를 통해서 들어온 요청을 모두 담은 다음
사용자에게는 신청이 완료되었다고 알리는 방향은 어떨지 고민입니다.
락 자체를 사용한다는 게 Race Condition을 만들기 마련이고
메시지 큐에 요청의 순서대로 집어 넣고 처리할 때 수강정원만큼 선착순 요청 처리를 하면 구현할 수 있을 것 같습니다.
물론 이것도 실제로 구현해 보고 테스트까지 해 봐야 좋겠습니다.
또한, 생각나는 주의점으로는
- 사용자는 "신청 완료" 응답을 받지만, 이것이 "수강 성공"을 의미하지는 않습니다.
- 실제 성공/실패 결과(수강정원 초과로 인한 실패)는 나중에 따로(SSE, FCM)으로 알려줘야 합니다.
- 메시지 큐라는 자료구조를 별도로 도입하고 운영해야 합니다. 이는 관리할 부분이 많아진다는 것을 의미합니다.
- 사용자 요청 시점과 실제 데이터가 DB에 반영되는 시점 사이에 다른 기술 활용보다 많은 지연이 발생합니다.
즉각적인 결과를 알려 주는 것이 필수가 아닌 서비스에 적합한 해결책 같은데 ..
선착순 수강신청은 사용자 입장에서 즉각적으로 신청이 되었는지, 실패했는지 알고 싶어할 것 같습니다.