개발자공부일기

트랜잭션 본문

CS지식/데이터베이스

트랜잭션

JavaCPP 2025. 11. 18. 17:18

1. 트랜잭션이란 무엇인가?

트랜잭션(Transaction)은

논리적으로 한 번에 처리되어야 하는 작업들의 묶음

이라고 이해하면 된다.

조금 더 풀어서 말하면:

  • 하나의 비즈니스 작업(계좌이체, 주문, 게시글 작성 등)을 수행하기 위해
  • 여러 개의 SQL이 실행될 때
  • 그 전체를 하나의 단위로 묶어서
  • 전부 성공하거나, 전부 실패하도록 만든 것

을 트랜잭션이라고 부른다.

1-1. 계좌이체 예시로 보는 트랜잭션

계좌이체를 예로 들면

 

A가 B에게 10,000원을 송금한다고 했을때 다음과 같은 과정이 논리적으로 한 번에 처리되어야 한다.

  1. A 계좌에서 10,000원 출금
  2. B 계좌에 10,000원 입금

이 두 작업 중 하나만 성공하고 하나는 실패하면 안 된다.

  • 출금만 되고 입금이 안 되면? → 돈이 사라짐
  • 입금만 되고 출금이 안 되면? → 돈이 생겨버림

그래서 DB는 이 두 SQL을 하나의 트랜잭션으로 묶고

  • 둘 다 성공 → commit
  • 중간에 오류 발생 → rollback (둘 다 취소)

이렇게 동작하도록 보장하려고 한다.


2. 트랜잭션의 성질: ACID

트랜잭션이 믿고 쓸 수 있는 단위가 되기 위해 데이터베이스가 지키려고 하는 4가지 성질을 묶어서 ACID라고 부른다.

  • A: Atomicity (원자성)
  • C: Consistency (일관성)
  • I: Isolation (격리성)
  • D: Durability (지속성)

각각을 하나씩 뜯어보자.


2-1. Atomicity (원자성)

원자성은 한 줄로 요약하면

트랜잭션에 속한 작업들은 전부 수행되거나, 전부 수행되지 않아야 한다

이다.

즉, 중간까지 실행된 상태로 남으면 안 된다.

  • 성공 시 → 트랜잭션 안의 변경 내용이 전부 반영
  • 실패 시 → 트랜잭션 안의 변경 내용이 전부 취소(rollback)

구현 관점에서 DB는 다음과 같은 일을 한다.

  • 변경사항을 로그에 기록한다 (redo/undo 로그 등)
  • 트랜잭션 중 오류 발생 시 로그를 기준으로 이전 상태로 되돌린다

계좌이체 예시에서

  • A 계좌에서 돈은 빠졌는데
  • B 계좌 입금에서 에러가 났다

이런 경우, 원자성이 보장된다면

  • A 계좌에서 빠졌던 돈도 롤백되어
  • 트랜잭션 전 상태로 돌아가게 된다.

2-2. Consistency (일관성)

일관성은

트랜잭션이 시작되기 전과 끝난 후에, 데이터는 항상 정의된 규칙을 만족해야 한다

는 성질이다.

여기서 말하는 규칙은:

  • DB 제약 조건들
    • NOT NULL, UNIQUE, PRIMARY KEY, FOREIGN KEY
    • CHECK 제약, 데이터 타입 제약 등
  • 비즈니스 규칙
    • 예: 전체 잔액 합은 0 이상이어야 함
    • 예: 재고는 음수가 될 수 없음

트랜잭션이 끝난 시점에

  • 제약 조건이 깨져 있거나
  • 말이 안 되는 비즈니스 상태가 되면

일관성이 깨졌다고 볼 수 있다.

주의할 점은

  • DB가 제약을 잘 걸어주고
  • 애플리케이션이 이상한 업데이트를 못 하도록 설계해야

일관성이 유지된다는 것.
규칙 자체를 잘못 만들면, ACID가 있어도 결과는 엉망이 될 수 있다.


2-3. Isolation (격리성)

격리성은

