데이터를 DB에 저장하는 이유: 트랜잭션
서비스를 개발하면 자연스럽게 데이터를 데이터베이스에 저장하게 되는데 한 번쯤은 아래와 같은 의문이 들 때가 있습니다.
데이터를 파일에 저장할 수도 있지 않을까?
DB를 사용하는 여러 가지 이유가 있겠지만 그중 대표적인 이유는 데이터베이스가 트랜잭션이라는 기능을 지원하기 때문입니다.
트랜잭션은 직독직해하자면 거래라는 뜻이고 해당 기능은 아래에 열거할 ACID 원칙을 따르기 때문에 데이터 정합성 및 안전성을 보장해줘야 합니다.
- 원자성(Atomicity): 트랜잭션 내에서 실행할 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패하도록 처리
- 일관성(Consistency): 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지하며 DB에서 정한 무결성 제약 조건을 항상 만족
- 격리성(Isolation): 트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장하는 것을 의미, 격리성은 동시성과 관련된 성능 이슈로 인해 Isolation Level 설정이 가능
- 지속성(Durability): 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되고 중간에 에러가 발생하더라도 로그 등을 통해 성공한 트랜잭션 내용을 복구할 수 있어야 함
트랜잭션은 위에서 언급한 ACID 중 원자성, 일관성, 그리고 지속성은 확실하게 보장해 주는 반면 격리성의 경우 성능과의 trade-off 문제가 존재하여 사용자가 지정한 트랜잭션 격리 수준을 따릅니다.
트랜잭션 격리 수준(Isolation Level)은 아래와 같습니다.
- Read Uncommitted
- Read Committed
- Repeatable Read
- Serializable
격리성을 완벽하게 수행하기 위해서는 마지막인 Serializable을 선택해야 하지만 해당 옵션을 선택할 경우 동시 처리 성능이 매우 나빠집니다.
즉, 단계가 아래로 내려갈수록 격리성이 높아지나 성능이 안 좋아집니다.
일반적으로는 Read Committed 옵션을 선택하며 Read Uncommitted의 경우 데이터 정합성 원칙에 위배되어 사용하지 않는다고 보면 됩니다.
* 커밋(Commit): 모든 작업이 성공해서 데이터베이스에 정상 반영하는 작업
* 롤백(Rollback): 연계된 작업 중 하나라도 실패하면 작업이 수행되기 이전 상태로 되돌리는 작업
DB 세션
트랜잭션을 이해하기 위해 클라이언트가 DB에 연결하는 과정과 DB 세션에 대해 알아보겠습니다.
- WAS 내 커넥션 풀은 애플리케이션이 로딩되는 시점에 미리 커넥션을 생성하는데 각 connectino마다 DB 서버 내부에 DB 세션을 생성
- 커넥션을 통한 모든 요청은 DB 세션을 통해 처리
- DB 세션은 트랜잭션을 시작하고 SQL을 실행한 뒤 커밋/롤백을 통해 트랜잭션을 종료 (이후에 새로운 트랜잭션 시작 가능)
- 사용자가 커넥션을 닫거나 DBA가 강제로 종료할 경우 세션 종료
위 과정을 도식화하면 아래와 같습니다.
트랜잭션 pseudo code
트랜잭션을 수도 코드로 작성하면 아래와 같습니다.
set autocommit(false);
run sql query;
commit/rollback;
set autocommit(true);
- 자동 커밋 모드는 디폴트로 설정되어 있고 해당 모드에서는 쿼리 실행 직후에 자동으로 커밋을 호출
- 트랜잭션 내에는 여러 쿼리가 호출될 수 있으므로 우리가 원하는 트랜잭션을 수행하기 위해서는 모든 쿼리를 수행 후 커밋을 할지 롤백을 할지 정해야 함
- 따라서, 자동 커밋 모드를 비활성화시키면서 수동 커밋 모드로 변경 필요
- ex) A가 B에게 계좌 이체를 할 때 A 계좌로부터 이체 금액을 출금시키는 쿼리와 B 계좌에 송금시키는 쿼리를 한 번의 작업 내 수행해야 하는데 autocommit 모드가 활성화되어 있는 상태에서 출금하는 쿼리는 정상적으로 실행되었지만 송금하는 쿼리에서 오류가 날 경우 문제가 발생
- A 계좌는 금액이 감소했지만 B 계좌는 금액이 그대로인 상황
- 수동 모드로 전환하여 두 쿼리가 모두 정상적으로 수행되어야 커밋 아니면 롤백하도록 처리해야함
- 커넥션 반납 전 autocommit(true);로 다시 변환 필요
- 커넥션 풀에서 커넥션을 꺼낼 때는 autocommit(true)인 상태로 가정
DB Lock
앞서 언급한 ACID 원칙 중 원자성을 보장하기 위해서는 세션 A가 아직 커밋되지 않은 데이터를 수정 중일 때 세션 B에서 해당 데이터를 동시에 수정할 수 없게 해야 합니다.
데이터베이스에서는 위와 같은 문제를 해결하기 위해 Lock이라는 개념을 제공합니다.
락이 동작하는 과정은 아래와 같습니다.
- 세션 A와 세션 B가 동일한 row에 대한 수정을 진행하고자 할 때 먼저 로우의 락을 획득한 트랜잭션이 먼저 진행 (세션 A가 획득했다고 가정)
- 세션 B는 세션 A의 작업이 끝날 때까지 즉, 락을 획득할 때까지 대기 (무한정 대기는 아니고 설정된 대기시간을 넘길 경우 timeout 발생)
- 세션 A가 작업을 마무리하고 트랜잭션이 종료되어 락을 반환하면 세션 B가 락을 획득하여 트랜잭션 진행
- 세션 B가 작업을 마무리하고 락 반납
이처럼 락은 트랜잭션의 원자성을 보장하기 위해 등장한 개념입니다.
또한, DB Lock은 반드시 update query에만 사용되는 개념이 아닙니다.
조회할 경우에도 "SELECT FOR UPDATE" 구문을 사용하여 락을 획득할 수 있으며 해당 케이스에서도 트랜잭션이 종료되면 락을 반납합니다.
은행이나 증권에서 돈과 관련된 트랜잭션을 진행할 때에는 위에서 언급한 대로 조회 시점에 락이 필요합니다.
참고
스프링 DB 1편 - 데이터 접근 핵심 원리 (김영한 강사님)
'DB > 개념 정리' 카테고리의 다른 글
[용어 정리] DAO, DTO, VO, Mapper, Repository, Entity (0) | 2023.11.06 |
---|---|
[DB] Connection Pool 정리 (0) | 2023.03.20 |
[DB] JDBC 정리 (2) | 2023.03.17 |