[Spring] @Transcational

김민범·2024년 11월 8일

Spring

목록 보기
14/29


Spring 을 사용하여 Scheduler API 를 구현하던 중, Service 에서 두개 이상의 repository 메서드 호출이 있을 경우 Transactional 을 사용였고 다음과 같은 피드백을 받았다.

읽기 요청 등 디비를 변경하지 않는 요청과 관련된 경우 Transactional 이 오히려 성능저하를 야기할 수 있다?

위와 같은 의문을 해결하기 위해 트랜잭션에 대해 알아보기로 했다.

@Transactional 애너테이션 정의

@Transactional트랜잭션을 관리하기 위해 Spring 프레임워크에서 제공하는 애너테이션이다. 데이터의 일관성을 유지하고, 작업 도중 오류가 발생했을 때 트랜잭션을 롤백하여 DB 상태를 이전 상태로 복구할 수 있게 한다.

동작 원리

  • 프록시 기반 AOP: @Transactional이 선언된 메서드는 Spring의 AOP 기능에 의해 프록시 객체가 생성된다. 프록시 객체는 실제 객체를 감싸며, 트랜잭션 시작, 커밋, 롤백 같은 트랜잭션 처리를 담당한다.

  • 트랜잭션 경계 설정: 메서드가 호출되면 프록시가 트랜잭션을 시작하고, 메서드가 정상적으로 종료되면 커밋하여 변경 사항을 반영한다. 만약 메서드 실행 중 예외가 발생하면 트랜잭션을 롤백하여 작업을 취소한다.

  • 전파 옵션: @Transactionalpropagation 옵션을 통해 트랜잭션 경계를 제어할 수 있다. 예를 들어, 이미 진행 중인 트랜잭션이 있을 경우 이를 재사용할지, 별도의 트랜잭션을 생성할지를 선택할 수 있다.

사용법

  • 클래스 또는 메서드에 선언: @Transactional클래스나 메서드 레벨에 선언할 수 있다. 클래스에 선언할 경우, 해당 클래스의 모든 메서드가 트랜잭션 내에서 실행된다.

  • 기본 속성:

    @Transactional
    public void someServiceMethod() {
        // 트랜잭션 내에서 실행되는 로직
    }
  • 고급 설정:

    @Transactional(
        propagation = Propagation.REQUIRED, // 기본값: 이미 진행 중인 트랜잭션이 있으면 재사용
        isolation = Isolation.DEFAULT,     // 트랜잭션 격리 수준, 기본값은 데이터베이스 설정을 따름
        timeout = 30,                      // 트랜잭션 최대 실행 시간(초)
        rollbackFor = Exception.class      // 특정 예외 발생 시 롤백 수행
    )
    public void someServiceMethod() {
        // 트랜잭션 내에서 실행되는 로직
    }

서비스, 컨트롤러에 사용되는 예시

  • 서비스에서 사용: 일반적으로 트랜잭션은 서비스 계층에 선언한다. 데이터베이스 작업을 포함한 비즈니스 로직이 서비스 계층에 위치하기 때문이다.

    @Service
    public class PostService {
    
        @Transactional
        public void createPost(Post post) {
            postRepository.save(post);
        }
    }
  • 컨트롤러에서의 사용 지양: 컨트롤러는 요청과 응답을 처리하는 역할을 담당하므로, 트랜잭션 관리가 필요한 경우 서비스 계층에서 트랜잭션을 적용하는 것이 바람직하다. 컨트롤러에 @Transactional을 사용하는 것은 데이터베이스 접근과 비즈니스 로직이 분리되지 않게 하여 유지보수와 확장성에 문제를 일으킬 수 있다.

트랜잭션 사용의 장단점

장점

  1. 데이터 일관성 보장: 트랜잭션을 사용하면 데이터베이스의 모든 변경 작업이 일관성 있는 상태로 유지된다. 예를 들어, 여러 작업 중 하나라도 실패하면 모든 작업이 롤백되어 데이터의 무결성을 유지할 수 있다.
  2. 작업의 원자성 보장: 트랜잭션이 전체 작업을 하나의 단위로 처리하게 하여, 일부 작업만 완료된 상태로 남는 것을 방지한다.
  3. 오류 복구 용이: 오류가 발생했을 때 트랜잭션을 롤백함으로써 데이터베이스 상태를 오류 이전 상태로 손쉽게 복구할 수 있다.
  4. 동시성 제어: 트랜잭션 격리 수준을 통해 여러 트랜잭션이 동시에 수행될 때 경합 및 동시성 문제를 제어할 수 있다.