여러 트랜잭션이 동시에 실행되더라도, 각각은 마치 혼자 실행되는 것처럼 보이게 해야 한다

는 성질이다.

 

동시에 실행되는 여러 트랜잭션은 서로 영향을 주지 않고 독립적으로 실행되는 것처럼 보여야 한다.

 

실제로는 여러 트랜잭션이 섞여서 실행되지만
결과만 보면 마치 순차적으로 하나씩 실행한 것과 같도록 만드는 것.

이걸 완벽하게 보장하는 것을 직렬화 가능(Serializable)이라고 부른다.
다만 성능을 위해 대부분의 DB는 격리 수준(Isolation Level)을 조정해서
정합성과 성능 사이에서 타협한다.

대표적인 격리 수준은 다음과 같다.

  • Read Uncommitted
  • Read Committed
  • Repeatable Read
  • Serializable

일반적으로

  • Read Committed: 많은 상용 DB의 기본값
  • Repeatable Read, Serializable: 정합성 강하지만 성능 비용 큼

격리 수준에 따라 허용되는 이상 현상의 종류가 달라진다.

  • Dirty Read
  • Non-repeatable Read
  • Phantom Read

이 부분은 따로 “트랜잭션 격리 수준” 주제로 깊게 파면 내용이 꽤 많다.


2-4. Durability (지속성)

지속성은

트랜잭션이 커밋되었다고 응답한 이후에는, 장애가 나더라도 그 결과가 유지되어야 한다

는 성질이다.

다시 말해서:

  • 커밋 성공 응답을 받았는데
  • 서버가 재시작되거나 전원이 나갔더니 데이터가 사라졌다

이런 일이 발생하면 안 된다.

이를 위해 DB는 보통

  • 트랜잭션 로그를 디스크에 기록하고
  • 장애 발생 시 로그를 기반으로 복구 작업을 수행한다

이 과정 덕분에 커밋된 데이터는 장애 이후에도 살아남을 수 있다.


3. 트랜잭션과 Lock(락)의 관계

트랜잭션과 Lock은 실질적으로 세트로 움직인다.

  • 트랜잭션: 논리적인 작업 단위
  • Lock: 그 작업이 진행되는 동안 다른 트랜잭션이 데이터에 함부로 접근하지 못하게 막는 자물쇠

정합성을 지키기 위해서는 실제로 락이 매우 중요하다.


3-1. Lock을 쓰는 이유

예시 상황:

  • 재고가 1개 남은 상품이 있다.
  • 동시에 두 사용자가 주문을 넣었다.

두 트랜잭션을 T1, T2라고 하자.

T1 순서

  1. 재고 조회 → 1
  2. 재고 1 감소 → 0

T2 순서 (거의 동시에)

  1. 재고 조회 → 1
  2. 재고 1 감소 → -1

이런 상황이 발생하면 재고가 음수가 되는 말도 안 되는 상태가 된다.

그래서 DB는 보통:

  • T1이 해당 재고 행에 대한 쓰기 락(배타 락, Exclusive Lock)을 잡고
  • T1 트랜잭션이 끝날 때까지(T1 커밋/롤백 전까지)
  • T2의 재고 수정 요청을 대기시키거나 에러를 발생시킨다.

이렇게 해서 여러 트랜잭션이 동시에 수정해도 데이터가 깨지지 않도록 보호하는 것.


3-2. Lock의 종류 (개념 위주)

DB마다 이름과 디테일은 조금씩 다르지만, 기본 개념은 비슷하다.

  1. 공유 락 (Shared Lock, Read Lock)
  • 데이터를 읽을 때 거는 락
  • 여러 트랜잭션이 동시에 공유 락을 잡고 읽을 수 있다
  • 하지만 공유 락이 걸린 동안에는 쓰기 락(배타 락)을 잡을 수 없다
  1. 배타 락 (Exclusive Lock, Write Lock)
  • 데이터를 변경(UPDATE, DELETE, INSERT 등)할 때 거는 락
  • 한 트랜잭션만 해당 데이터에 배타 락을 잡을 수 있다
  • 배타 락이 걸린 동안 다른 트랜잭션은 보통 그 데이터를 변경할 수 없다
  1. 락의 범위(Granularity)
  • Row Lock: 행 단위
  • Page Lock: 페이지 단위 (여러 행이 모인 단위)
  • Table Lock: 테이블 전체

