개발천재

[Spring Boot] @Transactional, 스프링에서 트랜잭션을 다루는 방법 본문

개발 준비/Spring Boot

[Spring Boot] @Transactional, 스프링에서 트랜잭션을 다루는 방법

세리블리 2025. 2. 26. 22:05
반응형

@Transactional 이해하기

@Transactional은 주로 데이터베이스 작업에서 일관성과 무결성을 보장하기 위해 사용되는 어노테이션이다. 이 어노테이션을 메서드나 클래스에 적용하면, 해당 메서드나 클래스 내에서 수행되는 데이터베이스 작업이 하나의 트랜잭션으로 묶인다. 트랜잭션은 여러 개의 데이터베이스 작업을 하나의 단위로 묶어서 실행되는데, 모든 작업이 성공적으로 완료되면 데이터베이스에 반영되고, 만약 하나라도 실패하면 모든 작업을 취소하고 원래 상태로 되돌리게 된다.

 


 

@Transactional 핵심 개념 (ACID)

트랜잭션은 ACID라는 네 가지 특성을 가진다.

Atomicity (원자성)

트랜잭션 내의 모든 작업은 하나의 단위로 처리된다. 모든 작업이 성공하면 커밋되고, 실패하면 모두 롤백된다.

 

예를 들어 은행에서 100,000원을 송금하는 트랜잭션을 시작한다고 가정해보자. 
첫 번째로 송금하는 사람의 계좌에서 100,000원을 차감해야하고 두번째로는 수신자의 계좌에 100,000원을 입금해야 한다. 만약 첫 번째 작업은 성공했지만 두 번째 작업에서 오류가 발생하면, 송금이 반쪽만 이루어진다. 이때 원자성 덕분에 두 작업이 모두 성공하거나 모두 실패해야 하므로, 두 번째 작업에서 오류가 나면 첫 번째 작업도 롤백되어 송금이 취소된다.

Consistency (일관성)

트랜잭션이 시작되기 전과 후의 데이터가 일관된 상태를 유지해야 한다.

 

두 명의 사람이 동시에 같은 항목을 구매하는 온라인 쇼핑몰이 있다. 예를 들어, 상품의 재고가 1개뿐인데 두 명이 동시에 그 상품을 구매하려고 한다. 트랜잭션이 시작되기 전에 재고는 1개인데 트랜잭션이 끝났을 때 상품 재고는 0개여야 하고, 추가로 재고가 차감된 상태여야 한다. 만약 두 명이 동시에 구매할 수 있게 트랜잭션이 관리되지 않으면 재고가 -1개가 될 수 있고, 이는 일관성이 깨진 상태이다. 일관성을 보장하려면 트랜잭션이 진행되면서 재고의 상태가 규칙을 벗어나지 않도록 해야 한다.

Isolation (격리성)

각 트랜잭션은 다른 트랜잭션에 영향을 미치지 않도록 독립적으로 실행된다. 즉, 트랜잭션이 진행 중일 때 다른 트랜잭션이 그 데이터를 수정할 수 없다.

 

두 사람이 동시에 은행 계좌에서 돈을 출금하는 트랜잭션을 실행한다고 가정해 보자. 사용자 A가 10만 원을 출금하려고 시도하고, 사용자 B도 동일한 계좌에서 5만 원을 출금하려고 시도한다.
격리성을 보장하면, 트랜잭션 A와 B는 서로 독립적으로 실행되어 각 트랜잭션이 완료될 때까지 다른 트랜잭션의 영향을 받지 않는다. 예를 들어, A가 먼저 실행되고 나서 B가 실행되도록 하여 출금 금액이 정확하게 반영되도록 할 수 있다. 이와 달리 트랜잭션이 제대로 격리되지 않으면, 두 트랜잭션이 동시에 계좌 잔액을 변경하여 오류를 일으킬 수 있다.

Durability (영속성)

트랜잭션이 성공적으로 완료되면, 그 결과는 영구적으로 데이터베이스에 저장된다. 시스템 장애가 발생해도 데이터는 유지된다.

 

