본문 바로가기
개발 공부/Database

Trasaction(트랜잭션) 기본적으로 톺아보기

by Hoya324 2025. 1. 16.

들어가기 전..

오늘은 트랜잭션에 대해 톺아보겠습니다. 특히 Spring에서 어떻게 적용되는지를 중점으로 알아보겠습니다.

목차는 다음과 같습니다.

  1. Transaction(트랜잭션)의 이해
  2. Transaction 전파 속성(Propagation)
  3. Transaction 격리 수준(Isolation Level)과 발생 가능한 동시성 문제
  4. MVCC(Multi-Version Concurrency Control) Undo/Redo 로그
  5. 트랜잭션의 동작 과정 예시

Transaction(트랜잭션)의 이해

트랜잭션은 작업을 처리하는 최소 단위로, 데이터베이스에서 데이터를 수정하거나 관리할 때 예외가 발생하는 등의 변수를 고려해 작업의 성공, 실패, 복구를 보장하기 위해 사용됩니다.

트랜잭션은 작업이 성공하면 commit으로 결과를 확정하고, 실패 시 rollback으로 작업을 취소합니다.

예를 들어, 고객이 상품을 구매하면서 포인트를 사용하는 경우, 관련 데이터로 고객 정보, 상품 정보, 포인트 정보가 포함됩니다. 더 나아가 상품 재고 관리, 포인트 차감, 유효성 검증 등 여러 단계가 연관됩니다. 이러한 복잡한 로직에서 데이터의 무결성과 비즈니스 로직의 안정성을 보장하기 위해 트랜잭션이 필요합니다.

트랜잭션은 작업을 하나의 원자적 단위로 묶어 처리하며, 아래의 ACID 원칙을 보장합니다:

  • Atomicity(원자성): 모든 작업이 전부 성공하거나 전부 실패해야 합니다.
  • Consistency(일관성): 작업 완료 후 시스템 상태가 항상 유효해야 합니다.
  • Isolation(고립성): 작업 중간 상태가 외부에 노출되지 않아야 합니다.
  • Durability(지속성): 작업 성공 시, 시스템 장애가 발생하더라도 결과가 유지되어야 합니다.

트랜잭션은 이처럼 비즈니스 로직의 안정성과 데이터 무결성을 보장하는 서비스의 핵심 역할을 합니다.

Transaction 설계시 고려사항

망나니개발자님의 블로그에서 [ 트랜잭션 주의 사항 ] 부분을 정리한 내용입니다.

해당 문제에 대해 작업해본 내용은 해당 글 [DB Connection 점유 줄이기: OSIV와 단계적 CQRS 그리고 트랜잭션 설계 링크] 에서 확인해주세요!

 

만약 아래와 같은 절차대로 작업이 진행된다고 할 때 고려할 점을 알아보겠습니다.

1) 처리 시작
  => 커넥션 풀에서 커넥션 객체 조회
  => 트랜잭션 시작
2) 사용자의 로그인 여부 확인
3) 사용자의 글쓰기 내용의 오류 여부 확인
4) 첨부로 업로드된 파일 확인 및 저장
5) 사용자의 입력 내용을 DBMS에 저장
6) 첨부 파일 정보를 DBMS에 저장
7) 저장된 내용 또는 기타 정보를 DBMS에서 조회
8) 게시물 등록에 대한 알림 메일 발송
9) 알림 메일 발송 이력을 DBMS에 저장
<= 트랜잭션 종료(Commit)
<= 커넥션 반납
10) 처리 완료
출처: <https://mangkyu.tistory.com/288> [MangKyu's Diary:티스토리]

이 예시에서 가장 큰 문제는 트랜잭션이 불필요하게 길어지는 것이 문제입니다. 불필요한 트랜잭션 점유로 인해서 커넥션 풀의 wait이 발생하지 않기 위해서는 ✨최대한 트랜잭션 범위를 적게 잡는 것이 중요합니다.

때문에 이 예시에서는

  1. 5번 전까지는 트랜잭션이 필요없으므로 4번 전까지는 트랜잭션을 시작하지 않는다. (트랜잭션의 시점)
  2. 외부 통신이 걸려있는 8번에서는 트랜잭션 범위를 제거한다. (트랜잭션의 범위 제한)
  3. 5, 6번과 같이 관련있는 트랜잭션은 묶고, 7번이나 9번 같은 독립적인 작업은 트랜잭션을 별도로 가지는 것이 필요하다. (트랜잭션의 범위 구분)

