Intro.
전 시간에 DB의 양대산맥인 RDB와 NoSQL에 대해서 알아보았다. 오늘은 그중 특히 RDB에서 데이터 무결성을 보장하기 위해 고안된 개념인 ACID와 트랜잭션에 대해서 알아보도록 하겠다. 데이터 무결성에 대해서는 지난 시간에 설명했으니 생략하고 넘어가겠다.
2026.03.22 - [CS/DB] - 📦RDB와 NoSQL
📦RDB와 NoSQL
Intro.어쩌면 다른 분야도 마찬가지겠지만, 소프트웨어에는 만병통치약 같은 기술 스택이나 방법이 존재하지 않는다. 흔히 'No Silver Bullet'이라고 표현된다. 지금까지 그랬고 앞으로도 그럴 것이고
asht1214.tistory.com
ACID 원칙
데이터 무결성을 지키기 위해 데이터베이스는 ACID라는 4가지 핵심 원칙을 따른다.
- Atomicity (원자성): "All or Nothing". 트랜잭션 내의 작업들은 모두 성공하거나 모두 실패해야 한다. 송금 과정에서 출금과 입금은 하나의 묶음으로 처리되어야 하며, 중간에 오류가 나면 아예 처음 상태로 되돌아가야(Rollback) 한다.
- Consistency (일관성): 트랜잭션이 성공적으로 완료되면, 데이터베이스는 항상 일관된 상태를 유지해야 한다. 예를 들어, 계좌 잔액은 마이너스가 될 수 없다는 규칙이 있다면, 트랜잭션 전후로 이 규칙은 무조건 지켜져야 한다.
- Isolation (격리성): 여러 트랜잭션이 동시에 실행될 때, 서로의 작업에 간섭하지 못하도록 격리해야 한다. 내가 송금하는 동안 다른 사람이 내 계좌 잔액을 조회하더라도, 송금 중인 불안정한 상태의 데이터를 보게 해서는 안 된다.
- Durability (지속성): 트랜잭션이 성공적으로 완료(Commit)되었다면, 그 결과는 시스템에 장애가 발생하더라도 영구적으로 보존되어야 한다. 정전이 나더라도 내 송금 내역은 데이터베이스에 안전하게 남아 있어야 한다.
Transaction
그렇다면 이 ACID 원칙을 어떻게 구현할까? 그 수단이 바로 Transaction(트랜잭션)이다. 트랜잭션은 데이터베이스의 상태를 변화시키는 하나의 논리적 작업 단위다. 즉, ACID를 지키기 위해 여러 개의 쿼리를 하나의 작업으로 묶어 주는 울타리라고 생각하면 된다.
- 원자성(A) 구현: 트랜잭션은 COMMIT(완료)과 ROLLBACK(취소)이라는 명령어를 통해 원자성을 보장한다. 울타리 안의 작업이 하나라도 실패하면 전체를 ROLLBACK하여 없던 일로 만든다.
- 일관성(C) 구현: 트랜잭션이 시작되기 전과 끝난 후, DB에 설정된 제약 조건(Constraint, Trigger 등)을 검사하여 일관성을 유지한다.
- 격리성(I) 구현: 트랜잭션은 DB의 Lock(잠금) 메커니즘을 사용하여 다른 트랜잭션이 접근하지 못하게 막음으로써 격리성을 확보한다.
- 지속성(D) 구현: 트랜잭션이 COMMIT되면, DB는 그 결과를 메모리뿐만 아니라 디스크의 로그 파일(Redo Log 등)에 기록하여 지속성을 보장한다.
결국 ACID는 우리가 달성해야 할 목표이고, 트랜잭션은 그 목표를 달성하기 위해 사용하는 도구인 셈이다.
격리 수준(Isolation Level)과 동시성 제어
ACID 중에서도 가장 다루기 까다로운 것이 바로 격리성(Isolation)이다. 완벽한 격리를 위해서는 한 번에 하나의 트랜잭션만 처리하면 되지만, 그러면 성능이 바닥을 칠 것이다. 반대로 성능을 위해 다 같이 처리하게 두면 데이터가 꼬이게 된다. 이 트레이드오프 사이에서 타협점을 찾은 것이 바로 격리 수준(Isolation Level)이다.
|
격리 수준
|
설명
|
발생 가능한 문제
|
|
Read Uncommitted
|
커밋되지 않은 데이터도 읽을 수 있음
|
Dirty Read (잘못된 데이터 읽기)
|
|
Read Committed
|
커밋된 데이터만 읽을 수 있음
|
Non-Repeatable Read (반복 조회 시 결과 다름)
|
|
Repeatable Read
|
트랜잭션 동안 같은 데이터를 읽게 보장
|
Phantom Read (유령 데이터 발생)
|
|
Serializable
|
완벽한 격리. 순차적으로 실행
|
성능 저하, Deadlock(교착 상태) 위험 증가
|
격리 수준을 높일수록 데이터의 정합성은 올라가지만, 성능은 떨어지고 Deadlock(데드락) 같은 문제가 발생할 확률이 높아진다. 데드락은 두 트랜잭션이 서로가 가진 자원(Lock)을 기다리며 무한정 대기하는 상태를 말한다. 좁은 골목길에서 두 대의 차가 마주쳐서 서로 비키라고 빵빵거리기만 하는 상황이다.
트랜잭션이 항상 정답일까?
트랜잭션은 데이터의 신뢰를 보장하는 강력한 무기지만, 남용하면 독이 된다. "일단 트랜잭션으로 감싸면 안전하겠지"라는 생각이 오히려 시스템 전체를 망가뜨리는 경우가 실무에서는 꽤 많다.
트랜잭션 범위가 넓어지면 DB 커넥션을 오래 물고 있는다.
트랜잭션이 시작되면 DB 커넥션 하나를 점유하게 된다. 그리고 그 트랜잭션이 COMMIT 또는 ROLLBACK으로 끝날 때까지 커넥션은 반환되지 않는다. DB 커넥션 풀(Connection Pool)은 무한하지 않다. 트랜잭션 범위를 넓게 잡아서 커넥션 점유 시간이 길어지면 다른 요청들은 커넥션을 받지 못하고 대기하게 된다. 식당에 테이블이 10개뿐인데, 한 팀이 식사를 다 하고도 2시간 동안 수다를 떨며 자리를 안 비워 주는 상황이다.
외부 API 호출이 트랜잭션 안에 들어가면 대참사가 난다
이게 실무에서 가장 흔하게 저지르는 실수다. 주문 처리 로직 중 외부 결제 API(PG사 통신)를 호출하는 부분이 트랜잭션 안에 있다고 가정해 보자. 만약 PG사 API가 3초 동안 응답을 안 준다면? 그 3초 동안 앞서 건드린 데이터에 Lock이 걸려 있게 된다. 외부 통신처럼 내가 제어할 수 없는 작업은 반드시 트랜잭션 밖으로 빼야 한다.
읽기 전용 작업에는 트랜잭션이 과하다.
단순히 게시글 목록을 조회하는 SELECT 쿼리에까지 트랜잭션을 거는 것은 불필요한 오버헤드다. 트랜잭션을 시작하면 DB는 내부적으로 트랜잭션 ID를 할당하고, 격리 수준에 따라 스냅샷을 생성하거나 Lock을 관리하는 등의 추가 비용이 발생한다. 1+1 계산에 슈퍼컴퓨터를 동원하는 격이다.
결론적으로, 핵심은 트랜잭션의 범위를 최소화하는 것이다. 꼭 원자적으로 처리해야 하는 데이터 변경 작업만 트랜잭션 안에 넣고, 외부 호출이나 무거운 비즈니스 로직은 트랜잭션 밖으로 분리해야 한다. "이 작업이 실패했을 때, 앞의 작업도 같이 되돌려야 하는가?"라는 질문에 "예"라고 답할 수 있는 작업들만 하나의 트랜잭션으로 묶는 것이 실무적인 정답이다.
NoSQL에서는 어떨까?
지금까지 이야기한 ACID와 트랜잭션은 주로 관계형 데이터베이스(RDBMS)의 이야기다. 그렇다면 대용량 분산 처리에 특화된 NoSQL은 어떨까?
NoSQL은 완벽한 일관성(ACID)을 조금 포기하는 대신, 가용성과 성능을 선택한 BASE 모델을 따른다.
- Basically Available: 언제든지 시스템은 사용 가능해야 한다.
- Soft state: 외부의 개입이 없어도 상태가 변할 수 있다.
- Eventually consistent: 당장은 아니더라도, '결과적으로는' 일관성이 맞춰진다.
은행 시스템처럼 1원의 오차도 용납되지 않는 곳에는 ACID가 필수적이지만, SNS의 '좋아요' 개수처럼 약간의 지연이나 오차가 치명적이지 않은 곳에서는 BASE 모델이 훨씬 효율적이다. 물론 최근에는 MongoDB나 Couchbase 같은 NoSQL들도 분산 트랜잭션을 지원하며 ACID의 영역을 넘보고 있긴 하다.
결국 완벽한 기술은 없다. 데이터의 성격과 비즈니스 요구사항에 맞춰 RDBMS의 ACID와 NoSQL의 BASE 사이에서 적절한 도구를 선택하는 것이 개발자의 역량이다.
'CS > DB' 카테고리의 다른 글
| 🎛️정규화 vs 반정규화 (0) | 2026.03.22 |
|---|---|
| 🗂️인덱스, 넌 누구니? (0) | 2026.03.22 |
| 📦RDB와 NoSQL (0) | 2026.03.22 |
