👋
안녕하세요~ 평비입니다!
오늘은 지난 시간에 이어 트랜잭션 락 개념에 대해서 나름(?) 자세하게 준비한 포스팅을 준비해봤습니다!
지난 번 트랜잭션 격리 수준은 트랜잭션이 데이터 읽기(조회)를 할 때, 어떻게 할 것인가? 에 대한 개념이었습니다.
반면, 이번에 다루는 트랜잭션 락은 트랜잭션이 데이터 쓰기(갱신)를 할 때, 어떻게 할 것인가? 에 대한 개념입니다!
https://gatchbee.tistory.com/66
갱신 손실(lost update) 이슈는 어떻게 방지할까요? - 기술 면접 준비
면접관 : A 금융 프로젝트에서 송금 기능을 구현하셨는데요.송금과 동시에 자동 이체로 돈이 빠져나가면 잔고 정합성이 맞지 않는 문제가 있을텐데,이 이슈는 어떻게 방지하셨나요? 📢 문제 상
gatchbee.tistory.com
갱신 손실 이슈를 막기 위한 지난 포스트에서, 락과 격리 수준을 조절했는데요. 락 관련한 용어들이 어떤 것들이 나왔는지 정리해봤습니다.
- SELECT FOR UPDATE
- 락 경합
- Row-level Lock
- 락 대기 시간
- 데드락
- 서로 다른 순서로 여러 테이블을 락
- 계좌별 단일 큐 처리 방식
- Versioning(Optimistic Locking, 낙관적 락)
- Pessimistic Locking (비관적 락)
우선, 위의 내용들을 3가지 카테고리로 나눠볼 수 있을 것 같습니다.
1. 데이터를 건드리지 못하도록 잠금 (Pessimistic 방식)
우선, 말 그대로, 데이터를 건드리지 못하도록 잠금하는 방식인 Pessimistic, 비관적 락입니다.
비관적 락의 철학은 다음과 같습니다.
다른 트랜잭션이 건드릴 수도 있으니 미리 락을 걸자
비관적 락을 쉽게 설명하기 위해서 실제 사례를 그림으로 표현해봤습니다.
위의 사진을 보면, 식당에서 방해를 받지 않기 위해 일정 시간 동안 브레이크 타임(락)을 걸고 있는 것을 볼 수 있습니다.
손님 입장에서는 매우 곤란한 상황입니다... 애써 찾아간 맛집이 브레이크 타임이라니! 무슨 일을 하고 있길래, 브레이크 타임씩이나 하는 거야?! 그렇게 바쁜가? 저는 이런 생각이 들던데요!
가게 입장에서는 이렇게 하지 않으면 쉴 수 없다고 비관적으로 생각하여 잠금을 한 상태입니다.
1-1. SELECT FOR UPDATE
= Row-level Lock
SELECT * FROM TB_USER WHERE USER_ID = 'ABCD1234' FOR UPDATE;
위 구문이 대표적인 비관적 락 구문입니다. USER_ID = 'ABCD1234'인 row에 락을 거는 것입니다. (Row-level Lock)
이 락이 걸려있는 동안, 다른 트랜잭션은 이 ROW를 접근할 수 없습니다. 읽기는 가능합니다. (Exclusive lock)
🚨 경고! WHERE 절이 없다면, 모든 full table row-level lock이 걸려버리겠죠?!
1-1-1. Exclusive lock (배타 락)
한 ROW에 대해서 배타 락이 걸리면, 일반 읽기는 가능하지만 다른 트랜잭션은 해당 ROW에 접근이 불가능합니다.
1-2. 락 경합
락이 걸린 row를 동시에 접근하려는 트랜잭션은 아주 미세한 차이로도 경합을 해야겠죠? 경합에서 진 트랜잭션은 대기를 해야 합니다. 대기 시간이 긴 경우, 타임아웃이 발생할 수 있습니다.
1-3. 데드락
서로 다른 트랜잭션이 서로가 가진 락을 기다리는 교착 상태를 뜻합니다.
데드락의 가장 흔한 케이스가 서로 다른 순서로 여러 테이블을 락 거는 경우입니다.
2. 락을 사용하지 않는 낙관적 락 (Optimistic 방식)
락은 사용하지 않지만, 동시 쓰기 시 쓰기 충돌이 일어나서 트랜잭션이 실패하도록 처리하는 방식인 낙관적 락입니다.
일반적으로, 하나의 버전 컬럼을 두고 데이터 수정 시점에 버전이 같은 지 비교하는 방식입니다.
낙관적 락의 철학은 다음과 같습니다.
무슨 락까지 걸어... 방해 받으면 막자!
낙관적 락을 쉽게 설명하기 위해서 실제 사례를 그림으로 표현해봤습니다.
위의 사진을 보면, 식당 문이 활짝 열리고 손님이 들어왔는데, 식사는 막히는 것을 볼 수 있습니다. 손님 입장에서는 문이 열려 있어서, 즐겁게 들어갔지만, 식사는 할 수 없는 상황입니다. 하지만, 납득은 됩니다... 식사를 하고 계셨군요!
가게 입장에서는 이 시간에 손님이 오겠어? 하고 낙관적으로 생각하여 굳이 락까지 걸지는 않되, 방해 받으면 안내해주자! 생각한 것입니다.
2-1. 낙관적 락 구현
낙관적 락은 DB 수준에서 제공하는 기능이 아니라, 애플리케이션 레벨에서 구현하는 동시성 제어 방식입니다.
2-1-1. SQL 기반 예시
트랜잭션 1이 (FOR UPDATE 락 없이) 데이터를 조회합니다.
SELECT * FROM TB_USER WHERE USER_ID = 'ABCD1234';
-- version = 0
트랜잭션 2가 갱신을 선수칩니다!
UPDATE TB_USER
SET USER_NAME = 'User1', version = version + 1
WHERE USER_ID = 'ABCD1234' AND version = 0;
-- USER_ID = 'ABCD1234'의 version은 1로 갱신됨
-- ✅ 성공 (1 row affected)
트랜잭션 1이 갱신을 시도합니다. 하지만, 버전이 다르기 때문에 실패합니다.
UPDATE TB_USER
SET USER_NAME = 'User2', version = version + 1
WHERE USER_ID = 'ABCD1234' AND version = 0;
-- version = 0인 USER_ID = 'ABCD1234' 없음
-- ❌ 실패 (0 row affected)
2-1-2. Spring + JPA 기준 예시
@Entity
public class User {
@Id
private String userId;
private String userName;
@Version
private Integer version;
}
JPA는 @Version 필드를 기준으로 자동으로 낙관적 락을 적용합니다.
충돌 시, OptimisticLockException 이 발생합니다. 해당 예외로 예외 처리 로직 혹은 재시도 전략을 도입할 수 있습니다.
3. 시스템 설계적 동시성 제어
단일 큐 처리 방식은 시스템을 동시성 이슈를 방지하도록 설계된 방식입니다. 요청이 바로 서버를 통해 데이터베이스에 반영되는 것이 아니라, 먼저 단일 큐에 요청을 넣고 서버가 단일 큐로부터 요청을 하나씩 받아서 처리할 수 있도록 하는 방식입니다.
이렇게 설계하면, 단일 큐 방식이기 때문에 직렬 구조라서 자연스럽게 병렬성이 떨어지게 됩니다. 이 경우를 대비해서, 계좌 ID 별로, 좌석 ID 별로 큐를 분리하는 등의 방법을 활용합니다.
=> 계좌별 단일 큐 처리 방식
일례로, Kafka에서는 메시지의 key를 기준으로 파티션이 결정되도록 합니다.
같은 key는 항상 같은 파티션으로 들어가서 해당 파티션은 순차적으로 처리되는 반면, 다른 key에 대해서는 파티션이 달라서 병렬 처리가 될 수 있도록 하는 중요한 기능이죠.
👏
자, 이렇게 동시성 이슈 중 하나인 갱신 손실을 막는 방법에 대해서 알아보며 트랜잭션 격리 수준과 락에 대해서 다뤄봤습니다!
저도 이 포스팅을 작성하면서, 좀 더 상세하게 공부를 하게 된 것 같은데요...!
다시 첫 포스팅으로 돌아가보면, 고인물은 아래와 같은 말을 했었는데, 이제 이해가 되실까요?
🧙고인물
Serializable 격리 수준은 비용이 크므로 Repeatable Read + Optimistic Lock 조합을 사용했고, 고빈도 계좌에 대해서는 Event Sourcing 기반의 비동기 Command Queue를 통해 성능과 정합성을 확보했습니다.
1. Serializable 격리 수준은 비용이 크다?
트랜잭션 정합성을 강하게 요구하는 도메인에서는 Serializable 격리 수준을 사용할 수도 있겠지만, 대규모 트래픽 상황에서는 락 경합이 많이 일어나서 병렬성이 떨어지니까 성능이 저하됩니다. 이 부분에서 비용이 크다고 하는 것입니다.
2. Repeatable Read + Optimistic Lock
일반 계좌에 대해서는 Repeatable Read를 통해서, 조회에 다른 트랜잭션의 갱신과 무관하도록 처음 조회한 값이 계속 유지가 되게 합니다. 다만 갱신 손실 이슈를 막기 위해서 갱신이 일어나는 시점에 대해서는 낙관적 락을 걸어서, 버전이 다른 경우 쓰기 충돌이 일어나서 방지합니다.
3. Event Sourcing 기반의 비동기 Command Queue 패턴
데이터 분석을 통해 알아낸 고빈도 계좌에 대해서는, Kafka 등과 같은 도구를 통해 Event Sourcing 기반의 비동기 Command Queue 패턴(계좌별 단일 큐 처리 방식의 일종)으로 해결하는 것입니다.
즉, 고인물이 이야기한 것은 Repeatable Read + Optimistic Lock, Event Sourcing 기반의 비동기 Command Queue 패턴을 조합해서 성능과 정합성을 모두 챙기는 아키텍처를 설계하였다고 하는 것입니다.
평비의 이 평범한 글이 여러분에게 비범한 도움이 되었으면 좋겠습니다 👍
'기술 면접 준비' 카테고리의 다른 글
동기/비동기와 블로킹/논블로킹 - 기술 면접 준비 (1) | 2025.05.26 |
---|---|
동시성과 병렬성 - 기술 면접 준비 (0) | 2025.05.23 |
트랜잭션 격리수준 - 기술 면접 준비 (0) | 2025.05.13 |
갱신 손실(lost update) 이슈는 어떻게 방지할까요? - 기술 면접 준비 (0) | 2025.05.11 |
B+ tree란? - 기술 면접 준비 (2) | 2025.05.07 |