사용자가 온라인 쇼핑몰에서 주문을 완료하고 결제도 성공적으로 진행했다고 가정해 보자. 트랜잭션이 완료되면, 주문 내역과 결제 정보는 데이터베이스에 저장된다.
만약 트랜잭션이 완료된 직후 서버가 다운되더라도, 결제 정보는 영속적으로 데이터베이스에 저장되어 있기 때문에 주문은 잃어버리지 않는다. 영속성 덕분에 한 번 완료된 트랜잭션의 결과는 데이터베이스에 계속 저장되고, 시스템 오류나 서버 다운이 발생해도 잃어버리지 않도록 보장된다.

 


 

@Transactional 동작원리

① 트랜잭션 시작

@Transactional이 적용된 메서드가 호출되면, Spring은 트랜잭션을 시작한다.

② 데이터베이스 작업 실행

트랜잭션 내에서 여러 데이터베이스 작업이 이루어진다. 예를 들어, 데이터 삽입, 수정, 삭제 등의 작업이다.


③ 트랜잭션 커밋

메서드가 정상적으로 종료되면, 트랜잭션 내의 모든 작업이 커밋되어 데이터베이스에 반영된다.

④ 트랜잭션 롤백

만약 메서드 실행 중 예외가 발생하면, 트랜잭션은 롤백되어 모든 변경 사항이 취소된다. 즉, 데이터베이스 상태가 트랜잭션을 시작하기 전 상태로 되돌려진다.

아래의 예시에서 @Transactional이 붙은 updateUserInfo 메서드는 userRepository를 통해 사용자 정보를 수정한다. 만약 newName이 null인 경우, 예외가 발생하고 트랜잭션은 롤백되어 사용자 정보는 수정되지 않는다.

@Service
public class UserService {

    @Transactional
    public void updateUserInfo(Long userId, String newName) {
        User user = userRepository.findById(userId);
        user.setName(newName);
        userRepository.save(user);
        
        // 예시로 의도적으로 예외를 발생시킬 수 있음
        if (newName == null) {
            throw new IllegalArgumentException("Name cannot be null");
        }
    }
}

 

 


 

 

롤백 규칙

기본적으로 @Transactional은 런타임 예외 (RuntimeException) 또는 Error가 발생하면 롤백한다. 반면, 체크된 예외 (Checked Exception)가 발생해도 롤백되지 않는다. 이를 변경하고 싶다면, rollbackFor 속성을 사용할 수 있다.

@Transactional(rollbackFor = Exception.class)
public void someMethod() throws Exception {
    // 예외가 발생하면 롤백
}


이와 같이 @Transactional을 사용하면 여러 개의 데이터베이스 작업을 안전하게 묶을 수 있고, 트랜잭션의 특성 덕분에 데이터의 무결성과 안정성을 보장할 수 있다.

 

@Test에서 @Transactional을 사용하면 자동으로 롤백

기본적으로 @Test + @Transactional을 사용하면 테스트 후 롤백된다. 만약 테스트 데이터를 유지하고 싶다면  @Rollback(false)을 추가해야 한다. 또한 DB 상태를 유지해야 하는 테스트라면 @Transactional을 제거하고 직접 정리하는 게 좋다.

테스트 실행 전, 트랜잭션이 시작되고, 테스트 실행 중에 DB에 INSERT, UPDATE, DELETE 등 실행이 된다(DB 반영). 그리고 테스트가 끝나면 자동으로 롤백된다.(DB에 반영되지 않음)

 

아래 코드를 보면 userRepository.save(user); 실행 후 DB에는 데이터가 저장된다. 하지만 테스트가 끝난 후 자동으로 롤백되므로 실제 DB에는 반영되지 않는다.

@SpringBootTest
@Transactional  // 테스트가 끝나면 자동 롤백됨!
class UserRepositoryTest {
    
    @Autowired
    private UserRepository userRepository;

    @Test
    void 사용자_추가_테스트() {
        // Given
        User user = new User();
        user.setName("테스트 유저");
        user.setEmail("test@example.com");

        // When
        userRepository.save(user);

        // Then
        long count = userRepository.count();
        assertThat(count).isEqualTo(1); // 테스트 중에는 데이터가 있음
    }
}

 

만약 테스트에서 데이터가 유지되길 원한다면, @Rollback(false)을 추가하면 된다.

@Test
@Rollback(false) // 롤백하지 않음 → 데이터가 DB에 실제로 반영됨
void 사용자_추가_테스트() {
    User user = new User();
    user.setName("테스트 유저");
    user.setEmail("test@example.com");

    userRepository.save(user);
}
반응형