트랜잭션 by JPA

2024. 12. 17. 23:09·Spring

이 글을 쓰게된 배경

저는 지금까지 스프링부트에서 트랜잭션을 사용할 때 @Transactional 어노테이션을 붙여서 사용했고 어떤 때에 트랜잭션을 사용해야 하는지도 인지하고 사용했습니다. 이번 학기에 데이터베이스설계 과목을 들으면서 트랜잭션과 락에 대한 내용을 공부하면서 트랜잭션에도 제가 알고 있던 것 보다 더 깊은 부분이 있다는 것을 상기시킬 수 있었습니다. 트랜잭션이 단순히 ACID를 보장하는 것 뿐 아니라 더 깊은 수준의 개념들을 스프링부트가 어떤 방식으로 제공하고 있는지에 대한 것을 정확하게 알아야겠다는 생각에 옛날에 봤었던 김영한님의 강의 중 트랜잭션에 관련된 내용을 다시 학습하고 제가 이해한 내용을 바탕으로 이 글을 작성합니다.

트랜잭션이란

트랜잭션은 직역하자면 '거래' 라는 뜻으로 데이터베이스에서 트랜잭션은 하나의 거래를 안전하게 처리해 주는 것을 뜻합니다.

 

트랜잭션이 필요한 상황

트랜잭션은 하나의 작업처럼 이루어져야 하는 상황에 필요하다. 예를 들어 A가 B에게 만원을 입금하는 작업을 한다고 하면 A의 계좌에서는 만원이 차감되고 B의 계좌에는 만원이 입금되어야 합니다. 거래 도중에 무슨 일이 발생해서 거래가 이루어지지 않으면 A와B 사이에 어떤 돈의 이동도 없어야 합니다. 가령 A의 계좌에서는 만원이 차감되었지만 B에 계좌에 만원이 입금되지 않는 일이 발생하면 안됩니다.

자동 커밋과 수동 커밋

자동 커밋과 수동 커밋에 대해 설명하기 전 커밋과 롤백에 대한 개념부터 확실하게 짚고 넘어가겠습니다.

커밋이란?
모든 작업이 성공 성공해서 데이터베이스에 정상적으로 반영되는 것
롤백이란?
작업 중 하나라도 실패해서 거래 이전으로 되돌리는것

 

트랜잭션 ACID

트랜잭션은 ACID를 보장합니다. ACID는 꼭 트랜잭션이 아니더라도 개발 공부를 하다보면 다른 여러곳에서 사용되는 개념이기 때문에 간단히 설명하겠습니다

  • 원자성: 한 트랜잭션은 모두 성공하거나 모두 실패해야됨
  • 일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야한다. 데이터 무결성 만족.
  • 격리성: 여러개의 트랜잭션이 동시에 실행될 때 트랜잭션끼리 서로 영향을 줄 수 없어야 한다.
  • 지속성: 트랜잭션이 끝나면 그 결과가 기록되어야한다. 이미 커밋된 트랜잭션은 돌이킬 수 없다.

트랜잭션 격리 수준

제가 트랜잭션을 공부하면서 가장 궁금했던 것 중 하나입니다. 트랜잭션은 격리 수준에 따라 4가지로 구분되는데 실제로 개발할 때 주로 사용하는 격리 수준은 무엇인지 였습니다.

강의 내용에 따르면 보통 READ COMMITED(커밋된 읽기)와 REPEATABLE READ(반복 가능한 읽기)를 주로 사용한다고 합니다.

많은 데이터베이스가 기본 격리수준으로 READ COMMITED를 제공합니다.

아래는 4가지 격리 수준에 대한 내용입니다.

  • READ UNCOMMITED: 커밋되지 않은 데이터도 읽을 수 있음
  • READ COMMITED: 커밋된 데이터만 읽을 수 있음
  • REPEATABLE READ: 한번 읽은 데이터는 트랜잭션 내에서 다시 호출할 때 같은 결과를 보장
  • SERIALIZABLE: 직렬화 가능

위로 갈수록 성능은 좋지만 격리성이 떨어지고 아래로 갈수록 성능은 떨어지지만 격리성이 좋습니다. 

자동 커밋과 수동 커밋

자동 커밋을 사용하면 쿼리 실행 직후 자동으로 커밋을 호출하는 것입니다. 스프링에서는 기본으로 자동커밋이 적용됩니다. 그렇기 때문에 보통 수동커밋으로 설정할 때 트랜잭션을 시작한다고 표현합니다. 자동 커밋이라고 해서 트랜잭션이 없는 것이 아니고 쿼리 하나하나 마다 작은 트랜잭션이 시작되었다가 쿼리가 끝날 때 종료되는 것입니다.

