Skip to content

Commit 807c17e

Browse files
committed
2026-02-28-db.md post
1 parent e6407cf commit 807c17e

File tree

1 file changed

+158
-121
lines changed

1 file changed

+158
-121
lines changed

_posts/db/2026-02-28-db.md

Lines changed: 158 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,149 +1,186 @@
11
---
2-
title: "RDB 정규화"
3-
description: "RDB 정규화 개념에 대해 설명합니다."
4-
date: 2025-08-07 +00:00:00
5-
permalink: /posts/2025-08-07-db/
2+
title: "비관적 락 vs 낙관적 락"
3+
description: "비관적 락과 낙관적 락이 뭔지와 어떻게 락을 걸 수 있는지 어떤 차이점이 있는지 알아보도록 하겠습니다."
4+
date: 2026-02-28 +00:00:00
5+
permalink: /posts/2026-02-28-db/
66
categories: [Blogging,db]
77
---
88

9-
관계형데이터베이스의 테이블을 효율적으로 설계하기위한 일련의 규칙, 즉 정규화를 말합니다. 데이터 중복을 최소화하고, 데이터 무결성을 향상시키며, 불필요한 데이터 변경을 방지하는것을 목표로 합니다.
9+
동시성 문제를 해결하기 위해서 DB 레벨에서 해결하는 경우가 있습니다. 비관적 락과 낙관적 락으로 해결할 수 있는 방법을 설명합니다.
1010

11-
![Untitled](/assets/img/db/2025-08-07-db-01.png)
11+
> 재고가 100개인 상품을 동시에 사용자 1000명이 요청이 왔을때의 시나리오로 진행하겠습니다.
1212
13-
일반적으로 3NF까지 도달하면 정규화가 됐다고 하고 있습니다. 이후 단계를 하는것은 복잡성이 증가하고 비효율적일 가능성이 높습니다.
13+
## 비관적 락
1414

15-
### 예제테이블 설명
15+
비관적 락을 알려면 배타적 락과 공유 락을 먼저 이해해야합니다.
1616

17-
수강신청한 학생, 강의, 교수의 정보를 담은 정규화 되지 않은 테이블을 만들어 생성합니다.
17+
### 배타적 락(Exclusive Lock / X-Lock)
18+
19+
락을 획득한 트랜잭션만 해당 데이터에 읽기/쓰기 모두 독점합니다. 다른 트랜잭션은 락이 해제될 때 까지
20+
21+
X-Lock, S-Lock 모두 대기합니다.
1822

19-
```sql
20-
CREATE TABLE student_courses_0nf (
21-
student_id INT,
22-
student_name VARCHAR(50),
23-
student_phone VARCHAR(20),
24-
student_major VARCHAR(50),
25-
course_id TEXT,
26-
course_name TEXT,
27-
professor TEXT,
28-
department TEXT,
29-
professor_phone TEXT,
30-
grade TEXT
31-
);
3223
```
24+
TX 1 ──────────────────────────────────────────────────────────▶
25+
🔒 X-Lock 획득 📖 재고 확인 & 사용 ✅ COMMIT (락 해제)
26+
(FOR UPDATE)
3327
34-
![스크린샷 2025-08-07 오전 10.47.49.png](/assets/img/db/2025-08-07-db-02.png)
35-
36-
### 1NF: 원자값 보장
37-
38-
수강신청한 `course_id` ,`course_name` , `professor` 들이 여러개의 값을 가지고 있습니다. 1NF는 이를 원자값으로 가지게하는 작업을 말합니다.
39-
40-
```sql
41-
CREATE TABLE student_courses_1nf (
42-
student_id INT,
43-
student_name VARCHAR(50),
44-
student_phone VARCHAR(20),
45-
student_major VARCHAR(50),
46-
course_id VARCHAR(10),
47-
course_name VARCHAR(50),
48-
professor VARCHAR(50),
49-
department VARCHAR(50),
50-
professor_phone VARCHAR(20),
51-
grade CHAR(1)
52-
);
28+
TX 2 ──────────────────────────────────────────────────────────▶
29+
⏳ 대기중... ⏳ 대기중... 🔒 X-Lock 획득
30+
(읽기도 차단됨) (이미 사용됨 감지)
5331
```
5432

55-
![스크린샷 2025-08-07 오전 11.48.54.png](/assets/img/db/2025-08-07-db-03.png)
33+
### 공유 락(Shared Lock / S-Lock)
34+
35+
여러 트랜잭션이 동시에 읽기는 허용하되, 누군가 읽는 동안 쓰기는 차단합니다. “읽는 동안 데이터가 바뀌지 않음”을 보장합니다. X-Lock 대기, S-Lock 허용합니다.
36+
37+
```
38+
TX 1 ──────────────────────────────────────────▶
39+
🔑 S-Lock 획득 📖 재고 읽기...
40+
(FOR SHARE)
41+
42+
TX 2 ──────────────────────────────────────────▶
43+
🔑 S-Lock 획득 ✅ 📖 재고 읽기...
44+
(동시 허용!) (TX1과 동시에 실행됨!)
45+
46+
TX 3 (쓰기 시도) ──────────────────────────────▶
47+
⏳ X-Lock 대기중...
48+
(S-Lock이 모두 해제될 때까지 쓰기 불가)
49+
```
50+
51+
동시성 문제를 해결하기 위해 비관적 락을 사용하면 배타적 락을 걸어 동시성 문제를 해결 할 수 있습니다.
52+
53+
```java
54+
@Lock(LockModeType.PESSIMISTIC_WRITE)
55+
@Query("SELECT i FROM Inventory i WHERE i.product.id = :productId")
56+
Optional<Inventory> findByProductIdWithPessimisticLock(Long productId);
57+
```
58+
59+
### 문제점
60+
61+
1. 처리량 병목
5662

