서론
- 실제 금융/암호화폐/전자지갑 서비스에서 동시성 결함이나 트랜잭션 제어 미비로 인해 막대한 금액의 해킹 피해가 발생할 수 있으므로 보안과 데이터 무결성은 중요한 개념
- i.g. FlexCoin, Poloniex 해킹 사례
- 여러 건의 출금을 거의 동시에 요청 → 음수 잔고 허용, 데이터베이스에 잘못된 레코드가 삽입되었고 이로 인해 시스템 무결성이 깨지고 공격자는 훨씬 더 많은 금액을 탈취함
- 트랜잭션 보장은 매우 중요한 개념
- 구글 Spanner 등 신세대 DBMS조차 "트랜잭션을 포기하는 것보다, 필요할 때 성능 병목을 해결하는 것이 현명하다"라고 명시
- MongoDB 등도 결국 ACID 보장 기능을 추가 (4.0+ 버전부터 적용)
1. ACID와 트랜잭션 동시성 제어
- 모든 DB 작업은 트랜잭션 내에서 진행됨
- Auto-commit 모드: Statement마다 암묵적으로 트랜잭션 시작/커밋
- 명시적 트랜잭션: BEGIN~COMMIT/ROLLBACK으로 여러 Statement를 하나의 작업 단위로 묶음
1.1 ACID 네 가지 속성
가. Atomicity (원자성)
- 트랜잭션 내 모든 작업이 전부 성공하거나 전부 실패 (rollback)
- 실패 시 중간 변경은 모두 이전 상태로 복원 (undo log 등 활용)
나. Consistency (일관성)
- 트랜잭션 전후로 데이터가 항상 유효한 상태 (모든 제약조건, 무결성 규칙 준수)
- 하나라도 위반되면 전체 트랜잭션 롤백
다. Isolation (격리성)
- 여러 트랜잭션이 동시에 실행되어도, 각각은 다른 트랜잭션의 중간 상태를 볼 수 없음
- Serializable이 가장 높은 격리 수준 (완전 직렬 실행과 동등)이지만 성능 저하 있음
라. Durability (내구성)
- 커밋된 트랜잭션의 결과는 서버 다운, 장애 후에도 항상 보존됨
- Redo log, Write-Ahead Log(WAL), 트랜잭션 로그 등 사용
1.2 DBMS별 ACID 구현
가. Atomicity와 Rollback
- 모든 변경은 실제 데이터 구조(테이블, 인덱스 등)에 적용되지만, 커밋 전에는 언제든지 undo log 등으로 이전 상태로 롤백 가능해야 함.
- Oracle: undo tablespace에서 이전 데이터 버전을 관리
- SQL Server: 트랜잭션 로그에 undo/redo 정보 모두 기록
- PostgreSQL: 데이터 자체에 다중 버전 (MVCC)으로 이전 상태를 보관, VACUUM으로 공간 회수
- MySQL(InnoDB): undo 로그와 purge 프로세스로 이전 상태 관리
나. Durability
- 커밋 시점에 모든 변경이 Redo Log, WAL 등에 안전하게 기록되어야 함
- Oracle: Redo 로그 버퍼를 Log Writer가 디스크로 flush
- SQL Server: 트랜잭션 로그를 commit마다 디스크로 flush (2014+ 버전은 configurable durability)
- PostgreSQL: WAL을 commit마다 flush (비동기 가능)
- MySQL: Redo 로그 (innodb_flush_log_at_trx_commit=1이 권장)
다. Consistency
- 모든 트랜잭션은 데이터베이스의 모든 제약조건을 만족시켜야 함
- 하나라도 위반 시 전체 롤백, DB는 항상 유효한 상태 유지
라. Isolation
- 1.4에서 후술 할 내용
1.3 Consistency의 두 의미
- ACID에서의 Consistency (무결성): 타입, 길이, null, unique, FK 등 제약조건 위반 없이 항상 유효한 DB 상태 유지
- CAP 정리에서의 Consistency (선형화 가능성): 분산 시스템에서 "모든 노드가 같은 값을 본다"는 의미
- ACID의 일관성과 다름
1.4 표준 격리 수준 (Isolation) 및 DBMS별 격리 수준
가. SQL-92 표준 격리 수준
- Read Uncommitted: Dirty Read, Non-Repeatable Read, Phantom Read 모두 허용
- Read Committed: Dirty Read 금지, Non-Repeatable/Phantom Read 허용
- Repeatable Read: Dirty/Non-Repeatable Read 금지, Phantom Read 허용
- Serializable: 모든 현상 금지 (완전 직렬화 가능)
나. 각 DBMS별 격리 수준
- Oracle: Read Committed, Serializable만 제공 (Snapshot Isolation 기반)
- SQL Server: 2PL 기본, Snapshot Isolation은 별도 설정 필요
- PostgreSQL: 기본적으로 MVCC, Repeatable Read=Snapshot Isolation
- MySQL: InnoDB는 기본 Repeatable Read (MVCC), Serializable은 락 기반
1.5 동시성 제어 방식
가. 2PL (Two-Phase Locking)
- 모든 트랜잭션이 확장 단계 (락 획득), 수축 단계 (락 해제) 구분
- 순수 직렬화 보장, 성능 저하, 데드락 발생 가능
나. MVCC (Multi-Version Concurrency Control):
- 읽기/쓰기 간 락 최소화, 트랜잭션별 snapshot/버전 활용
- writer끼리만 충돌, reader-writer/reader-reader는 충돌 없음
1.6 동시성 현상 (Phenomena)
- Dirty Write: 커밋 전 데이터가 다른 트랜잭션에 의해 덮어써짐
- Dirty Read: 커밋되지 않은 데이터를 읽음
- Non-Repeatable Read: 같은 레코드 두 번 읽었을 때 값이 다름
- Phantom Read: WHERE 조건에 부합하는 레코드의 수가 트랜잭션 중간에 달라짐
- Read Skew/Write Skew/Lost Update: MVCC에서만 발생, 여러 행/테이블의 변경이 동기화되지 않음
* 자세한 내용은 https://jaimemin.tistory.com/2694 참고
[RDBMS] 트랜잭션
1. 트랜잭션 개요트랜잭션은 데이터베이스에서 원자적 (Atomic)이고, 일관적 (Consistent)이며, 격리 (Isolated)되고, 영속적 (Durable)인 작업 단위실무에서는 동시 사용자 요청, 서버 장애, 네트워크 오류
jaimemin.tistory.com
2. 비관적 (Pessimistic) Locking
2.1 Locking의 종유와 개념
가. 비관적 락 (Pessimistic Lock)
- 물리적 락
- 실제 DB의 행(row), 페이지, 테이블 등 데이터에 대해 락을 걸어 동시성 충돌을 방지
- 트랜잭션 격리 수준에 따라 자동 발생
- 명시적으로 사용하는 방법: 쿼리에서 직접 FOR UPDATE, FOR SHARE 등으로 지정
나. 낙관적 락 (Optimistic Lock)
- 논리적 락
- 실제 DB 락을 쓰지 않고, 버전 (@Version 필드)이나 타임스탬프 등으로 충돌 감지
- @Version 등 기본기능으로 제공
- 명시적으로 사용하는 방법: LockModeType.OPTIMISTIC_FORCE_INCREMENT 등 사용
2.2 DBMS별 명시적 락 SQL
가. 공유 락 (Shared/Read)
- Oracle: FOR UPDATE (실제로는 배타적 락만 지원)
- MySQL: LOCK IN SHARE MODE
- SQL Server: WITH (HOLDLOCK, ROWLOCK)
- PostgreSQL: FOR SHARE
- DB2: FOR READ ONLY WITH RS
나. 배타적 락 (Exclusive/Write)
- Oracle/MySQL/PostgreSQL/DB2: FOR UPDATE
- SQL Server: WITH (UPDLOCK, HOLDLOCK, ROWLOCK)
2.3 Hibernate/JPA 락 모드
- NONE: 락 없음 (default 값)
- OPTIMISTIC/READ: 버전 체크, 낙관적
- OPTIMISTIC_FORCE_INCREMENT/WRITE: 버전 증가 + 체크
- PESSIMISTIC_READ: 공유 락 (다른 트랜잭션의 배타적 락, 쓰기 막음)
- PESSIMISTIC_WRITE: 배타적 락 (다른 모든 읽기/쓰기 막음)
- PESSIMISTIC_FORCE_INCREMENT: 배타적 락 + 버전 증가
2.4 Predicate Locking과 Table Locking
가. Predicate Locking
- 쿼리 조건에 해당하는 모든 레코드 (혹은 인덱스의 범위)에 락을 거는 방식
- MySQL(REPEATABLE READ), SQL Server, PostgreSQL 등 일부 DB에서 지원
- MySQL은 REPEATABLE READ에서 갭 락 (Gap Lock)까지 적용, INSERT 차단 가능
나. Table Locking
- 일부 DB (Oracle 등)에서 predicate lock 미지원 시 전체 테이블 락
- i.g. LOCK TABLE ... IN SHARE MODE
2.5 FOR UPDATE (NO WAIT/SKIP LOCKED)
가. NO WAIT
- 락 이미 잡혀 있으면 즉시 예외 발생 (대기하지 않음)
- 지원하는 DB: Oracle, PostgreSQL, SQL Server, MySQL(8.0+)
- Hibernate/JPA: setLockTimeout(LockOptions.NO_WAIT)
나. SKIP LOCKED
- 이미 락 잡힌 행은 건너뛰고 나머지 행만 즉시 반환 (큐 처리 등에서 유용)
- 지원하는 DB: Oracle, PostgreSQL, SQL Server, MySQL(8.0+)
- 사용 예시: 여러 워커가 동시에 작업 큐에서 할당받을 때 프로세스/쓰레드 충돌 없이 동시에 안전하게 작업을 분배할 수 있음
- 현재 트랜잭션에서 아직 락이 걸리지 않은 (=다른 워커가 처리 중이 아닌) 행만 읽어서 락을 획득
- 이미 다른 워커가 락을 잡은 행은 "건너뛰고" 다음 행을 선택
2.6 PostgreSQL Advisory Lock
- 세션/트랜잭션 단위의 임의 key 기반 소프트 락
- 실제 행/테이블과 무관, 애플리케이션 레벨의 동시성 제어
- i.g. 특정 리소스별로 임계구역 동기화, 분산 환경에서 다중 노드 동기화 가능
3. 낙관적 (Optimistic) Locking
3.1 낙관적 Locking 원리
- 실제 데이터베이스 락을 잡지 않고, 엔티티에 버전 혹은 타임스탬프 컬럼을 사용하여 동시성 충돌을 감지하는 방식
- @Version 필드를 활용하면, 엔티티를 읽을 때 버전값을 함께 읽고, UPDATE/DELETE 시점에 WHERE 절에 버전값까지 명시
- "id=... AND version=..." 조건
- 누군가 이미 버전을 변경했다면, 해당 쿼리는 업데이트되지 않고 애플리케이션에서는 OptimisticLockException 예외가 발생함
3.2 @Version vs Timestamp
가. 타임스탬프 기반
- DB/시스템 시간 동기화 문제 (동기화, NTP, time zone, leap second 등)로 인해 실무에서는 추천하지 않음
- 각 DBMS별로 정밀도/동작 차이 있음
나. int/short 기반 버전 필드
- 대부분의 엔터프라이즈 환경에서 권장하는 방식
- @Version 필드에 int(4바이트) 대신 short(2바이트)도 사용 가능, 대용량 테이블에서 스토리지 절약 효과
- 트랜잭션이 짧다면 short도 충분
- 하나의 레코드가 64,000번 이상 연속 변경되는 경우는 드묾
3.3 Bulk Update 시 유의사항
- 대량 UPDATE/DELETE 쿼리는 WHERE 절에 반드시 버전 조건을 포함하여야 함
- 그렇지 않으면 동시성 충돌 (lost update)이 방지되지 않음
UPDATE post SET status=?, version=version+1 WHERE status=? AND version=?
3.4 Versionless Optimistic Locking
- Hibernate는 @Version 필드 없이도 @DynamicUpdate와 @OptimisticLocking(type = DIRTY)로 변경된 컬럼만 WHERE 조건에 포함하여 동시성 제어 가능
- 단, 일반적인 상황에서는 버전 필드를 쓰는 것이 더 명확하고 안전함
3.5 실무 적용 팁 및 한계
- 낙관적 락킹은 단일 트랜잭션 내 Lost Update 방지에는 유용한 방식
- 논리적 트랜잭션 (여러 HTTP 요청/폼 제출 등)에도 버전 필드만 잘 사용하면 안전하게 동시성 충돌 감지 가능
- 그러나 대량 업데이트/삭제, 복잡한 집계(aggregate) 변경, 버전 컬럼 없는 엔티티 등은 별도 설계 및 조치 필요
- Aggregate 내부의 모든 변경을 부모 버전에 반영하려면 Hibernate 커스텀 이벤트 리스너, @OptimisticLocking(type = ALL/Dirty), Force Increment 등 고급 기능을 활용해야 함
참고
인프런 - 고성능 JPA & Hibernate (High-Performance Java Persistence)
'DB > JPA' 카테고리의 다른 글
[Hibernate/JPA] 캐싱 (0) | 2025.06.11 |
---|---|
[Hibernate/JPA] Fetching (0) | 2025.06.09 |
[Hibernate/JPA] Batching (0) | 2025.06.04 |
[Hibernate/JPA] Statement (0) | 2025.06.04 |
[Hibernate/JPA] 영속성 컨텍스트 (0) | 2025.06.04 |