락

두 개의 트랜잭션에서 하나의 데이터를 동시에 수정하려고 하면 데이터 무결성이 깨집니다. A라는 통장에 대해서 B 트랜잭션은 A의 통장에 있는 10만원 중 5만원을 쓰려고하고 C 트랜잭션은 5만원을 입금하려고 할 수 있습니다. 이때 B와 C가 동시에 일어나게 되면 B에는 통장에 남은 금액이 5만언이고 C트랜잭션에서는 통장에 남은 금액이 15만원이 됩니다. 서로 다른 트랜잭션이 어떤 작업을 수행했는지 모르기 때문에 이와 같은 일이 발생합니다. 이렇게 됐을때 5만원과 15만원 중 어떤 금액을 커밋해야하는지에 대한 문제가 발생합니다.

이때 사용할 수 있는 것이 락입니다. 락은 데이터를 수정하기 전에 내가 이 데이터를 변경하고 있으니 다른 곳에서 사용하지 말라는 의미로 걸어놓는 것입니다. 이렇게 락을 사용하고 트랜잭션이 끝날 때 락도 반납하는 형식으로 사용하는 것입니다. 다른 트랜잭션은 락을 무한정 기다리는 것이 아니라 락 대기시간이 지나면 락 타임아웃 오류를 발생시킵니다. 락 대기시간은 설정할 수 있습니다.

락은 일반적으로 조회에는 사용하지 않지만 조회된 데이터를 가지고 변경이 필요한 작업을 해야할 때 'select for update'문으로 조회락을 사용할 수 있습니다.

트랜잭션 적용

트랜잭션 추상화

트랜잭션을 실제로 적용하려면 서비스 코드에서 데이터 접근 기술(JDBC, JPA 등)을 사용해야 하는 문제가 생깁니다. 이게 왜 문제가 되냐면 데이터 접근 기술을 변경했을 때 데이터 접근 계층만 변경하는 것이 아닌 서비스 계층도 변경해야 하는 상황이 생기기 때문입니다. 이는 계층별로 분리가 잘 되어있지 않는 구조입니다.

 

이를 해결하기 위해서는 트랜잭션을 추상화 시키는 것이 좋습니다. 스프링에서는 PlatfromTransactionManager라는 인터페이스와 각 데이터 접근 기술에 맞는 구현체까지 제공해줍니다.

트랜잭션 동기화

트랜잭션을 유지하려면 트랜잭션의 시작부터 끝까지 같은 커넥션을 유지시켜야 합니다. 이를 위해 스프링은 트랜잭션 동기화 매니저 라는 것을 제공합니다. 이것은 쓰레드 로컬을 사용해서 커넥션을 동기화 시켜줍니다.

쓰레드 로컬이란?
각 쓰레드가 독립적으로 값을 저장하고 관리 할 수 있도록 지원하는 기술입니다. 동일한 변수를 여러 쓰레드가 사용하더라도 각 쓰레드 에서 독립적인 값을 사용할 수 있습니다.

 

동작 과정

  1. 트랜잭션을 시작할 때 트랜잭션 매니저는 데이터소스를 통해 커넥션을 만들고 트랜잭션을 시작합니다.
  2. 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 저장합니다.
  3. 리포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용합니다.
  4. 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고 커넥션을 닫습니다.

트랜잭션 템플릿

트랜잭션을 사용할 때 서비스 계층에서 반복적으로 커밋하거나 롤백하는 트랜잭션을 처리하는 로직이 반복해서 들어갈 수 밖에 없습니다. 서비스 계층을 완전히 순수하게 비즈니스 로직만 두기 위해서 스프링이 트랜잭션 템플릿 이라는 것을 제공해서 반복되는 코드를 제거할 수 있도록 합니다.

 

트랜잭션 AOP

어쨋든 서비스계층에서 트랜잭션을 사용하기 위해서는 트랜잭션 매니저를 사용하는 등의 트랜잭션 관련 코드가 조금이나마 들어갈 수 밖에 습니다. 서비스 계층에서 트랜잭션 관련 코드를 완전히 제거하기 위해선 스프링 AOP 기술을 트랜잭션에 활용 하는 방법을 사용할 수 있습니다.

