Concurrency & Transactions
엔터프라이즈 애플리케이션에서 동시성 문제를 다루는 핵심 개념과 패턴.
두 가지 근본 문제
Lost Update: Martin이 파일을 편집하는 동안 David도 같은 파일을 편집. David이 먼저 저장하고 Martin이 나중에 저장하면 David의 변경이 사라진다.
Inconsistent Read: Martin이 locking 패키지(7개 파일)를 세고, David이 두 패키지 모두 수정한 뒤, Martin이 multiphase 패키지(8개)를 센다. 합계 15개 — 하지만 12개(수정 전)도 17개(수정 후)도 아닌, 존재한 적 없는 숫자다.
Optimistic vs Pessimistic
Optimistic: 소스 코드 관리(Git)처럼 동작. 자유롭게 작업하고, commit 시점에 충돌을 감지. 대부분의 웹 앱에서 기본 전략.
Pessimistic: 파일 잠금처럼 동작. 누군가 편집 중이면 다른 사람은 기다려야 한다. 충돌 비용이 높을 때.
System Transaction vs Business Transaction
System Transaction: DB가 관리하는 ACID 트랜잭션. BEGIN → SQL들 → COMMIT/ROLLBACK. 짧게 유지해야 한다.
Business Transaction: 사용자 관점의 트랜잭션. 로그인 → 여러 화면 조작 → 최종 확인. 여러 요청에 걸칠 수 있다.
문제: 비즈니스 트랜잭션을 하나의 시스템 트랜잭션으로 감싸면 long transaction이 되어 DB 성능이 붕괴한다. 해법: 각 요청을 별도 시스템 트랜잭션으로 처리하고, 비즈니스 트랜잭션의 일관성은 Offline Lock으로 관리한다.
ACID 속성
| 속성 | 의미 | 예시 | |------|------|------| | Atomicity | 전부 성공하거나 전부 실패 | 계좌 이체: 출금+입금이 함께 | | Consistency | 트랜잭션 전후 모두 일관된 상태 | 잔고가 마이너스가 되면 안 됨 | | Isolation | 다른 트랜잭션의 중간 상태가 보이지 않음 | 이체 중간에 잔고 조회하면 정확한 값 | | Durability | commit된 결과는 영구 | 서버 크래시 후에도 이체 결과 유지 |
트랜잭션 격리 수준
| 격리 수준 | Dirty Read | Unrepeatable Read | Phantom | |----------|-----------|-------------------|---------| | Read Uncommitted | ✅ 허용 | ✅ 허용 | ✅ 허용 | | Read Committed | ❌ 방지 | ✅ 허용 | ✅ 허용 | | Repeatable Read | ❌ 방지 | ❌ 방지 | ✅ 허용 | | Serializable | ❌ 방지 | ❌ 방지 | ❌ 방지 |
대부분의 웹 앱은 Read Committed로 충분하다. Serializable은 정확하지만 성능 비용이 크다.
Offline Concurrency Patterns
Optimistic Offline Lock
Pessimistic Offline Lock
Coarse-Grained Lock
관련 객체 그룹을 하나의 Lock으로 묶는다. Student와 그의 Enrollments를 개별로 잠그는 대신, Student를 잠그면 Enrollments도 함께 잠긴다.
Implicit Lock
Lock 획득을 프레임워크가 자동으로 처리. 개발자가 까먹을 수 없다. Unit of Work나 Layer Supertype에 Lock 로직을 내장하는 방식.
예시: Optimistic Offline Lock 구현
-- 테이블에 version 컬럼 추가
CREATE TABLE students (
id VARCHAR(20) PRIMARY KEY,
name VARCHAR(100),
department VARCHAR(50),
version INT DEFAULT 1, -- 버전 카운터
modified_by VARCHAR(50), -- 누가 수정했는지
modified_at TIMESTAMP -- 언제 수정했는지
);
-- 조회 시 version도 함께 가져옴
SELECT id, name, department, version FROM students WHERE id = ?;
-- 수정 시 version을 WHERE 조건에 포함 + 증가
UPDATE students
SET name = ?, department = ?, version = version + 1,
modified_by = ?, modified_at = CURRENT_TIMESTAMP
WHERE id = ? AND version = ?;
-- → row count가 0이면 충돌 발생!
class StudentMapper {
void update(Student& student) {
int rowCount = db.execute(
"UPDATE students SET name=?, department=?, version=version+1, "
"modified_by=?, modified_at=CURRENT_TIMESTAMP "
"WHERE id=? AND version=?",
{student.name, student.department,
currentUser(), student.id, student.version});
if (rowCount == 0) {
throw ConcurrencyException(
"Student " + student.id + " was modified by another session. "
"Please reload and try again.");
}
student.version++; // 메모리의 버전도 증가
}
};
템플릿: 동시성 전략 선택
## 동시성 전략 설계
### 1. 비즈니스 트랜잭션 분석
| Use Case | 단일 요청? | 여러 요청에 걸침? | 충돌 가능성 |
|----------|----------|----------------|-----------|
| | | | 낮/중/고 |
### 2. 패턴 선택
| Use Case | 시스템 트랜잭션 | Offline Lock |
|----------|--------------|-------------|
| 단일 요청 UC | Request Transaction | 불필요 |
| 다중 요청 + 충돌 낮음 | Request Transaction | Optimistic |
| 다중 요청 + 충돌 높음 | Request Transaction | Pessimistic |
### 3. Optimistic Lock 구현 체크리스트
- [ ] 모든 엔티티 테이블에 `version INT` 컬럼 추가
- [ ] 선택: `modified_by`, `modified_at` 컬럼 추가 (디버깅용)
- [ ] UPDATE/DELETE의 WHERE에 `AND version = ?` 포함
- [ ] UPDATE 후 row count 검사 → 0이면 ConcurrencyException
- [ ] Layer Supertype에서 version 관리 자동화 (Implicit Lock)
### 4. DB 격리 수준
> **선택**: _______________
> **근거**: _______________