@Transacntional

Geun Nam Park·4일 전

Spring Boot

목록 보기
9/9

개요

Spring boot 개발 중 데이터베이스의 값을 변경하는 로직을 작성하게 된다. 이때 데이터의 무결성과 일관성을 보장하기 위해 반드시 사용하는 어노테이션으로 @Transactional이 있다.


@Transactional이란?

@Transactional은 스프링 프레임워크에서 선언적 트랜잭션(Declarative Transaction)을 지원하기 위해 제공하는 어노테이션이다.

트랜잭션(Transaction)은 데이터베이스의 상태를 변화시키기 위해 수행하는 하나의 작업 단위를 뜻한다. @Transactional을 메서드나 클래스에 붙이면, 스프링은 해당 로직이 시작될 때 트랜잭션을 시작하고, 로직이 성공적으로 지나면 Commit(반영), 예외가 발생하면 Rollback(취소)을 자동으로 처리해준다.

트랜잭션의 필요성 예시: 계좌 이체
1단계: A 계좌에서 10,000원을 출금한다. (성공)
2단계: B 계좌에 10,000원을 입금한다. (네트워크 오류로 실패)
⇒ 만약 트랜잭션 처리가 안 되어 있다면 A 계좌의 돈만 사라지는 일이 발생한다.


기본적인 사용 방법

트랜잭션 처리가 필요한 서비스(Service) 계층의 메서드나 Class위에 어노테이션을 붙여주면 된다.

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Transactional
    public void signUp(MemberDto memberDto) {
        // 1. 회원 정보 저장
        memberRepository.save(memberDto.toEntity());
        
        // 2. 가입 환영 쿠폰 발급 (실패 시 회원 저장도 롤백)
        couponService.issueWelcomeCoupon(memberDto.getId());
    }
}

옵션

1. readOnly = true (읽기 전용 트랜잭션)
데이터의 수정 없이 조회만 하는 메서드에는 readOnly = true 옵션을 붙이는 것이 좋다.

@Transactional(readOnly = true)
public MemberResponse findMember(Long id) {
    return memberRepository.findById(id)
            .map(MemberResponse::from)
            .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));
}

장점: 스프링이 하이버네이트(Hibernate)의 스냅샷 저장 및 더티 체킹(변경 감지) 기능을 비활성화하여 성능이 향상되고, 실수로 데이터가 수정되는 것을 방지한다.


2. rollbackFor (특정 예외 설정)
Transactional은 기본적으로 RuntimeExceptionError가 발생했을 때만 롤백을 수행한다.

Exception이나 IOException같은 체크 예외(Checked Exception)가 발생하면 롤백되지 않고 커밋된다.
체크 예외 발생 시에도 롤백하고 싶다면 rollbackFor 옵션을 지정해야 한다.

@Transactional(rollbackFor = Exception.class)
public void businessLogic() throws Exception {
    // 체크 예외가 발생해도 안전하게 롤백됩니다.
}

주의해야 하는 사용법

1. 내부 호출 문제

동일한 Class 내에서 @Transactional이 없는 메서드가 @Transactional이 있는 다른 메서드를 호출하면 트랜잭션이 적용되지 않는다.

public class OrderService {
	// 트랜잭션 없음
    public void startOrder() {
    	createOrder(); // 내부 호출 발생
    }
    
    @Transactional
    public void createOrder() {
    	// ~로직 구현
    }
}

원인: 스프링은 프록시(Proxy) 객체를 이용해 트랜잭션을 처리한다. 외부에서 프록시를 거치지 않고 클래스 내부에서 직접 메서드를 호출하면 프록시 로직이 끼어들 수 없다.

해결: 의존성을 분리하여 다른 서비스 클래스로 메서드를 이동시킨다.


**2. 트랜잭션 내부에서 외부 API 호출** 트랜잭션이 켜져 있는 동안에는 데이터베이스 커넥션(Connection)을 계속 붙잡고 있게 된다. 만약 결제 API, AI API 등 시간이 오래 걸리는 외부 네트워크 통신을 트랜잭션 네부에서 처리하면 커넥션 풀이 마르고, 애플리케이션 전체가 마비될 수 있다.

해결: 외부 API 호출은 트랜잭션 범위 밖으로 분리하고, 순수 데이터베이스 저장/수정 로직에만 @Transactional을 적용하자.

0개의 댓글