그럼 이번에는 전파 속성에 대해 알아보겠습니다.

전파 속성(Propagation)

트랜잭션 전파 속성은 위의 예시에서 문제점을 해결하기 위한 것처럼 여러 트랜잭션 적용 범위를 하나로 묶어 큰 트랜잭션 경계를 만들거나, 트랜잭션의 진행 방식을 제어하기 위해 사용됩니다.

이때, 물리 트랜잭션과 논리 트랜잭션 개념이 등장하는데 기존의 트랜잭션에 참여한다는 것은 같은 물리 트랜잭션을 공유한다는 것을 의미합니다.

내부의 논리 트랜잭션이 커밋되더라도 마지막 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋되고, 내부 트랜잭션이 하나라도 롤백되면 물리 트랜잭션이 롤백되는 방식을 가지고 있습니다.

 

REQUIRED

  • 기본 전파 속성. 이미 시작된 트랜잭션에 참여하거나 없으면 새로 시작.
  • 다른 트랜잭션 메서드 호출 시 동일 트랜잭션에 묶임.
  • 하나의 물리 트랜잭션

SUPPORTS

  • 기존 트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 진행.
  • 트랜잭션이 없어도 Connection, Hibernate Session 공유 가능.

MANDATORY

  • 기존 트랜잭션이 있으면 참여. 없으면 예외 발생.
  • 독립적인 트랜잭션 진행이 불가능한 경우에 사용.

REQUIRES_NEW

  • 항상 새로운 트랜잭션 시작. 기존 트랜잭션은 보류.
  • 독립적 작업을 수행할 때 사용.
  • 물리 트랜잭션 모두 분리

NOT_SUPPORTED

  • 기존 트랜잭션을 보류하고, 트랜잭션 없이 작업 진행.

NEVER

  • 기존 트랜잭션이 존재하면 예외 발생. 항상 트랜잭션 없이 작업 진행.

NESTED

  • 기존 트랜잭션이 있으면 중첩 트랜잭션 시작.
  • 부모 트랜잭션의 영향을 받지만, 자식 트랜잭션의 롤백은 부모 트랜잭션에 영향을 미치지 않음.

Transaction 격리 수준(Isolation Level)과 발생 가능한 동시성 문제

격리 수준은 동시에 실행되는 여러 트랜잭션 간의 데이터 접근을 제어합니다. 데이터베이스의 동시성 제어일관성 유지에 중요한 역할을 합니다.

이때 MySQL에서는 MVCC가 중요한 역할을 하는데 이는 격리 수준에 대해 먼저 정리하고 각각의 문제점을 MVCC가 어떻게 해결하고 있는지 알아보겠습니다.

 

READ_UNCOMMITTED

  • 설명: 가장 낮은 격리 수준. 한 트랜잭션의 미완료 데이터(커밋되지 않은 데이터)를 다른 트랜잭션이 읽을 수 있음.
  • 장점: 성능이 가장 빠름.
  • 단점: 데이터 일관성이 크게 떨어짐.

발생 가능한 문제:

1. Dirty Read (더티 리드):

  • 한 트랜잭션이 커밋되지 않은 데이터를 다른 트랜잭션이 읽을 수 있음.
  • 트랜잭션이 롤백되면 잘못된 데이터를 읽게 됨.

2. Non-Repeatable Read (반복 불가능 읽기):

  • 동일 데이터를 여러 번 읽을 때 값이 다를 수 있음.

3. Phantom Read (팬텀 리드):

  • 동일 조건으로 쿼리 시 결과의 행 개수가 달라질 수 있음.

이때, MVCC는 적용되지 않으며, 데이터 간섭 문제가 발생할 수 있습니다.

 

READ_COMMITTED

  • 설명: 기본적으로 대부분의 DBMS에서 사용되는 격리 수준. 한 트랜잭션이 커밋한 데이터만 다른 트랜잭션이 읽을 수 있음.
  • 장점: Dirty Read를 방지.
  • 단점: 여전히 데이터 일관성 문제가 발생할 수 있음.

발생 가능한 문제:

1. Non-Repeatable Read (반복 불가능 읽기):

  • 동일 데이터를 읽을 때, 다른 트랜잭션에서 값이 수정되면 결과가 다르게 나타남.

2. Phantom Read (팬텀 리드):

  • 한 트랜잭션 동안 동일 조건으로 조회 시, 다른 트랜잭션에서 새로운 행을 삽입하거나 삭제하면 결과 행 개수가 달라질 수 있음.