프록시를 사용하면 트랜잭션 처리 관련 코드를 트랜잭션 프록시에 둘 수 있기 때문에 서비스 계층에는 비즈니스 로직만 둘 수 있습니다. 

트랜잭션 AOP를 사용하기 위해서는 @Transactional 어노테이션을 붙여주면 됩니다.

 

@Transactional 주의점

1. 트랜잭션의 동작 범위

한 클래스 내에 a라는 메소드에서 b라는 메소드를 호출할 때가 있습니다. a에 트랜잭션을 적용하고 있더라도 a와b는 같은 트랜잭션으로 묶이지 않습니다. 이걸 같은 트랜잭션으로 묶으려면 b를 다른 클래스(빈)으로 분리해야 된다고 합니다. 스프링 AOP가 객체를 생성하고 호출하는 방식과 관련이 있는 듯 한데 처리되는 과정까지는 공부가 조금 더 필요할 것 같습니다. 일단은 하나의 비즈니스 로직이라면 메소드를 분리하지 않고 하나의 메소드 안에서 처리하는 방법을 사용하도록 해야겠습니다.

2. 예외 처리

트랜잭션 내에서는 런타임예외가 발생할 때만 롤백을 진행합니다. 언체크 예외가 발생하는 경우에는 @Transactional(rollbackFor = Exception.class) 를 사용해야 합니다

 

주의점은 아니지만 성능 향상을 위해서 읽기 전용 작업에는 @Transactional(readOnly = true)를 설정하면 좋습니다.

 

추가(2025.05.29)

AOP를 학습한 후 트랜잭션의 동작범위에 대해서 새로 알게된 내용입니다. a와 b가 한 클래스 내에 있을때 트랜잭션이 전파되지 않는 이유는 같은 클래스의 메소드 호출은 프록시 객체를 타지 않기 때문입니다. 프록시 객체를 타지 않으면 AOP가 적용되지 않기 때문에 @transactional이 호출 자체가 안됩니다. 대신 a와 b가 다른 빈에 존재할 때는 트랜잭션 전파가 가능한데 트랜잭션 전파에도 여러가지 방법이 있습니다.

트랜잭션 전파 옵션

전파 옵션 설명 주 용도
REQUIRED (기본값) 기존 트랜잭션이 있으면 참여, 없으면 새로 생성 대부분의 서비스 로직
REQUIRES_NEW 기존 트랜잭션 중단, 항상 새 트랜잭션 생성 로그 저장, 알림 전송 등 독립 처리 필요 시
NESTED 기존 트랜잭션 내에 Savepoint 설정 후 실행 부분 롤백이 필요한 경우
SUPPORTS 트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 실행 트랜잭션 있어도 없어도 되는 단순 조회
NOT_SUPPORTED 기존 트랜잭션을 중단하고 트랜잭션 없이 실행 외부 API 호출 등 DB 락 없이 실행해야 할 때
MANDATORY 반드시 기존 트랜잭션이 있어야 실행 (없으면 예외) 반드시 트랜잭션 내부에서만 실행되어야 할 때
NEVER 트랜잭션이 있으면 예외 발생 트랜잭션 환경에서 실행되면 안 되는 작업

 

'Spring' 카테고리의 다른 글

스프링 부트에서 예외와 처리 방법  (2) 2024.12.30
Spring Security 필터에서 발생한 인증/인가 예외 처리하는 방법  (5) 2024.12.30
커넥션 풀 vs 복잡한 로직 시간 비용  (3) 2024.12.22
연관관계 매핑이 꼭 필요한가  (2) 2024.12.16
Spring Boot에서 Querydsl 사용  (2) 2024.12.04
'Spring' 카테고리의 다른 글
  • Spring Security 필터에서 발생한 인증/인가 예외 처리하는 방법
  • 커넥션 풀 vs 복잡한 로직 시간 비용
  • 연관관계 매핑이 꼭 필요한가
  • Spring Boot에서 Querydsl 사용
onetaek
onetaek
finding-scholarship.vercel.app
  • onetaek
    원택투택
    onetaek
  • 전체
    오늘
    어제
    • 전체 (23)
      • Spring (13)
      • Docker (1)
      • Redis (3)
      • Study (6)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 숨은 장학금 찾기 사이트
  • 공지사항

  • 인기 글

  • 태그

    RDMBS
    Spring
    컴퓨터 구조
    No SQL
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
onetaek
트랜잭션 by JPA
상단으로

티스토리툴바