57-
**1NF 문제점: 삽입/삭제/수정 이상**
63+
```
64+
사용자 10,000명 동시 요청
65+
66+
[ X-Lock 대기열 ]
67+
TX1 처리중... (50ms)
68+
TX2 ⏳ 대기
69+
TX3 ⏳ 대기
70+
TX4 ⏳ 대기
71+
...
72+
TX10000 ⏳ 대기 (최대 50ms × 10,000 = 500초 대기)
73+
```
5874
59-
미적분학 김교수가 갑작스럽게 퇴직하여 다른 교수로 업데이트한다고 했을 때 강의를 듣는 김철수, 이영희 학생에 대한 ROW 2개를 업데이트를 하게됩니다. 이러한 과정에서 업데이트가 원활하게 이루어지지 않는다면 데이터 일관성이 깨지게 됩니다.
75+
한번에 딱 1개의 트랜잭션만 재고 row에 접근 가능하기 때문에, 동시에 요청이 많을수록 마지막 트랜잭션은 대기 시간이 증가할 수 밖에 없습니다.
6076
61-
### 2NF: 부분 함수 종속성 제거
77+
```
78+
재고 차감 (X-Lock) 재고 조회 (S-Lock)
79+
↓ ↓
80+
같은 row 접근 시도
81+
82+
TX_구매1: X-Lock 보유 중...
83+
TX_조회1: ⏳ S-Lock 대기 (X-Lock 때문에 읽기도 못함)
84+
TX_조회2: ⏳ S-Lock 대기
85+
TX_조회3: ⏳ S-Lock 대기
86+
TX_구매2: ⏳ X-Lock 대기
87+
```
6288
63-
현재 프라임키 `student_id` , `course_id` 에는 non-attribute key들이 부분 종속되어 있는걸 확인 할 수 있습니다.
89+
재고를 차감하는 row와 조회하는 row가 겹칠 경우에도 처리량에 문제가 될 수 있습니다.
6490
65-
**부분 함수 종속 관계들:**
91+
2. 데드락
6692
67-
- `student_name, student_phone``student_id` (student_id에만 종속)
68-
- `course_name, professor, department``course_id` (course_id에만 종속)
93+
```
94+
TX1: 상품A 락 획득 → 상품B 락 대기
95+
TX2: 상품B 락 획득 → 상품A 락 대기
96+
```
6997
70-
**완전 함수 종속:**
98+
두 트랜잭션이 묶음으로 서로 다른 상품을 위에 시나리오 처럼 구매하는 경우 데드락이 발생할 수 있습니다.
7199
72-
- `grade``(student_id, course_id)` (복합키 전체에 종속)
73100
74-
![스크린샷 2025-08-07 오전 11.48.54 (1).png](/assets/img/db/2025-08-07-db-04.png)
101+
## 낙관적 락
75102
76-
```sql
77-
-- 학생 테이블 (student_id에만 종속되는 속성들)
78-
CREATE TABLE students_2nf (
79-
student_id INT PRIMARY KEY,
80-
student_name VARCHAR(50),
81-
student_phone VARCHAR(20),
82-
student_major VARCHAR(50)
83-
);
103+
동시성문제를 어플리케이션에서 처리하여 데드락과 성능문제를 풀어볼 수 있습니다.
84104
85-
-- 과목 테이블 (course_id에만 종속되는 속성들)
86-
CREATE TABLE courses_2nf (
87-
course_id VARCHAR(10) PRIMARY KEY,
88-
course_name VARCHAR(50),
89-
professor VARCHAR(50),
90-
department VARCHAR(50),
91-
professor_phone VARCHAR(20)
92-
);
105+
row별로 버전 컬럼을 사용하여 동시성 문제를 해결합니다.
93106
94-
-- 수강 성적 테이블 (프라임키 전체에 종속되는 속성들)
95-
CREATE TABLE enrollments_2nf (
96-
student_id INT,
97-
course_id VARCHAR(10),
98-
grade CHAR(1),
99-
PRIMARY KEY (student_id, course_id),
100-
FOREIGN KEY (student_id) REFERENCES students_2nf(student_id),
101-
FOREIGN KEY (course_id) REFERENCES courses_2nf(course_id)
102-
);
107+
{% tabs log %}
108+
109+
{% tab log 낙관적 락 흐름 %}
110+
```
111+
🔓 DB 락 없이 조회 → ✏️ @Version으로 체크 → 💥 충돌 시 예외 → 🔄 앱에서 재시도
112+
```
113+
{% endtab %}
114+
115+
{% tab log Entity %}
116+
```java
117+
public class Inventory {
118+
...
119+
120+
@Version
121+
@Column(nullable = false)
122+
@Builder.Default
123+
private Long version = 0L;
124+
125+
}
103126
```
127+
{% endtab %}
104128

105-
**2NF문제점**: **이행적 함수 종속성**
106-
107-
과목테이블의 기본키 `course_id``professor_phone` , `department` 결정되는게 아니라 교수의 이름 `professor``professor_phone` , `department` 결정하게 됩니다.
108-
109-
> 🤔 이행적 함수 종속성이 뭔가요?\
110-
> A → B, B → C 관계가 성립할 때, A → C가 성립하는걸 의미합니다.데이터 베이스에서는 기본키(A)가 아닌 일반 컬럼(B)이 다른 일반 컬럼(C)을 결저하는 것을 말합니다.
111-
112-
### 3NF: 이행적 함수 종속성 제거
113-
114-
![스크린샷 2025-08-08 오전 9.48.09.png](/assets/img/db/2025-08-07-db-05.png)
115-
116-
```sql
117-
-- 교수 테이블
118-
CREATE TABLE professors_3nf (
119-
professor_name VARCHAR(50) PRIMARY KEY,
120-
professor_phone VARCHAR(20),
121-
department_name VARCHAR(50)
122-
);
123-
124-
-- 과목 테이블 (3NF)
125-
CREATE TABLE courses_3nf (
126-
course_id VARCHAR(10) PRIMARY KEY,
127-
course_name VARCHAR(50),
128-
professor_name VARCHAR(50),
129-
FOREIGN KEY (professor_name) REFERENCES professors_3nf(professor_name)
130-
);
131-
132-
-- 학생 테이블 (변경 없음)
133-
CREATE TABLE students_3nf (
134-
student_id INT PRIMARY KEY,
135-
student_name VARCHAR(50),
136-
student_phone VARCHAR(20),
137-
student_major VARCHAR(50)
138-
);
139-
140-
-- 수강 성적 테이블 (변경 없음)
141-
CREATE TABLE enrollments_3nf (
142-
student_id INT,
143-
course_id VARCHAR(10),
144-
grade CHAR(1),
145-
PRIMARY KEY (student_id, course_id),
146-
FOREIGN KEY (student_id) REFERENCES students_3nf(student_id),
147-
FOREIGN KEY (course_id) REFERENCES courses_3nf(course_id)
148-
);
129+
{% tab log JPA Repository %}
130+
```java
131+
@Lock(LockModeType.OPTIMISTIC)
132+
@Query("SELECT i FROM Inventory i WHERE i.product.id = :productId")
133+
Optional<Inventory> findByProductIdWithOptimisticLock(Long productId);
149134
```
135+
{% endtab %}
136+
137+
{% tab log Service Code %}
138+
```java
139+
public class CreateOrderWithOptimisticLockUseCase {
140+
private final OrderRepository orderRepository;
141+
private final UserRepository userRepository;
142+
private final InventoryRepository inventoryRepository;
143+
144+
public record Input(@NotNull Long userId, @NotNull Long productId) { }
145+
146+
@Retryable(
147+
retryFor = ObjectOptimisticLockingFailureException.class,
148+
maxAttempts = 3,
149+
backoff = @Backoff(delay = 100)
150+
)
151+
public void execute(Input input) {
152+
User user = userRepository.findById(input.userId())
153+
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
154+
155+
Inventory inventory = inventoryRepository.findByProductIdWithOptimisticLock(input.productId())
156+
.orElseThrow(() -> new IllegalArgumentException("재고를 찾을 수 없습니다"));
157+
158+
inventory.getStockQuantity().decreaseStockQuantity();
159+
160+
Order order = Order.builder()
161+
.product(inventory.getProduct())
162+
.user(user)
163+
.build();
164+
165+
orderRepository.save(order);
166+
167+
}
168+
169+
@Recover
170+
public void recover(ObjectOptimisticLockingFailureException e, Input input) {
171+
log.warn("낙관적 락 재시도 모두 실패: productId={}",input.productId());
172+
throw new RuntimeException("잠시 후 다시 시도해주세요.");
173+
}
174+
}
175+
```
176+
{% endtab %}
177+
178+
### 문제점
179+
180+
1. 충돌이 많은 경우 재시도 증가
181+
182+
낙관적 락은 충돌이 드문 경우 사용이 효율적인데, 충돌이 많은 경우 재시도 횟수와 SELECT + UPDATE가 쿼리가 증가하면서 비관적 락보다 DB 부하가 더 심해질 수 있습니다.
183+
184+
2. 복잡한 로직
185+
186+
어플리케이션내에서 직접 처리해줘야 해서 복잡한 재시도 로직들을 구현해야합니다.

0 commit comments

Comments
 (0)