Language/Java

[Spring] 트랜잭션 정리

공부좀하시졍 2025. 10. 16. 11:19

👀 서비스 로직을 개발할 때 여러 작업을 하나로 묶어서 모두 성공하거나 모두 실패하게 처리해야 할 때가 있다.

ex) 회원가입과 동시에 포인트를 적립하는 상황에서 회원만 등록되고 포인트 적립이 실패하면 안됨

 

👉 이러한 문제를 해결하기 위해 트랜잭션을 사용한다.


1️⃣ 스프링 트랜잭션의 핵심: AOP 프록시 + @Transactional

 

스프링에서 @Transactional 을 메소드에 붙이면, 해당 메소드는 프록시(proxy) 객체를 통해 호출된다. 이 프록시는 메소드 실행 전후에 트랜잭션의 시작, 커밋, 롤백을 자동으로 처리해준다.

 

🤷‍♀️ 프록시란?

진짜 객체 대신 앞에서 대리로 행동하는 가짜 객체

ex) 직접 은행에 가서 돈을 인출하는 대신 나 대신 일을 처리해주는 직원(프록시)에게 부탁할 수 있음

 

✅ 동작 흐름

 

[Service 메소드 호출]
       ↓
 [스프링 프록시 객체]
   ├─ 트랜잭션 시작
   ├─ 비즈니스 로직 실행
   └─ 예외 없으면 커밋 / 예외 발생 시 롤백
       ↓
[실제 로직 실행 완료]

 

✅ 예시 코드

@Service
public class UserService {

    @Transactional  // 트랜잭션 시작~종료를 자동으로 처리
    public void registerUser(User user) {
        userRepository.save(user);             // 1. DB insert
        pointService.addWelcomePoint(user);    // 2. 포인트 적립
    }
}

👉 registerUser() 안에서 예외가 발생하면, DB insert까지 포함해서 모두 롤백된다.

@Transactional 이 없는 상태에서 addWelcomePoint()에서 예외가 발생하면 이미 커밋된 1번 insert는 그대로 DB에 남는다.

 

2️⃣ @Transactional 기본 옵션 정리

옵션 기본값 설명
propagation REQUIRED 트랜잭션 전파 방식 (기존 트랜잭션 참여 , 없으면 새로 생성)
isolation DB 기본 동시에 실행될 때 데이터 격리 수준
rollbackFor RuntimeException 어떤 예외가 발생하면 롤백할지 지정
readOnly false 읽기 전용 여부 (성능 최적화용)
timeout -1 트랜잭션 최대 지속 시간 (초 단위)

 

3️⃣ 트랜잭션 전파 (Propagation)

이미 트랜잭션이 진행 중일 때, 새로 호출되는 메소드가 어떤 방식으로 참여할지 결정

옵션 설명 트랜잭션 존재여부에 따른 동작 사용 예시
REQUIRED (Default) 기존 트랜잭션이 있으면 참여, 없으면 새로 시작 있으면 참여 / 없으면 새로 생성 서비스 계층의 메소드 대부분
REQUIRES_NEW 항상 새 트랜잭션을 시작, 기존 트랜잭션은 일시 중단 무조건 새로 시작 로그 저장, 이메일 발송 이력 등 메인 트랜잭션과 분리해 실패해도 남겨야 하는 작업
NESTED 기존 트랜잭션 내부에 저장점(savepoint) 생성 후 부분 롤백 가능 있으면 저장점 생성 / 없으면 새 트랜잭션 메인 처리는 유지하되, 일부 보조 로직만 실패 시 되돌리고 싶은 경우
SUPPORTS 트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 실행 있으면 참여 / 없으면 비트랜잭션 조회 전용 로직이나 트랜잭션이 선택사항인 경우 (readOnly = true 옵션으로 해결되는 경우가 많아 잘 쓰이지X)
NOT_SUPPORTED 트랜잭션이 있으면 일시정지 하고 비트랜잭션으로 실행 있으면 정지 후 비트랜잭션 / 없으면 그냥 실행 별도의 비트랜잭션으로 무거운 쿼리를 돌릴 경우 
ex) 대용량 배치 조회

 

 

4️⃣ 격리 수준 (Isolation)

👉 여러 트랜잭션이 동시에 실행될 때 데이터 정합성을 어떻게 보장할지 결정한다.

 

수준 설명 주의점
READ_UNCOMMITTED 커밋 안 된 데이터도 읽음 더티 리드 발생 가능
READ_COMMITTED 커밋된 데이터만 읽음 대부분 DB 기본값
REPEATABLE_READ 한 트랜잭션 내에서 조회 결과가 항상 같음 팬텀 리드 가능성
SERIALIZABLE 완벽한 정합성 보장 동시성 👇, 성능👇

 

5️⃣ 롤백 규칙 & 주의점

스프링은 기본적으로 RuntimeException이나 Error가 발생하면 롤백하고 Checked Exception은 롤백하지 않는다.

@Transactional(rollbackFor = {IOException.class})
public void someMethod() throws IOException {
    userRepository.save(...);
    throw new IOException("Checked 예외 발생!");
}

이렇게 하면 checked exception도 롤백 대상으로 포함할 수 있다.

 

⚠️ 주의할 점: 내부 호출(self-invocation)

 

@Service
public class UserService {

    @Transactional
    public void outer() {
        inner(); // ❌ 프록시 안 거침 (this.inner())
    }

    @Transactional
    public void inner() {
        // 트랜잭션 적용 기대했지만 안 됨
    }
}

👉@Transactional 은 프록시를 거쳐야 동작하기 때문에, 같은 클래스 안에서 자기 자신 메소드를 직접 호출하면 트랜잭션이 적용되지 않는다.

이런 경우엔 메소드를 다른 클래스로 분리하거나, 프록시를 통해 호출하는 방식으로 우회해야 한다.

 

해결책 1: 메소드를 다른 클래스로 분리

@Service
public class InnerService {

    @Transactional
    public void inner() {
        System.out.println("InnerService 트랜잭션 적용");
    }
}

@Service
public class UserService {

    private final InnerService innerService;

    public UserService(InnerService innerService) {
        this.innerService = innerService;
    }

    @Transactional
    public void outer() {
        System.out.println("outer 실행");
        innerService.inner(); // ✅ 프록시 객체를 통해 호출됨
    }
}

 

해결책 2: 자기 자신을 프록시로 주입받아 호출

@Service
public class UserService {

    private final UserService self; // 자기 자신 프록시 주입

    public UserService(UserService self) {
        this.self = self;
    }

    @Transactional
    public void outer() {
        System.out.println("outer 실행");
        self.inner(); // ✅ 프록시 객체로 inner() 호출
    }

    @Transactional
    public void inner() {
        System.out.println("inner 트랜잭션 적용됨 ✅");
    }
}

💡 단, 이 방식은 순환참조 설정에 주의해야 하고, 가독성 면에서 분리 방식(1번)이 좀 더 권장됩니다.

'Language > Java' 카테고리의 다른 글

Spring  (0) 2024.10.02
[JAVA] 퀵 정렬 (Quick Sort)  (0) 2022.12.14
[JAVA] 삽입 정렬 (Insertion Sort)  (2) 2022.12.14
[JAVA] 선택정렬 (Selection Sort)  (0) 2022.12.14
[JAVA] 거품 정렬 (Bubble Sort)  (0) 2022.12.09