MVCC를 활용하여 Dirty Read 문제를 방지합니다. 트랜잭션은 작업 중 커밋된 최신 데이터를 읽으며, 락 없이 읽기 작업을 수행합니다.

 

REPEATABLE_READ

  • 설명: 동일 트랜잭션 내에서 동일 데이터를 반복적으로 읽어도 같은 값을 보장. 다른 트랜잭션에서 읽은 데이터는 수정할 수 없도록 막음.
  • 장점: Non-Repeatable Read 문제 방지.
  • 단점: 팬텀 리드 문제는 여전히 발생 가능.(MySQL의 경우 갭락으로 어느정도 해결)

발생 가능한 문제:

1. Phantom Read (팬텀 리드):

  • 동일 조건으로 데이터를 조회할 때, 다른 트랜잭션이 새로운 행을 삽입하거나 삭제하면 결과가 달라질 수 있음.

MVCC를 통해 Non-Repeatable Read 문제를 방지합니다.

트랜잭션은 작업 시작 시점의 스냅샷 데이터를 읽으며, 다른 트랜잭션의 수정 작업이 영향을 미치지 않습니다.

읽기 작업은 락 없이 수행되지만, 쓰기 작업은 새로운 데이터 버전을 생성합니다.

 

RPEATABLE READ의 격리 수준에서는 내부적으로 트랜잭션 번호를 참고하여 자신보다 먼저 실행된 트랜잭션의 데이터만을 조회합니다.

만약 자신보다 나중에 실행된 트랜잭션에 데이터가 있는 경우 언두 로그(Undo Log)에서 읽게 됩니다. 해당 개념은 아래에서 더 자세히 알아보겠습니다.

“언두 로그에서 데이터를 읽는다면 RPEATABLE READ에서도 그럼 MVCC로 인해 팬텀 리드가 없어야하지 않을까?” 라고 생각할 수 있으나, 여기서 잠금있는 읽기(SELECT FOR UPDATE)의 등장으로 팬텀 리드가 발생하게 됩니다.

또한 잠금있는 읽기는 데이터 조회가 언두 로그가 아닌 테이블에서 수행하기 때문입니다. (참고로 언두 로그는 append only라 잠금이 불가능합니다.)

이러한 이유로 SELECT FOR UPDATE나 SELECT FOR SHARE로 레코드를 조회하는 경우 언두 영역의 데이터가 아니라 테이블의 레코드를 가져오게 되고, 이로 인해 팬텀리드가 발생하는 것입니다.

일반적인 DBMS의 경우 이 상황에서 팬텀리드가 발생하지만 MySQL에서는 이를 방지하기 위한 갭 락(`Gap Lock`)이 존재하여 대부분의 상황에서 팬텀리드를 방지할 수 있습니다.

 

출처: https://medium.com/daangn/mysql-gap-lock-다시보기-7f47ea3f68bc

 

갭락으로 인해 테이블에 데이터가 삽입/삭제되더라도 잠금있는 읽기의 조회 조건에 해당된다면 갭 락이 걸리게 되고, 조건에 맞지 않는 경우 레코드 락, 즉 MVCC에 의해 처리되면서 MySQL에서는 팬텀리드 문제를 거의 해결할 수 있습니다.

 

예외의 경우가 있다면, 갭 락이 걸리기 전에 조건에 들어오는 레코드가 삽입/삭제된 경우(락이 없으면 바로 Commit)라고 볼 수 있습니다.

 

MySQL에서 팬텀리드가 발생할 수 있는 상황을 정리해보면 아래와 같습니다.

  • SELECT FOR UPDATE(잠금 읽기) → SELECT(잠금 없음)
    • 갭락 / 팬텀리드 X
  • SELECT FOR UPDATE**(잠금 읽기)** → SELECT FOR UPDATE
    • 갭락 / 팬텀리드 X
  • SELECT → SELECT:
    • MVCC / 팬텀리드 X
  • SELECT(TX1) → INSERT / DELETE (TX2, 바로 Commit됨) → SELECT FOR UPDATE**(잠금 읽기)** (TX1)
    • 팬텀 리드 O

SERIALIZABLE

 

SERIALIZABLE

  • 설명: 가장 높은 격리 수준. 모든 트랜잭션을 순차적으로 실행하여 동시성을 완전히 차단.
  • 장점: Dirty Read, Non-Repeatable Read, Phantom Read 모든 문제가 방지됨.
  • 단점: 성능이 가장 낮으며, 데드락 발생 가능성 높음.

