카테고리 없음

[Spring] 트랜잭션

tkxx_ls 2024. 11. 16. 23:46

1. 트랜잭션 코드와 비즈니스 로직의 결합 문제


다음 코드는 트랜잭션 관리 로직과 비즈니스 로직이 혼재되어 있어 코드의 복잡성과 책임이 증가하는 예입니다.

public void addUsers(List<User> userList) {
    TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
    
    try {
        for (User user : userList) {
            if (isEmailNotDuplicated(user.getEmail())) {
                userRepository.save(user);
            }
        }
        this.transactionManager.commit(status);
    } catch (Exception e) {
        this.transactionManager.rollback(status);
        throw e;
    }
}
 

위 코드에는 두 가지 주요 문제가 있습니다:

  1. 책임 분리 부족: 트랜잭션 관리 로직과 핵심 비즈니스 로직이 혼합되어 있습니다.
  2. 유지보수 어려움: 코드 수정 시 트랜잭션 로직과 비즈니스 로직을 동시에 고려해야 하므로 복잡도가 높아집니다.

 

2. 트랜잭션 코드 분리: AOP 적용


Spring은 이러한 문제를 해결하기 위해 AOP(Aspect-Oriented Programming)를 사용합니다. 트랜잭션 코드와 같은 부가 기능은 핵심 비즈니스 로직 외부로 분리하여 관리할 수 있도록 설계되었습니다. 이를 통해 트랜잭션 관련 코드는 마치 존재하지 않는 것처럼 처리할 수 있습니다.

Spring에서는 이를 위해 @Transactional 어노테이션을 제공합니다. 아래는 트랜잭션 로직을 분리한 리펙링된 코드입니다.

@Service
@RequiredArgsConstructor
@Transactional
public class UserService {

    private final UserRepository userRepository;

    public void addUsers(List<User> userList) {
        for (User user : userList) {
            if (isEmailNotDuplicated(user.getEmail())) {
                userRepository.save(user);
            }
        }
    }
}
  • 트랜잭션 관리 코드 제거: 트랜잭션 시작, 커밋, 롤백 같은 작업은 @Transactional 어노테이션에 의해 자동으로 처리됩니다.
  • 책임 분리: 비즈니스 로직은 핵심 작업만 수행하며, 트랜잭션은 AOP를 통해 관리됩니다.

 

3. 트랜잭션 세부 설정


Spring의 @Transactional 어노테이션은 다양한 트랜잭션 속성을 설정할 수 있습니다. 대표적으로 트랜잭션 전파, 격리 수준, 제한 시간, 읽기 전용과 같은 속성이 있습니다.

 

트랜잭션 전파 (Propagation)

트랜잭션 전파는 이미 진행 중인 트랜잭션이 있을 때 새로운 트랜잭션을 어떻게 처리할지를 정의합니다.

  • PROPAGATION REQUIRED 
    이미 진행 중인 트랜잭션이 있으면 그 트랜잭션에 참여하고, 없으면 새로운 트랜잭션을 시작합니다.
  • ex)  A 작업이 진행 중일 때 B 작업이 시작되면 A와 B는 하나의 트랜잭션으로 묶입니다. A, B 중 하나라도 실패하면 전체 트랜잭션이 롤백됩니다.

 

  • PROPAGATION REQUIRES NEW
    새로운 트랜잭션을 시작하고, 기존 트랜잭션은 일시 정지합니다.
  • ex)  A 작업 중 B 작업이 시작되면 B는 독립적으로 처리됩니다. A가 롤백되더라도 B는 영향을 받지 않습니다.

 

  • PROPAGATION NOT SUPPORTED
    트랜잭션 없이 동작합니다.
  • ex) 단순 조회 작업처럼 트랜잭션이 필요 없는 경우

스프링은 총 7개의 전파 속성을 지원합니다.

1) REQUIRED (기본값)

  • 트랜잭션이 있으면 참여하고, 없으면 새로 시작.
  • 대부분의 트랜잭션 관리에 충분하며 가장 자연스럽고 간단한 방식.

2) SUPPORTS

  • 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 실행.
  • 트랜잭션 여부와 상관없이 로직을 실행할 수 있을 때 적합.

3) MANDATORY

  • 반드시 기존 트랜잭션이 존재해야 하며, 없으면 예외 발생.
  • 독립적으로 실행되지 않아야 하는 로직에 적합.

