Backend Development/SPRING

Spring Transaction

foreverWon 2025. 7. 23. 23:12
목차

    1. 트랜잭션(Transaction)

    • DB 상태를 변화시키는 작업의 논리적 단위
    • 반드시 모두 성공하거나 모두 실패해야 함
    • 데이터 무결성(일관성) 보장과 예외 상황 대비

    트랜잭션 속성: ACID

    • 트랜잭션이 신뢰성과 무결성을 갖추기 위한 핵심 원칙
    속성 의미  목적 실무 포인트
    A Atomicity (원자성) 모두 성공 또는 모두 실패(All or Nothing) 작업 단위 보호
    C Consistency (일관성) 트랜잭션 전후 데이터 일관성(무결성) 유지 데이터 오염 방지
    I Isolation (격리성) 동시에 실행되는 트랜잭션 독립성 유지 동시성 이슈 방지
    D Durability (지속성) 성공된 작업은 영구 보존 장애 발생 시 복구 보장
    • Isolation (격리성)
      • 각 트랜잭션이 서로 영향을 받지 않도록 분리
      • 격리 수준(Isolation Level)
    수준 특징
    READ UNCOMMITTED 다른 트랜잭션의 변경 내용을 읽음 (Dirty Read 허용)
    READ COMMITTED 커밋된 데이터만 읽음
    REPEATABLE READ 같은 데이터를 항상 동일하게 읽음
    SERIALIZABLE 가장 엄격, 트랜잭션 순차 실행
    • 적절한 격리 수준 선택 필요
    • 높은 격리성일수록 성능 저하 주의(SERIALIZABLE은 성능이 가장 낮음
    • Dirty Read
      • 다른 트랜잭션이 아직 commit 하지 않은 데이터를 읽는 것
      • 읽지 말아야 할 데이터를 읽음
    실무 TIP
    - 모든 트랜잭션 작업을 ACID 준수를 전제로 설계
    - 일관성 유지는 DB 설계부터 엄격하게!
    - 참조할 수 있는 값만 FK로 받기

    트랜잭션 상태 변화

    [BEGIN]
       │
       └─> [ACTIVE] ──> [PARTIALLY COMMITTED]
                                │
                   ┌────────────┴─────────────┐
             (예외 없음)                  (예외 발생)
             COMMIT                        ROLLBACK
               │                               │
         [COMMITTED]                    [FAILED / ABORTED]
               │                               │
         [END / 영구 반영]                  [END / 원상복구]

    2. Spring과 트랜잭션

    Spring의 트랜잭션 처리 방식

    1) 선언적 트랜잭션

    • 스프링에서는 AOP(Aspect-Oriented Programming) 기반으로 트랜잭션 관리
    • @Transactional 어노테이션 선언
    • 적용 위치
      적용 위치 의미
      클래스 레벨 모든 public 메서드에 적용
      메서드 레벨 해당 메서드에만 적용 (가장 세밀한 제어 가능)
    • 주요 속성
      속성 설명 자주 사용되는 값
      propagation 전파 방식 REQUIRED (기본값)
      isolation 격리 수준 DEFAULT
      timeout 제한 시간 (초) -1 (무제한)
      readOnly 읽기 전용 여부 true / false
      rollbackFor 롤백 발생 예외 Exception.class
      noRollbackFor 롤백 제외 예외 DataIntegrityViolationException.class
    • 예시
      •  
    @Service
    @RequiredArgsConstructor
    @Transactional
    public class UserService {
        private final UserRepository userRepository;
        private final EmailService emailService;
    
        @Transactional
        public void registerUser(User user) {
            userRepository.save(user);
            emailService.sendWelcomeEmail(user.getEmail());
        }
    
        @Transactional(readOnly = true)
        public User findUserById(Long id) {
            return userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException(id));
        }
    }
    • 예외 발생 시 자동으로 Rollback
    • 성공 시 자동으로 commit
    • @Transactional(readOnly = true)
      • 조회 전용 최적화
    주의
    - 내부 호출, private 메서드는 AOP 적용 X

    2) 프로그래밍 방식 트랜잭션

    • 선언적 트랜잭션이 내부적으로 동작하는 방식

    Spring AOP와 트랜잭션

    1) AOP란?

    • Aspect-Oriented Programming
    • 관심사를 분리하여 관리하는 프로그래밍 방식
    • Spring의 트랜잭션은 AOP 기반 프록시 객체를 통해 관리됨
    • 핵심 용어 정리
      용어  설명 예시
      Aspect 공통 관심사 트랜잭션 관리
      Join Point 특정 시점 메서드 실행
      Advice 특정 Join Point에서 실행되는 코드 트랜잭션 시작/커밋/롤백
      Pointcut Join Point의 집합 지정 @Transactional 달린 메서드
      Proxy 대상 객체를 감싸는 객체, Advice 적용  

    2) Proxy 기반 트랜잭션 처리

    • Spring에서 @Transactional을 통한 선언적 트랜잭션 처리는 프록시 객체를 통해 동작
    • 실제 비즈니스 로직 호출 X, 프록시가 가로채 트랜잭션을 시작하고 종료
    Client
       ↓
    Service(Proxy)
       ↓
    Transaction 시작 / 종료
       ↓
    Real Service(Bean)
    

    3. 트랜잭션 전파 (Propagation)

    • 현재 트랜잭션이 진행 중일 때, 하위 메서드가 새로운 트랜잭션을 사용할지 기존 트랜잭션에 참여할지를 결정하는 속성
    • @Transactional의 propagation 속성으로 설정
    • 트랜잭션마다 각각의 영속성 컨텍스트 존재
      • 병합하지 않으면 각자 commit
      • 병합하면 영속성 컨텍스트를 공유하여 한 번만 commit
    • 주요 전파 옵션
      옵션  설명  사용 예시
      REQUIRED 현재 트랜잭션 있으면 참여, 없으면 생성 대부분의 비즈니스 로직
      REQUIRES_NEW 항상 새로운 트랜잭션 시작 감사 로그, 이력 저장
      SUPPORTS 트랜잭션 있으면 참여, 없으면 비트랜잭션으로 수행 조회, 로깅
      MANDATORY 반드시 기존 트랜잭션 필요, 없으면 예외 발생 상위 트랜잭션 필수 로직
      NEVER 트랜잭션이 있으면 예외 발생, 없으면 비트랜잭션 외부 API 호출
      NOT_SUPPORTED 트랜잭션을 중단하고 비트랜잭션으로 수행 로그 저장 등
      NESTED 중첩 트랜잭션, SavePoint 사용 대용량 처리 중 일부 롤백

    4. 트랜잭션 격리 수준

    • 격리 수준에 따라 한 트랜잭션이 다른 트랜잭션에서 변경한 데이터를 어느 정도까지 볼 수 있는지 결정됨
    • 여러 트랜잭션이 동시에 실행될 때 DB의 일관성과 동시성 제어
    • @Transactional의 isolation 속성으로 설정
    • 주요 문제점
      • 1) Dirty Read : 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 현상
      • 2) Non-repeatable Read
        • 부정합 현상
        • 같은 쿼리를 실행했을 때 이전과 다른 결과가 나오는 현상
      • 3) Phantom Read
        • 일정 범위의 레코드를 두 번 이상 읽을 때 이전에 없던 레코드가 나타나는 현상
        • 스냅샷을 못 읽는 경우 발생
    • 격리 수준
      • READ UNCOMMITTED
        • @Transactional(isolation = Isolation.READ_UNCOMMITTED)
        • 커밋되지 않은 데이터도 모두 조회 가능
        • 속도가 중요한 경우 사용하나, 정확성을 보장하기 어려워 거의 사용 X
      • READ COMMITTED
        • @Transactional(isolation = Isolation.READ_COMMITTED)
        • PostgreSQL 기본값
        • 커밋된 데이터만 읽을 수 있음
        • 트랜잭션 도중 동일 테이터를 다시 조회하면 변경된 값을 볼 수 있음
        • 일반적인 서비스에서 많이 사용
        • Dirty Read 방지
        • Non-repeatable Read, Phantom Read 발생 가능
      • REPEATABLE READ
        • @Transactional(isolation = Isolation.REPEATABLE_READ)
        • 트랜잭션이 시작될 때 스냅샷을 생성하고, 끝날 때까지 동일한 데이터만 조회
        • 트랜잭션 동안 동일 데이터를 반복 조회 시 값이 유지됨
        • 읽기/쓰기 동시 작업 가능
        • Dirty Read, Non-repeatable Read 방지
        • Phantom Read 발생 가능
      • SERIALIZABLE
        • @Transactional(isolation = Isolation.SERIALIZABLE)
        • 누군가가 사용 중이면 모두 기다려야 함
        • 가장 높은 격리 수준, 모든 트랜잭션을 순차 실행처럼 보이게 함
        • Dirty Read, Non-repeatable Read, Phantom Read 모두 방지
        • 교착 상태(데드락) 발생 가능
          • timeout 설정으로 데드락 방지

    5. 트랜잭션 예외 처리

    예외 유형

    • 체크 예외(Exception)
      • 컴파일 시 강제 처리 → throws 또는 try-catch 필수
      • 스프링 트랜잭션: 기본 커밋, 필요 시 rollbackFor
      • 외부 API 연동 실패 → Checked Exception, rollbackFor 필요
      • throw new IOException("파일 입출력 실패");
    • 언체크 예외(RunTimeException)
      • JVM이 강제 처리 X → throws / try-catch 불필요
      • 스프링 트랜잭션: 자동 롤백, 필요 시 noRollbackFor
      • 도메인 오류 → RunTimeException으로 통일, 일관된 롤백 처리 가능
      • throw new IllegalArgumentException("잘못된 입력");

    트랜잭션 롤백 규칙

    예외 유형 기본 롤백 여부 설명
    언체크 예외 (RuntimeException 계열) 롤백 스프링 기본 정책, 런타임 에러는 대부분 롤백 필요
    체크 예외 (Exception 계열) 커밋 (롤백 X) 명시적 지정 없으면 커밋, 외부 의존 로직 등 예상된 상황
    에러 (Error) 롤백 JVM 치명적 에러, 롤백 수행
    • RunTimeException 또는 Error가 발생하면 자동 롤백
    • 체크 예외는 예외처리 대상 X

    활용 코드

    @Service
    public class OrderService {
    
        private final OrderRepository orderRepository;
    
        public OrderService(OrderRepository orderRepository) {
            this.orderRepository = orderRepository;
        }
    
        /**
         * 언체크 예외(RuntimeException) - 스프링 기본 동작: 자동 롤백
         */
        @Transactional
        public void uncheckedRollback(Order order) {
            System.out.println("[언체크 예외 발생 전] 저장 시도");
            orderRepository.save(order); // DB 저장 (하지만 롤백 예정)
    
            // RuntimeException은 기본적으로 롤백 대상
            throw new IllegalArgumentException("RuntimeException - 자동 롤백");
        }
    
        /**
         * 체크 예외(Exception) - rollbackFor 지정으로 롤백 강제
         * 기본적으로 체크 예외는 커밋되지만, rollbackFor 사용 시 롤백 가능
         */
        @Transactional(rollbackFor = Exception.class)
        public void checkedRollback(Order order) throws Exception {
            System.out.println("[체크 예외 발생 전] 저장 시도");
            orderRepository.save(order); // DB 저장 (하지만 롤백 예정)
    
            // 명시적으로 rollbackFor 설정했으므로 롤백됨
            throw new Exception("Checked Exception - rollbackFor로 롤백");
        }
        
        @Transactional
        public void checkedRollback(Order order) {
    		    try {
            System.out.println("[체크 예외 발생 전] 저장 시도");
            orderRepository.save(order); // DB 저장 (하지만 롤백 예정)
    
            // 명시적으로 rollbackFor 설정했으므로 롤백됨
            throw new Exception("Checked Exception - rollbackFor로 롤백");
    	      } catch(Exception.class) {
    			      throw new RuntimeException();
    	      }
        }
    
        /**
         * 언체크 예외(RuntimeException) - noRollbackFor 지정 시 롤백 방지
         * 기본 롤백 대상이지만, noRollbackFor로 커밋 유도
         */
        @Transactional(noRollbackFor = IllegalArgumentException.class)
        public void uncheckedNoRollback(Order order) {
            System.out.println("[언체크 예외 + noRollbackFor] 저장 시도");
            orderRepository.save(order); // DB 저장 (커밋됨)
    
            // RuntimeException인데도 롤백되지 않고 커밋됨
            throw new IllegalArgumentException("RuntimeException - noRollbackFor로 커밋");
        }
    
        /**
         * 체크 예외(Exception) - rollbackFor 미지정 시 커밋 (스프링 기본)
         */
        @Transactional
        public void checkedNoRollback(Order order) throws Exception {
            System.out.println("[체크 예외 발생 전] 저장 시도");
            orderRepository.save(order); // DB 저장 (커밋됨)
    
            // rollbackFor 없으므로 커밋됨
            throw new Exception("Checked Exception - rollbackFor 없으면 커밋");
        }
    
        /**
         * 에러(Error) - 스프링 기본 동작: 자동 롤백
         */
        @Transactional
        public void errorRollback(Order order) {
            System.out.println("[에러 발생 전] 저장 시도");
            orderRepository.save(order); // DB 저장 (하지만 롤백 예정)
    
            // Error는 스프링에서 롤백 대상
            throw new StackOverflowError("Error 발생 - 자동 롤백");
        }
    }

    정리

    예외 유형 기본 동작 rollbackFor noRollbackFor
    RuntimeException 롤백 유지 롤백 방지 가능
    Checked Exception 커밋 롤백 가능 의미 없음

    6. 실무 적용 가이드

    트랜잭션 경계 설정

    • Service 계층에만 @Transactional 선언
    • 조회 전용 메서드는 readOnly = true 적극 사용

    성능 최적화를 위한 트랜잭션 관리

    1) Read Only 속성 활용

    • 트랜잭션 내에서 조회만 수행하는 경우 readOnly = true 사용 → 변경 감지를 막고, Flush / Dirty Checking 생략 → 성능 최적화
      • Dirty Checking (변경 감지)
        • JPA는 트랜잭션 내에서 조회한 엔티티의 상태를 스냅샷으로 기억
        • 트랜잭션 종료 시점에 원본 스냅샷과 변경된 상태의 스냅샷을 비교해, 달라진 값이 있으면 UPDATE 쿼리를 자동으로 날림
        • 이것을 Dirty Cheking이라 함

    2) 트랜잭션 시간 최소화

    • DB Connection 오래 점유 X
    • 외부 API 호출은 트랜잭션 안에 금지

    3) 불필요한 트랜잭션 제거

    상황 트랜잭션 필요 여부
    단순 조회 X (readOnly 사용)
    로그 기록 X (DB 필요 없음)
    외부 시스템 연동 (Kafka 등) X (DB 필요 없음)
    데이터 변경 (Insert/Update) O
    • 습관적으로 모든 Service에 @Transactional을 붙이지 말자!
    실무 TIP
    상황 권장 처리 방식 이유
    조회 전용 @Transactional(readOnly = true) 사용 변경감지 / flush 비활성화,
    성능 최적화    
    DB 저장 / 수정 / 삭제 일반 @Transactional 사용 변경 사항 일관성 보장 필요
    외부 시스템 호출 (Kafka, Email 등) 트랜잭션 사용 금지 DB와 무관,
    롤백 의미 없음    
    로그 출력, 파일 기록 트랜잭션 사용 금지 DB와 무관,
    트랜잭션 자원 낭비    
    트랜잭션 범위가 긴 경우 짧게 분리  
    (저장 따로, 외부 호출 따로) DB Connection 점유 최소화,  
    성능 개선    
    </aside>    

    흔한 실수와 해결방안

    1) 내부 메서드 호출 문제(Proxy 우회)

    • @Transactional은 프록시(AOP) 기반으로 동작하는데, 이때 같은 클래스 내 메서드 직접 호출은 프록시를 거치지 않는다. 그럼 트랜잭션이 적용되지 않고, 예상한 롤백이 동작하지 않는 문제가 발생한다.
    • 해결 방법
      • 구조 분리: 클래스를 분리하고 주입 받아 사용
      @Service
      @RequiredArgsConstructor
      public class OuterService {
          private final InnerService innerService;
      
          public void outer() {
              innerService.inner();
          }
      }
      
      @Service
      public class InnerService {
          @Transactional
          public void inner() {
              throw new RuntimeException("롤백 정상");
          }
      }
      

    2) 트랜잭션 전파 실수(Propagation)

    옵션 설명
    REQUIRED 부모 있으면 합류, 없으면 새로 시작
    REQUIRES_NEW 무조건 새 트랜잭션 시작

    3) 긴 트랜잭션 범위 문제

    • 외부 API 호출할 때 발생
    • 불필요하게 Connection을 오래 점유해 성능저하나 교착상태 위험

    4) 예외 처리 누락으로 인한 롤백 실패

    5) 외부 시스템 트랜잭션 오용

    요약

    문제  원인  해결 방안
    내부 호출 프록시 우회 자기주입 / 분리
    전파 실수 REQUIRED 혼용 REQUIRES_NEW 구분 사용
    긴 트랜잭션 범위 과다 지정 최소화 분리
    예외 누락 체크예외 rollbackX rollbackFor 설정
    • 내부 호출 → AOP 우회 방지 필요 (self-injection or 분리)
    • 전파 옵션 → rollback 범위 명확히 구분
    • 긴 트랜잭션 → 최소한, 외부 호출 제외
    • Exception → rollbackFor로 명확히
    • 구조상 Controller → Service → Repository 유지
    728x90