보통은 동시성을 높이기 위해 가능한 한 작은 단위(row)로 락을 걸고,
필요하면 DB가 더 큰 단위로 락을 승격 시키기도 한다 (Lock Escalation).


3-3. 트랜잭션과 Lock이 함께 만드는 문제들

락이 있다는 건 곧 다음과 같은 문제들도 생길 수 있다는 뜻이다.

  1. 데드락(Deadlock)
  • T1: A 행에 락을 잡은 상태에서, B 행 락을 기다리는 중
  • T2: B 행에 락을 잡은 상태에서, A 행 락을 기다리는 중

서로 상대가 가진 락을 기다리면서 영원히 끝나지 않는 상태 → 데드락.

DB는 보통

  • 데드락을 감지하면
  • 둘 중 하나의 트랜잭션을 강제로 롤백시키고
  • 에러를 애플리케이션에 전달한다.
  1. 락 경합(Lock Contention)
  • 인기 많은 데이터(예: 어떤 특정 설정 테이블)가 자주 업데이트될 때
  • 해당 데이터에 락이 자주 걸려서
  • 다른 트랜잭션들이 계속 대기하게 되는 현상
  • 전체 응답 속도가 떨어지고, TPS가 줄어든다

4. 트랜잭션을 사용할 때 주의할 점

4-1. 트랜잭션은 최대한 짧게

트랜잭션이 오래 유지된다는 말은

그 트랜잭션이 들고 있는 락도 오래 유지된다는 뜻이다.

그 결과:

  • 다른 요청들이 줄줄이 대기하게 되고
  • 데드락 발생 가능성도 올라가고
  • 전체 시스템 응답성이 떨어진다

그래서 실무에서의 기본 원칙은

트랜잭션 안에는 DB 작업만 넣고, 최대한 빨리 끝내라

이다.

특히 다음을 트랜잭션 안에 넣는 것은 위험하다.

  • 사용자의 추가 입력 대기
  • 외부 API 호출 (HTTP 요청 등)
  • 오래 걸리는 연산, 파일 I/O 등

이런 작업은 트랜잭션 바깥에서 처리하거나
DB 트랜잭션과 분리된 별도의 프로세스로 보내는 것이 좋다.


4-2. 트랜잭션 범위를 명확하게

코드 수준에서

  • 어디서 트랜잭션을 시작하고 (begin)
  • 어디서 commit / rollback 하는지

범위를 명확하게 정리해야 한다.

  • 서비스 계층에서 함수 하나가 트랜잭션 단위가 되도록 설계한다든지
  • 프레임워크의 트랜잭션 어노테이션(@Transactional 등)을 어떤 레벨에 붙일지 기준을 정하는 것

범위가 애매하거나 여기저기 흩어져 있으면

  • 디버깅이 어렵고
  • 예기치 못한 락, 정합성 이슈가 생기기 쉽다.

4-3. 격리 수준(Isolation Level) 선택

격리 수준은

정합성을 얼마나 강하게 보장할지 vs 성능을 얼마나 포기할지

를 정하는 스위치라고 보면 된다.

  • Serializable: 가장 엄격, 정합성 최고, 성능 비용 큼
  • Read Committed / Repeatable Read: 현실적으로 많이 쓰는 타협안

실무에서는 보통

  • DB의 기본값(Read Committed 등)에 맞춰서 설계하고
  • 정말 중요한 케이스에서만 추가적인 락, 버전 관리, 비즈니스 로직으로 보완하는 식으로 간다.

핵심은

우리 서비스에서 어느 정도의 일시적인 불일치(예: 목록 조회 시 살짝 늦게 반영되는 것)를 허용할 것인가