4) REQUIRES_NEW

  • 항상 새로운 트랜잭션을 시작하며, 기존 트랜잭션은 일시 중지.
  • 독립적으로 실행되어야 하는 로직에 적합.

5) NOT_SUPPORTED

  • 기존 트랜잭션이 있으면 일시 중지하고, 트랜잭션 없이 실행.
  • 트랜잭션이 필요하지 않은 작업에 적합.

6) NEVER

  • 트랜잭션이 진행 중이면 예외 발생.
  • 트랜잭션 없이 실행되어야 하는 작업에 강제 사용.

7) NESTED

  • 기존 트랜잭션이 있으면 중첩 트랜잭션을 시작.
  • 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만, 중첩 트랜잭션 자체는 독립적으로 커밋/롤백 가능.
  • DataSourceTransactionManager만 지원.

 

격리 수준 (Isolation Level)

격리 수준은 트랜잭션이 다른 트랜잭션과 상호작용하는 방식을 정의합니다.

모든 DB 트랜잭션은 격리수준을 가지고 있어야 합니다.

서버에서는 여러 개의 트랜잭션이 동시에 진행될 수 있는데, 모든 트랜잭션을 독립적으로 만들고 순차 진행 한다면

안전하지만 성능이 크게 떨어질 수 밖에 없습니다. 따라서 적절하게 격리수준을 조정해서

가능한 많은 트랜잭션을 동시에 진행시키면서 문제가 발생하지 않도록 제어해야 합니다.

1) DEFAULT

  • 사용 중인 데이터 액세스 기술 또는 DB 드라이버의 기본 설정을 따릅니다.
  • 일반적으로 대부분의 데이터베이스는 READ_COMMITTED를 기본 격리 수준으로 사용합니다.
  • 일부 데이터베이스는 다른 기본값을 가질 수 있으므로, DEFAULT를 사용할 경우 드라이버와 데이터베이스의 문서를 참고하여 기본 격리 수준을 확인해야 합니다.

2) READ_UNCOMMITTED

  • 가장 낮은 격리 수준으로, 커밋되지 않은 변경 사항도 다른 트랜잭션에서 읽을 수 있습니다.
  • 데이터 정확성보다 성능이 절대적으로 중요한 경우에만 신중하게 사용합니다.
  • Dirty Read 현상이 발생할 수 있습니다.

3. READ_COMMITTED

  • 다른 트랜잭션에서 커밋된 데이터만 읽을 수 있습니다.
  • Dirty Read를 방지하지만, Non-Repeatable Read는 발생할 수 있습니다.
  • 하나의 트랜잭션에서 동일한 쿼리를 실행해도 결과가 달라질 수 있습니다.

4) REPEATABLE_READ

  • 트랜잭션 동안 읽은 데이터에 대한 수정을 다른 트랜잭션에서 할 수 없습니다.
  • Non-Repeatable Read를 방지합니다.
  • 새로운 로우의 삽입은 막지 않기 때문에 Phantom Read가 발생할 수 있습니다.

5) SERIALIZABLE

  • 가장 높은 격리 수준으로, 트랜잭션을 순차적으로 실행하여 완벽한 격리를 보장합니다.
  • Dirty Read, Non-Repeatable Read, Phantom Read 모두 방지합니다.
  • 성능이 가장 낮으며, 동시성이 크게 감소합니다.
  • 데이터 무결성이 최우선이며, 성능 저하를 감수할 수 있는 경우에만 사용합니다.

 

 

 

 제한 시간 (Timeout)

트랜잭션 수행 시간의 상한선을 설정합니다.

  • 제한 시간 초과 시 트랜잭션이 강제로 롤백됩니다.
  • 설정 방법: @Transactional(timeout = 5) (단위: 초)

 

 읽기 전용 (Read-Only)

읽기 전용으로 설정하면 트랜잭션 내에서 데이터 변경 시도를 막습니다.

  • 성능 최적화 가능: 데이터베이스 및 Hibernate 캐시에서 읽기 전용으로 동작.
  • 읽기 전용으로 설정하면 데이터 변경을 시도할 경우 예외를 발생시킵니다.


 noRollbackFor

  •  트랜잭션이 롤백되지 않아야 할 예외 클래스를 지정합니다.
  • 기본적으로 예외가 발생하면 트랜잭션은 롤백됩니다.
  • 여기서 지정한 예외는 롤백되지 않도록 예외 처리를 커스터마이징합니다.