최근에 팀 프로젝트에서 뉴스피드 기능을 개발하면서, 동시에 여러 사용자가 게시글을 수정하거나 좋아요를 누르는 상황을 고려하게 되었다. ( 내 파트는 아니지만, 그래도 궁금해졌다. )
" 만약 여러 사용자가 동시에 같은 데이터를 수정하면 충돌이 발생하지 않을까? "
" 이런 경우에는 어떻게 동시성 문제를 처리해야 할까? "이런 고민을 하다가 DB 동시성 개념에 대해 찾아보게 되었고,
오늘은 대표적인 락 방식인 낙관적 락, 비관적 락, 분산 락에 대해 정리해 보았다.
동시성을 설명하는 글이 아니기 때문에 가볍게 비유해서 설명하겠다.
철수와 영희는 아메리카노를 마시기 위해서 카페로 간다.
카페에 있는 아메리카노 재고는 1잔 뿐이다.
주문은 키오스크로 해야한다.
철수 : 아메리카노 1잔 ( A 키오스크에서 요청 )
영희 : 아메리카노 1잔 ( B 키오스크에서 요청 )
이 둘은 요청은 동시에 진행되었다면
현재 카페에 아메리카노는 1잔만 있는데, 아메리카노 2잔이 주문이 되어버린 것 이다.
( 당신이 카페 알바라면 손님을 찾아서 양해를 구해야겠지... )
서버 관점에서 보자.
철수: 주문 요청 (A 키오스크) - 서버로 요청 전송
영희: 주문 요청 (B 키오스크) - 서버로 요청 전송
- 철수 트랜잭션 시작
- 영희 트랜잭션 시작
- 금액 차감 (철수)
- 금액 차감 (영희)
- 커피 재고 차감 (철수)
- 커피 재고 차감 (영희)
- 철수 주문 완료 - DB에 업데이트
- 영희 주문 완료 - DB에 업데이트
위에서 우리는 동시에 라는 말을 써서 신청했다고 했다.
그런데 사실 컴퓨터 세계에서 "완벽한 동시"라는 것은 존재하지 않는다.
모든 요청은 CPU, 메모리, 그리고 DB가 처리 가능한 순서대로 차례차례 처리된다.
위에서 비유에서는 엄밀히 말하면 철수가 먼저 주문 요청을 보내는 순간 먼저 처리되기 시작한다.
하지만 이 시간 차이는 사람이 느낄 수 없을 만큼 미세하기 때문에 우리는 보통 "동시에 눌렀다"라고 표현한다.
문제는 이렇게 미세한 시간차로 인해서 DB 입장에서는 두 요청을 모두 허용해버릴 수 있다는 것이다.
결국 경쟁 상태가 발생하게 된다.
여러 작업(프로세스, 스레드, 트랜잭션) 이 서로 동시에 같은 데이터를 변경하려고 할 때
처리 순서에 따라 결과가 달라지는 상황을 경쟁 상태라고 한다.
정리하면...
이런 문제를 해결하기 위해서 우리는 여러 종류의 Lock 전략(낙관적, 비관적, 분산 락)을 사용할 수 있다.
비관적 락은 " 동시성 문제가 발생할 가능성이 높다. "고 절망적으로 예상하고,
데이터를 읽기 전에 먼저 락을 걸어 다른 트랜잭션들이 동시에 접근하지 못하게 막는 방법이다.
철수 : 아메리카노 1잔 주문 시도 (A 키오스크에서 요청)
- 철수의 주문 요청이 서버에 도착하면서 락이 걸린다.
- 영희가 주문 시도 (B 키오스크에서 요청)
- 영희의 요청은 철수의 락이 해제될 때까지 대기하게 된다.
철수 주문 처리 완료 - 재고가 0으로 변경됨 - 락 해제
영희 요청 처리 시작 - 재고 확인 - 재고 부족으로 주문 실패 안내
이번에는 아메리카노가 1잔만 있어서 정확하게 1잔만 판매되고,
영희는 재고 부족 메시지를 받게 된다.
(카페 알바가 손님에게 사과할 일이 없어졌다.)
낙관적 락은 " 충돌이 잘 안 날 거라고 희망 " 하고,
락을 걸지 않고 처리한 후 커밋 시점에 충돌을 감지해서 처리하는 방법이다.
즉, 주문 시작할 때는 락을 걸지 않고 그냥 진행하고,
주문 완료 시점에 재고가 바뀌었는지 확인하게 된다.
철수 : 아메리카노 1잔 주문 시도 (A 키오스크)
- 재고 확인 (현재 1잔) - 주문 처리 진행 중
영희 : 아메리카노 1잔 주문 시도 (B 키오스크)
- 재고 확인 (현재 1잔) - 주문 처리 진행 중
철수 - 주문 완료 시도 (커밋)
- 성공적으로 재고 1잔 - 0잔으로 변경 - 커밋 완료
영희 - 주문 완료 시도 (커밋)
- 커밋 시점에 버전이 바뀐 것 감지됨
- 재고 부족 - 주문 실패 - 사용자에게 실패 메시지 표시
결국 아메리카노는 1잔만 판매되고,
영희는 "재고 부족" 안내를 받게 된다.
분산 락은 서버가 여러 대일 때,
서버들끼리공통된 락을 관리해서 동시에 같은 데이터를 수정하지 않게 하는 락이다.
이해가 잘 안되어서 참고한 블로그 우아한 기술블로그
동시성 문제로 인해서 버그가 일어났다. 개인적인 생각으로는 비관적락이 적용이 되어 있겠지만, 서버가 달라서 이런 일이 발생한 것 같다.
예를 들어서 다음과 같다.
A 키오스크 - 서버 1 - DB
B 키오스크 - 서버 2 - DB
C 키오스크 - 서버 3 - DB철수는 A 키오스크에서 주문 영희는 B 키오스크에서 주문 민수는 C 키오스크에서 주문 문제는 서버 1이 락을 걸어도 서버 2, 3이 그 사실을 모를 수 있다는 점이다. 서버 2와 3도 동시에 주문 처리를 시도할 수 있다.
이와 같은 부분을 해결하기 위해서 분산 락 전략을 사용하는 것 이다.
창고 하나를 공유하는 커피 전문점이 3개 있다고 하자. (가정: 창고에는 딱 1개의 아메리카노를 제조할 수 있다.) A 매장에서 철수가 아메리카노를 주문 B 매장에서 영희가 아메리카노를 주문 C 매장에서 민수가 아메리카노를 주문 A 매장의 직원이 공용 창고로 가서 [ A 매장에서 아메리카노 처리 중 ] 이라는 표지를 붙인다. 이 표지가 붙어 있는 동안 B 매장과 C 매장은 공용 창고에서 아메리카노를 가져올 수 없고, A 매장의 처리가 끝날 때까지 기다리게 된다. 처리가 끝나면 표지가 제거되고, 그제서야 다음 매장이 처리를 시도할 수 있다.
Q. 그럼 비관적 락이랑 분산 락이랑 개념이 비슷한 것 같은데?
비관적 락도 내가 접근하고 있을 경우 다른 트랜잭션이 들어오지 못하게 막는 것이며, 분산 락도 내가 접근하고 있을 경우 다른 트랜잭션이 들어오지 못하게 막는다. 그럼 뭐가 다른 걸까?
| 락 종류 | 막는 범위 | 기술적 차이 |
|---|---|---|
| 비관적 락 | DB 내부 (트랜잭션 레벨) | DB 트랜잭션 안에서 SELECT FOR UPDATE 같은 쿼리로 락 |
| 분산 락 | 서버 여러 대 (서버 간) | Redis, Zookeeper 같은 외부 시스템에 락 정보를 기록하고 공유 |
즉, 비관적 락은 주문이 들어오면 주방(하나의 DB)에서 문을 잠그고 작업하는 것이고,
분산 락은 서버 여러 대가 공용 창고(외부 시스템)에 락을 걸고 서로 조율하는 것이다.
Distributed Locking(분산 락)은 이름과 달리 완벽한 락 보장을 하지 않는다. 실제로는 "여러 노드가 동시에 락을 가졌다고 착각할 수 있는 상황"이 발생할 수 있으며, 시스템은 이를 감당할 수 있도록 설계해야 한다. 결국 이것은 Leader Election(리더 선출) 과 매우 유사한 문제이며, 실제 구현에서는 Eventual Leader Election 기반으로 설계해야 한다.
( 분산 락 )
Q. 예를 들어서 A B C 매장이 있고, A 매장이 공용 창고를 쓴다고 표지를 붙었다. 그럼 B, C 는 A 의 작업이 끝날 때 까지 기다려야 한다. 그런데 만약 A 매장 직원이 잠깐 화장실을 간 상황을 생각해보자. 공용 창고에는 사람이 없는 것처럼 보인다. 그러면 B 매장은 "아무도 없는 것 같네?" 라고 판단해서 자신의 표지를 새로 붙이고 창고를 사용하게 될 수 있다. 그렇게 되면 A 직원이 돌아왔을 때는 결국 A와 B 두 명이 동시에 공용 창고를 사용하는 상황이 되어버린다.
이것에 관해서는 다음에 정리해보도록 하겠다.