단점

  1. 성능 저하: 트랜잭션은 리소스를 점유하고, 격리 수준이 높을수록 시스템에 부하를 발생시킬 수 있다. 특히 고성능이 필요한 시스템에서는 부정적 영향을 줄 수 있다.
  2. 복잡한 트랜잭션 관리: 다양한 트랜잭션 속성 및 격리 수준 설정으로 인해 관리 복잡성이 증가할 수 있으며, 잘못된 설정은 예기치 않은 오류를 발생시킬 수 있다.
  3. 락 발생: 트랜잭션 내에서 데이터를 변경하는 경우 락(lock)이 걸려 다른 트랜잭션이 접근하지 못하도록 하므로, 경합 및 대기 시간이 증가할 수 있다.

트랜잭션을 사용하는 경우와 사용하지 않는 경우

예시 1: 은행 계좌 이체

  • 트랜잭션을 사용하는 경우: 계좌 이체는 하나의 트랜잭션 단위로 이루어져야 하므로 송금자의 금액을 차감하고 수취자의 금액을 증가시키는 두 개의 작업이 반드시 원자적으로 이루어져야 한다. 트랜잭션을 사용하면 한쪽 작업이 실패하더라도 전체 작업이 롤백되므로, 잔액이 일관되게 유지된다.
  • 트랜잭션을 사용하지 않는 경우: 계좌 이체와 같이 중요한 작업에서 트랜잭션을 사용하지 않으면, 예를 들어 송금자 계좌에서 금액은 차감되었지만, 수취자 계좌에 금액이 추가되지 않는 불완전한 상태가 발생할 수 있다. 이는 데이터 불일치를 초래하고, 데이터 무결성이 깨지는 결과를 낳는다.

예시 2: 배치 처리 작업

  • 트랜잭션을 사용하는 경우: 특정 배치 작업에서 다수의 데이터를 일괄로 업데이트할 때, 데이터 무결성이 중요한 경우 트랜잭션을 적용하여 작업 전체가 일관되게 수행되도록 한다. 예를 들어, 주문 처리 배치 작업에서 모든 주문 상태가 성공적으로 변경되어야 할 경우 트랜잭션이 필요하다.

  • 트랜잭션을 사용하지 않는 경우: 각 작업이 독립적이고 다른 데이터에 영향을 주지 않는 경우(예: 단순 통계 데이터 집계 등), 트랜잭션을 사용하지 않고 개별 작업이 수행되게 하여 성능을 최적화할 수 있다. 실패한 작업은 추후 다시 시도하거나 로그를 통해 관리할 수 있다.

사용 선택 기준

  • 데이터의 일관성 보장 필요성: 데이터베이스에 여러 단계의 작업을 수행할 때 모든 작업이 원자성을 가져야 하는 경우 @Transactional을 사용한다.

  • 롤백 조건: 특정 예외나 에러 발생 시 트랜잭션 전체를 취소해야 하는 경우 @Transactional을 적용하여 트랜잭션을 롤백할 수 있다.

  • 성능: 트랜잭션 오버헤드를 고려하여 단일 조회 작업 등 트랜잭션이 불필요한 로직에는 @Transactional을 피한다.

  • 동시성 제어: 트랜잭션 격리 수준을 통해 동시성 문제를 제어할 수 있다. 예를 들어, Isolation.READ_COMMITTED를 사용하여 특정 트랜잭션이 커밋한 데이터만 읽도록 제한할 수 있다.

추가적인 설명

  • 트랜잭션 전파 옵션:

    • REQUIRED: 기본 옵션으로, 진행 중인 트랜잭션이 있으면 이를 사용하고, 없으면 새로 시작한다.
    • REQUIRES_NEW: 항상 새로운 트랜잭션을 시작하며, 진행 중인 트랜잭션이 있으면 이를 일시 중지한다.
  • 트랜잭션 격리 수준: 격리 수준은 여러 트랜잭션 간의 데이터 접근 방법을 정의한다.

    • READ_UNCOMMITTED: 가장 낮은 격리 수준으로, 커밋되지 않은 데이터를 다른 트랜잭션에서 읽을 수 있다.
    • READ_COMMITTED: 커밋된 데이터만을 읽을 수 있다.
    • REPEATABLE_READ: 트랜잭션 동안 같은 데이터를 여러 번 조회할 때 일관된 결과를 보장한다.
    • SERIALIZABLE: 가장 높은 격리 수준으로, 트랜잭션이 직렬화된 것처럼 동작하게 한다.

0개의 댓글