Spring ‐ 트랜잭션 - thought-corner/Backend-PlayGround GitHub Wiki
트랜잭션 개념
- 트랜잭션 ACID
- 원자성(Atomicity) : 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 한다.
- 일관성(Consistency) : 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.
- 격리성(Isolation) : 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리해야 한다.
- 지속성(Durability) : 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다.
- 트랜잭션의 격리 수준(Isolation Level)
- READ UNCOMMITED(커밋되지 않은 읽기)
- READ COMMITED(커밋된 읽기)
- REPEATABLE READ(반복 가능한 읽기)
- SERIALIZABLE(직렬화 가능)
1. ACID 원칙의 심화 이해
- 원자성 : All or Nothing. DB는 커밋(Commit)과 롤백(Rollback) 메커니즘을 사용하며, 중간에 오류가 발생하면 이전 상태로 되돌리는 작업을 보장한다.
- 일관성 : 데이터 무결성 제약 조건을 지키는 것이다.
- 격리성 : 성능과 데이터 안정성 사이의 트레이드오프 관계이다. 격리 수준을 높이면 데이터는 안전하지만 동시 처리 성능이 떨어진다.
- 지속성 : 시스템 장애가 발생해도 로그 등을 통해 성공한 트랜잭션의 결과를 복구해내는 능력이다.
2. 트랜잭션 격리 수준(Isolation Level)과 발생 문제
| 격리 수준 (Isolation Level) | Dirty Read | Non-repeatable Read | Phantom Read | 특징 및 주요 DB 기본값 |
|---|---|---|---|---|
| READ UNCOMMITTED | 허용 | 허용 | 허용 | 성능은 가장 좋으나 데이터 부정합 위험이 매우 커 거의 사용 안 함 |
| READ COMMITTED | 방지 | 허용 | 허용 | Oracle, PostgreSQL, SQL Server 등 대부분의 RDB 기본값 |
| REPEATABLE READ | 방지 | 방지 | 허용 | MySQL (InnoDB) 기본값 (MVCC를 통해 Phantom Read 일부 방지) |
| SERIALIZABLE | 방지 | 방지 | 방지 | 가장 엄격하지만 동시 처리 성능이 급격히 떨어져 실무 사용 드묾 |
3. 주요 부정합 문제의 정석 정의
- Dirty Read : 트랜잭션이 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 현상이다. 만약 A 트랜잭션이 롤백되면 B 트랜잭션은 존재하지 않는 데이터를 읽는 셈이 된다.
- Non-Repeatable Read : 한 트랜잭션 내에서 똑같은 SELECT를 2번 실행했을때, 그 사이 다른 트랜잭션이 데이터를 수정/삭제해 결과가 달라지는 현상이다.
- Phantom Read : 한 트랜잭션 내에서 똑같은 범위 조회를 2번 실행했을때, 그 사이 다른 트랜잭션이 데이터를 삽입해 유령처럼 행이 늘어나는 현상이다.
4. 실무 기준
- 격리 수준 선택 : 대부분의 애플리케이션은 READ_COMMITTED만으로 충분하다. 금융 서비스처럼 잔액 계산이 정밀해야 하는 경우엔 REPEATABLE_READ 이상을 고려하거나 비관적 락을 명시하기도 한다.
- MySQL 특수성 : MySQL InnoDB 엔진은 MVCC(Multi-Version Concurrency Control) 기술을 통해 REPEATABLE_READ에서 어느 정도 Phantom Read를 방지하는 강력한 기능을 제공한다.
- 트랜잭션 범위 : 트랜잭션은 최대한 짧게 유지해야 한다. DB 커넥션 점유 시간이 늘어나면 HikariCP의 풀이 고갈되어 시스템 전체 장애로 이어질 수 있다.
데이터베이스 연결 구조와 DB 세션
- DB 서버는 내부에 세션을 만들고 앞으로 해당 커넥션을 통한 모든 요청을 해당 세션을 통해 실행한다.
- 개발자가 클라이언트(WAS나 DB 접근 툴)를 통해 SQL을 전달하면 현재 커넥션에 연결된 세션이 SQL을 실행한다.
- 세션은 트랜잭션을 시작하고 커밋 또는 롤백을 통해 트랜잭션을 종료할 수 있으며, 이후에 새로운 트랜잭션을 다시 시작할 수도 있다.
- 자동 커밋(Auto Commit)
- 각각의 SQL 쿼리가 실행될 때마다 DB가 즉시 COMMIT을 호출하는 모드이다.
- 쿼리가 성공하면 물리적인 DB에 반영되고 실패하면 해당 쿼리만 무효화된다.
- 별도의 커밋 명령어를 내릴 필요가 없어 매우 편리하며, 단순 조회나 단발성 수정에 유리하다.
- 여러 쿼리를 하나의 작업 단위로 묶을 수 없다.
- 기본값 : 대부분의 DB 클라이언트와 라이브러리의 기본 설정이다.
- 수동 커밋(Manual Commit)
- 개발자가 직접 COMMIT이나 ROLLBACK을 호출하기 전까지는 데이터가 임시 상태로 유지되는 모드이다.
- 특징 :
set autocommit false를 설정하는 순간부터 트랜잭션이 시작된다. - 트랜잭션의 원자성을 보장할 수 있다. 여러 작업을 수행한 뒤, 모든 결과가 완벽할 때만 한꺼번에 Commit하거나 문제가 생기면 통째로 Rollback을 할 수 있다.
- 커밋을 잊어버리면 트랜잭션이 열린 상태가 되고 결국 데이터가 반영되지 않는다. 커넥션을 오래 붙잡고 있을 경우 다른 트랜잭션이 해당 데이터에 접근하지 못하는 락 대기 현상이 길어질 수 있다.
DB 락
1. 락(Lock)의 본질과 필요성
- 트랜잭션의 격리성과 원자성을 물리적으로 보장하기 위한 장치이다.
- 수정 중 보호 : 세션이 데이터를 수정할 때, COMMIT 또는 ROLLBACK 전까지 다른 세션이 해당 데이터를 수정하지 못하게 차단하여 Lost Update(업데이트 분실)을 방지한다.
- 조회 시 보호 : 일반적인 조회는 락을 사용하지 않지만, 비즈니스 로직상 조회 시점 데이터가 끝까지 유지되어야 하는 경우 락을 획득한다.
| 종류 | SQL 예시 | 특징 및 용도 |
|---|---|---|
| 배타 락 (X) | SELECT ... FOR UPDATE |
조회 시점에 락을 걸고, 트랜잭션 종료까지 다른 곳의 수정/조회 락 획득을 차단 (가장 강력함) |
| 공유 락 (S) | SELECT ... FOR SHARE |
다른 세션에서 읽기는 가능하지만, 수정은 못 하게 막음 (데이터 정합성 유지용) |
2. 락의 범위(Lock Granularity)
- Record Lock : 특정 Row 하나에만 락을 거는 방식. 가장 정밀하여 동시성이 높다.
- Page/Block Lock : 특정 데이터 페이지 전체에 락을 건다.
- Table Lock : 테이블 전체에 락을 건다. 데이터 구조 변경 시 발생하며, 운영에서는 절대로 해선 안 된다.
- Database Lock : 데이터베이스 전체에 락을 건다. 업데이트나 마이그레이션시 제한적으로 사용한다.
3. 조회 시점의 락(Locking Read)
- 기본적으로 MVCC룰 사용하는 현대 DB에서는 조회가 쓰기를 막지 않지만 필요한 경우에 따라 명시적으로 락을 걸 수 있다.
SELECT FOR UPDATE: 조회한 행에 대해 다른 세션이 수정하거나 락을 거는 것을 방지한다.SELECT FOR SHARE: 다른 세션에서 읽기는 가능하지만 수정은 못하게 막는다.4. 락 사용의 정석 가이드(Best Practices)
- 트랜잭션 범위 최소화 : 락은 트랜잭션이 유지되는 동안 계속 잡히게 된다. 트랜잭션을 짧게 가져가야 락 경합을 줄이고 시스템 전체 성능을 높일 수 있다.
- 교착 상태 방지 : 여러 테이블을 수정할 때, 항상 정해진 순서로 데이터를 수정하는 규칙을 세워야 한다.
- 락 타임아웃 설정 : 락을 얻기 위해 무한정 대기하면 전체 서비스가 마비될 수 있으니
innodb_lock_wait_timeout과 같은 설정을 통해 적절한 시점에 에러를 내고 재시도하게 해야 한다.