를 먼저 정의하고
그에 맞는 격리 수준과 설계를 선택하는 것이다.


4-4. 외부 시스템과의 일관성

트랜잭션 안에서 자주 하는 작업들:

  • 결제 API 호출
  • 이메일 발송
  • 슬랙 알림 전송
  • 메시지 큐(Kafka, RabbitMQ 등)에 메시지 발행

문제는 다음과 같은 상황이다.

  • DB 트랜잭션은 롤백됐는데, 결제는 이미 성공 처리됨
  • DB 저장은 됐는데, 큐 발행이 실패해서 서버 간 데이터가 어긋남

이런 문제를 해결하기 위해

  • Outbox 패턴
  • SAGA 패턴
  • 이벤트 소싱

같은 패턴들을 사용해서

  • DB 안에서는 이벤트 기록만 남기고
  • 별도 프로세스가 그 이벤트를 읽어 외부 시스템과의 일관성을 맞춘다

같은 구조를 많이 쓴다.
핵심은 “DB 트랜잭션 하나로 외부 시스템까지 완벽히 묶는 것은 어렵다”는 점이다.


4-5. 쿼리 튜닝과 인덱스 설계

트랜잭션 안의 쿼리가 느리면

  • 그만큼 트랜잭션이 길어지고
  • 그만큼 락도 오래 잡혀 있게 된다.

트랜잭션 성능 문제 = 쿼리·인덱스 문제인 경우가 매우 많다.

그래서:

  • 자주 업데이트되는 테이블에 불필요하게 큰 범위의 인덱스를 걸어놓지 않는지
  • where 조건에 맞는 적절한 인덱스가 있는지
  • 쿼리 플랜을 확인했을 때 풀 스캔이 남발되지 않는지

이런 부분들을 같이 보는 것이 중요하다.


4-6. 데드락에 대한 대응

데드락은

완전히 없애는 것보다, 발생했을 때 잘 처리하는 것

이 더 현실적이다.

  • DB는 데드락을 감지하면
    • 그 중 하나의 트랜잭션을 강제로 롤백시키고
    • 에러를 던진다
  • 애플리케이션에서는 이 에러를 감지하고
    • 해당 작업을 재시도하거나
    • 사용자에게 적절한 메시지를 보여주도록 처리해야 한다

예방 차원에서는 다음을 신경 쓸 수 있다.

  • 항상 같은 순서로 리소스(테이블, 행)를 접근하기
  • 트랜잭션을 최대한 짧게 유지하기
  • 불필요하게 많은 데이터를 한 번에 잡지 않기

5. 정리

  1. 트랜잭션
    • 논리적으로 하나의 작업 단위
    • 전부 성공하거나 전부 실패해야 한다
  2. ACID
    • Atomicity: 전부 또는 전무
    • Consistency: 제약 조건과 비즈니스 규칙 유지
    • Isolation: 동시에 실행돼도 서로에게 간섭하지 않은 것처럼 보여야 함
    • Durability: 커밋된 내용은 장애가 나도 살아남아야 함
  3. 트랜잭션과 Lock
    • 트랜잭션 중 데이터 무결성을 지키기 위해 DB가 락을 건다
    • 공유 락, 배타 락, 행/페이지/테이블 락 개념
    • 데드락, 락 경합 같은 문제와 직결
  4. 트랜잭션 사용할 때 주의할 점
    • 트랜잭션은 최대한 짧게
    • 범위를 명확히 (서비스 단위 등)
    • 격리 수준과 성능·정합성 밸런스 고려
    • 외부 시스템(결제, 이메일, MQ)과의 일관성은 별도 설계 필요
    • 쿼리/인덱스 튜닝으로 락 유지 시간 줄이기
    • 데드락 발생 시 재시도·에러 처리 전략 준비

 

'CS지식 > 데이터베이스' 카테고리의 다른 글

SQL JOIN  (0) 2025.03.10
무결성  (0) 2025.02.24
ER 모델,Primary Key ,Foreign Key  (0) 2025.02.21
데이터베이스 정규화  (0) 2024.12.03