[동시성 제어] 회의실 예약 시스템, 비관적 락으로 해결될까? (feat. Named Lock)

Category
Tags
Published
Last updated
Last updated January 14, 2026
회의실 예약 서비스를 개발한다고 가정해보자. 가장 중요한 핵심 기능은 무엇일까? 화려한 UI도, 빠른 검색도 아니다. 바로 **"같은 시간에 중복 예약이 발생하지 않는 것(Data Integrity)"**이다.
하지만 이 단순해 보이는 요구사항은 '동시성(Concurrency)'이라는 환경을 만나면 지옥의 난이도로 변한다. 오늘은 회의실 예약 시스템을 설계하며 겪을 수 있는 동시성 이슈와, 왜 일반적인 **비관적 락(Pessimistic Lock)**이 아닌 **네임드 락(Named Lock)**이 해답이 되는지 정리해 본다.

1. 문제의 시작: "조회하고 저장한다"의 함정

가장 직관적인 로직은 다음과 같다.
  1. 해당 시간대에 예약이 있는지 조회한다. (SELECT count(*))
  1. 없으면(0건이면) 예약 데이터를 저장한다. (INSERT)
하지만 이 로직은 동시성 테스트를 돌리는 순간 무너진다. User A와 User B가 0.001초 차이로 조회를 요청하면, 둘 다 "예약 없음(0건)"을 확인하고 INSERT를 날린다. 결과는 **Double Booking(이중 예약)**이다. 이것이 전형적인 **Race Condition(경쟁 상태)**이다.

2. 첫 번째 시도: 비관적 락 (Pessimistic Lock)

DB 좀 다뤄본 개발자라면 가장 먼저 떠올리는 것이 SELECT ... FOR UPDATE 구문을 활용한 비관적 락이다. 데이터를 읽을 때 락을 걸어 다른 트랜잭션이 건들지 못하게 하는 정석적인 방법이다.
하지만 회의실 예약에서는 이 방법이 통하지 않는다.
"락을 걸 대상(Row)이 없기 때문이다."
  • 은행 계좌: 이미 존재하는 내 잔고 Row를 수정(UPDATE)한다. -> Row에 락을 걸면 됨.
  • 회의실 예약: 아직 존재하지 않는 예약 데이터를 생성(INSERT)한다. -> 락을 걸 Row가 없다.
빈 시간대를 조회했을 때 DB는 아무것도 리턴하지 않는다. 실체가 없으니 락도 걸리지 않는다. 이를 데이터베이스 용어로 Phantom Read(유령 읽기) 문제라고 한다. 물론 Gap Lock 등을 활용할 수도 있지만, 구현 난이도가 높고 데드락 위험이 크다.

3. 두 번째 시도: 낙관적 락 (Optimistic Lock)

그렇다면 엔티티에 version을 둬서 충돌을 감지하는 낙관적 락은 어떨까? 데이터 무결성은 지킬 수 있다. 하지만 **사용자 경험(UX)**이 나쁘다.
  • A와 B가 동시에 버튼을 누른다.
  • A는 성공한다.
  • B는 "누군가 먼저 예약했습니다. 다시 시도해주세요."라는 에러를 보고 처음부터 다시 해야 한다.
선착순 이벤트라면 모를까, 일반적인 예약 시스템에서 사용자에게 계속 "실패했으니 다시 하라"고 하는 것은 좋은 경험이 아니다. 우리는 "줄을 서서 기다리면 내 차례에 처리되는" 구조가 필요하다.

4. 최종 해답: 네임드 락 (Named Lock / User Level Lock)

여기서 관점의 전환이 필요하다. 데이터(Row)가 없어서 락을 못 건다면, "회의실 이름"이라는 가상의 문자열(String)에 락을 걸면 되지 않을까?
MySQL은 GET_LOCK(str, timeout)이라는 함수를 제공한다.
  • User A: "Room_101 이라는 문자열 락 획득!" -> 성공
  • User B: "Room_101 락 획득 시도..." -> 대기(Blocking)
이 방식(Named Lock)을 적용하면 로직은 다음과 같이 변한다.
  1. 락 획득: GET_LOCK('Room_101', 10초)
  1. 검증: 예약 중복 조회 (SELECT count)
  1. 저장: 예약 생성 (INSERT)
  1. 락 해제: RELEASE_LOCK('Room_101')
이제 데이터의 존재 유무와 상관없이, 회의실이라는 자원에 대해 순차적 처리가 보장된다. Redis 같은 별도의 인프라 없이 MySQL 하나로 깔끔하게 동시성 제어가 가능해진다.

5. 치명적인 주의사항: 커넥션 풀(Connection Pool) 고갈

네임드 락은 만능이 아니다. 구현 시 단 하나의 치명적인 함정을 피해야 한다. 바로 커넥션 풀 고갈이다.
네임드 락은 Session 기반이다. 락을 획득하고 해제할 때까지 DB 커넥션을 하나 물고 있는다. 만약 락 전용 커넥션 풀을 분리하지 않는다면?
  • 예약 트래픽이 몰려 모든 커넥션이 "락 대기" 상태가 된다.
  • 예약과 상관없는 로그인, 게시판 조회 요청도 "DB 연결 불가"로 함께 죽어버린다.
  • 서버 전체 장애 발생.
[Best Practice] 반드시 네임드 락을 처리하는 DataSource(커넥션 풀)를 비즈니스 로직용과 분리해야 한다. (예: HikariCP-Core: 50개 / HikariCP-Lock: 10개) 이렇게 하면 예약이 밀려도 다른 서비스에는 영향을 주지 않는다.

6. 결론: 도메인에 맞는 락을 쓰자

이번 고민을 통해 **"상황에 따라 정답 락(Lock)은 다르다"**는 것을 배웠다.
  • 은행 잔고 수정 (Update): Row가 존재하므로 **비관적 락(Pessimistic Lock)**이 표준.
  • 회의실/공연 예약 (Insert): Row가 없으므로 **네임드 락(Named Lock)**이 효율적.
  • 대규모 트래픽: DB 부하를 줄여야 하므로 Redis 분산 락 고려.
"소 잡는 칼로 닭 잡지 말라"는 말이 있다. 소규모~중규모 회의실 예약 시스템에서 Redis 분산 락은 오버엔지니어링일 수 있다. RDBMS가 제공하는 Named Lock만 잘 활용해도, 데이터 정합성과 사용자 경험 두 마리 토끼를 잡는 견고한 시스템을 만들 수 있다.