발생 가능한 문제:

  • 없음. 모든 동시성 문제가 해결됨.

 

정리

격리 수준 Dirty Read Non-Repeatable Read Phantom Read MVCC 적용 여부

READ_UNCOMMITTED 발생 발생 발생 ❌ 적용되지 않음
READ_COMMITTED 방지 발생 발생 ✅ Dirty Read 방지
REPEATABLE_READ 방지 방지 발생 ✅ Non-Repeatable Read 방지
SERIALIZABLE 방지 방지 방지 ✅ 모든 문제 방지

 

동시성 문제 정리

Dirty Read (더티 리드):

  • 커밋되지 않은 데이터를 다른 트랜잭션이 읽음.
  • 예: 트랜잭션 A가 데이터를 수정했지만 아직 커밋하지 않은 상태에서, 트랜잭션 B가 해당 데이터를 읽음. 이후 트랜잭션 A가 롤백되면, 트랜잭션 B는 잘못된 데이터를 기반으로 작업하게 됨.

Non-Repeatable Read (반복 불가능 읽기):

  • 동일 데이터를 읽는 동안 다른 트랜잭션이 데이터를 수정하거나 삭제.
  • 예: 트랜잭션 A가 데이터를 읽고 있는 동안, 트랜잭션 B가 해당 데이터를 수정함. 트랜잭션 A가 데이터를 다시 읽으면 이전 값과 다른 값이 반환됨.

Phantom Read (팬텀 리드):

  • 동일 조건의 쿼리를 실행했을 때, 다른 트랜잭션의 삽입 또는 삭제로 인해 결과 집합의 행 수가 달라지는 현상.
  • 예: 트랜잭션 A가 특정 조건으로 데이터를 조회하는 동안, 트랜잭션 B가 해당 조건에 맞는 새로운 데이터를 삽입하거나 기존 데이터를 삭제. 트랜잭션 A가 동일 조건으로 데이터를 다시 조회하면 결과가 달라짐.

이번엔 MySQL을 기준으로 MVCC가 어떻게 위의 동시성 문제를 해결할 수 있는지 알아보겠습니다.

MVCC(Multi-Version Concurrency Control)

DB에서 가장 중요한 문제가 바로 위에서 언급한 동시성 문제인데, 이를 해결하기 위해서 가장 쉽게 을 활용할 수도 있지만, 을 사용하게 되는 경우 동시 요청 시 처리 속도가 매우 떨어지게 됩니다.

 

따라서 MySQL에서는 MVCC라는 버전 관리 기능을 사용합니다.

MVCC, 다중 버전 동시성 제어는 MySQL에서 InnoDB가 레코드 수준의 트랜잭션을 지원하기 위해 사용되는 기능입니다.

 

이때 MVCC는 동시성을 제어하기 위해 스냅샷을 이용하게 됩니다. 즉, 동시에 일어난 서로 다른 버전을 스냅샷을 통해서 레코드에 대해 여러 버전을 관리하게 됩니다.

이를 통해서 데이터에 대한 변경이 완료되어 Commit되기 전까지의 변경을 다른 사용자가 볼 수 없도록, 즉 위에서 언급한 동시성 문제가 일어나지 않도록 관리하게 됩니다.

스냅샷으로 어떻게 동시성을 제어하는가?

이 질문에 답하기 위해서는 언두 로그(Undo Log), 리두 로그(Redo Log)에 대해 알아야합니다. 둘은 버퍼(메모리) 공간에서 존재하면서 각각의 기능을 수행합니다. 먼저 두 로그가 있는 버퍼

언두 로그(Undo Log)란?

언두 로그는 트랜잭션과 격리 수준을 보장하기 위해 백업해둔 변경 전의 데이터입니다. 즉, 언두 로그는 데이터 조회시에 설정된 격리 수준에 맞는 데이터를 반환하는 역할을 하고, 트랜잭션이 Rollback되는 경우 변경되기 전의 데이터로 돌려놓는 역할을 합니다.

 

언두 로그 동작 방식

예를 들어, 아래와 같은 UPDATE 쿼리가 실행된다고 가정합니다:

UPDATE member SET age = 25 WHERE member_id = 1;

이때 데이터베이스는 커밋 여부와 관계없이 실제 데이터와 버퍼풀(메모리)의 값을 먼저 변경합니다. 그리고 언두 로그에는 변경 전 데이터를 백업합니다.

이후 동작에 따라 커밋되면 현재 상태를 유지하고 롤백되면 언두 로그 데이터를 이용해 이전 상태로 복구합니다.

언두 로그 사용 시 주의 사항

  1. 대량 데이터 변경/삭제: 예를 들어, 100GB 크기의 테이블에서 1억 건 데이터를 삭제하면 언두 로그로 복사되어 추가적인 100GB 공간이 필요하게 됩니다.
  2. 트랜잭션 장시간 유지: 트랜잭션이 오래 유지되면 언두 로그가 계속 쌓여 성능이 저하됩니다. 특히, 다른 쿼리가 언두 로그를 검색하면서 성능이 크게 떨어질 수 있습니다.

따라서 트랜잭션 범위를 최소화하고, 네트워크 요청 같은 외부 작업은 트랜잭션 범위에서 제외하는 것이 중요합니다.

리두 로그(Redo Log)란?

리두 로그는 트랜잭션의 지속성(Durability)을 보장하는 역할을 합니다.

MySQL은 데이터 변경 사항을 리두 로그에 기록해, 서버 비정상 종료 시에도 데이터 일관성을 유지할 수 있도록 합니다.

즉, 커밋된 데이터가 메모리에서만 저장되고 디스크에 기록되지 않은 경우, 리두 로그를 이용해 복구할 수 있고, 롤백되었으나 데이터 파일이 이미 기록된 데이터의 경우도 복구할 수 있습니다.

또한 리두 로그를 통해 트랜잭션이 커밋, 롤백, 또는 실행 중인지 확인합니다.

 

리두 로그 동작 방식

리두 로그는 버퍼 메모리에 저장되고, 일정 주기로 디스크에 동기화됩니다. 이렇게 디스크 작업을 지연시키는 이유는 디스크에 데이터를 즉시 쓰는 경우 랜덤 I/O가 발생해 성능이 저하될 수 있기 때문입니다.

트랜잭션의 동작 과정 예시

MySQL에서 격리 수준이 READ_COMMITTED인 경우, 데이터 변경 처리는 아래와 같은 흐름으로 진행됩니다.

 

1. 데이터 변경데이터는 메모리(버퍼풀)와 디스크에 기록됩니다.

INSERT INTO member(id, name, age) VALUES (1, "Hoya", 24);

 

2. 트랜잭션 시작 후 데이터 수정

  • 버퍼풀 값이 먼저 변경되고, 언두 로그에 변경 전 데이터가 백업됩니다.
  • 디스크에는 백그라운드 쓰레드를 통해 변경 사항이 반영됩니다.
START TRANSACTION; UPDATE member SET age = 25 WHERE id = 1;

 

3. 다른 트랜잭션에서 데이터 조회반환 데이터는 격리 수준에 따라 달라집니다:

  • READ_UNCOMMITTED: 커밋되지 않은 데이터를 반환.
  • READ_COMMITTED 및 그 이상: 언두 로그에 있는 백업 데이터를 반환.
SELECT * FROM member WHERE id = 1;

 

4. 커밋 또는 롤백

  • COMMIT: 변경된 데이터가 영구적으로 저장.
  • ROLLBACK: 언두 로그 데이터를 복구해 이전 상태로 되돌림.

다음 글에서는 Spring에서 트랜잭션이 어떻게 적용되는지에 대해 알아보겠습니다.

Reference

https://medium.com/daangn/mysql-gap-lock-다시보기-7f47ea3f68bc

https://mangkyu.tistory.com/299

https://mangkyu.tistory.com/269

https://mangkyu.tistory.com/169

https://devlog-wjdrbs96.tistory.com/368

https://velog.io/@songsunkook/언두-로그와-리두-로그

https://mangkyu.tistory.com/288

https://aws.amazon.com/ko/blogs/tech/achieve-a-high-speed-innodb-purge-on-amazon-rds-for-mysql-and-amazon-aurora-mysql/?utm_source=chatgpt.com

'개발 공부 > Database' 카테고리의 다른 글

DB | DB Locking과 Optimistic Lock/Pessimistic Lock  (1) 2023.09.20
DB | Join  (0) 2023.09.20
DB | 트랜잭션, 동시성 제어, 회복  (1) 2023.08.28
DB | 정규화  (0) 2023.08.28
DB | 데이터 모델링  (1) 2023.08.28