Spring

Spring 트랜잭션 전파

Spring에서 사용하는 어노테이션 '@Transactional'은 해당 메서드를 하나의 트랜잭션 안에서 진행할 수 있도록 만들어주는 역할을 합니다.

 

이때 트랜잭션 내부에서 트랜잭션을 또 호출한다면 스프링에서는 어떻게 처리하고 있을까요?

새로운 트랜잭션이 생성될 수도 있고, 이미 트랜잭션이 있다면 부모 트랜잭션에 합류할 수도 있을 것입니다.

진행되고 있는 트랜잭션에서 다른 트랜잭션이 호출될 때 어떻게 처리할지 정하는 것을 '트랜잭션의 전파 설정'이라고 부릅니다.

 

간단한 실습 코드를 먼저 작성합니다.

import lombok.ToString;

import javax.persistence.*;

@Entity
@ToString
public class TestObject {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    public TestObject(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
@Repository
public interface TestObjectRepository extends JpaRepository<TestObject, Long> {
}
@SpringBootTest
class TestObjectServiceTest {

    @Autowired
    TestObjectService testObjectService;

    @Autowired
    TestObjectRepository testObjectRepository;

    @Test
    public void transactionTest() throws Exception {
        TestObject testObject = new TestObject("자식");
        testObjectService.transactionTest(testObject);

        List<TestObject> all = testObjectRepository.findAll();

        System.out.println(all.size());
        for (TestObject object : all) {
            System.out.println(object);
        }
    }
}

- 테스트 코드

위의 코드를 만들어두고

TestObjectService에서 TransactionService라는 두 클래스를 통해서 각 예제를 진행해 보겠습니다.

TestObjectService의 트랜잭션이 부모 TransactionService의 트랜잭션이 자식으로 두개 코딩할 것입니다.

 

REQUIRED (기본값)

- 부모 트랜잭션이 존재한다면 부모 트랜잭션으로 합류합니다. 부모 트랜잭션이 없다면 새로운 트랜잭션을 생성합니다.

- 중간에 롤백이 발생한다면 모두 하나의 트랜잭션이기 때문에 진행사항이 모두 롤백됩니다.

@Transactional()을 그대로 사용하면 기본값이 적용됩니다.

 

@Service
@Slf4j
@RequiredArgsConstructor
public class TransactionService {

    private final TestObjectRepository testObjectRepository;

    @Transactional(rollbackFor = Exception.class)
    public void service(TestObject testObject) throws Exception {
        log.info("currentTransactionName : {}",
                TransactionSynchronizationManager.getCurrentTransactionName());
        testObjectRepository.save(testObject);
        // throw new Exception();
    }
}
@Service
@Slf4j
@RequiredArgsConstructor
public class TestObjectService {

    private final TransactionService transactionService;

    private final TestObjectRepository testObjectRepository;

    @Transactional()
    public void transactionTest(TestObject testObject) throws Exception {
        log.info("currentTransactionName : {}",
                TransactionSynchronizationManager.getCurrentTransactionName());
        testObjectRepository.save(new TestObject("부모"));
        transactionService.service(testObject);
    }
}

테스트 코드를 돌리면 

각각의 트랜잭션이 같은 것을 확인 할 수 있습니다.

DB에도 두개 모두 들어갔습니다.

 

중간에 롤백이 발생하면 전체가 롤백이라고 하였으니 만약 TransactionService에서 롤백이 발생하면 TestObjectService도 롤백 되겠죠?

TransactionService 아래 부분에 throw new Exception(); 주석을 풀어주세요.

각각의 트랜잭션이 같은데 DB에는 아무것도 저장되지않았습니다.

 

정보주의!!

만약 TestObjectService에서 try catch문으로 익셉션을 잡으면 어떻게 될까요? 

@Transactional()
    public void transactionTest(TestObject testObject) {
        try{
            log.info("currentTransactionName : {}",
                    TransactionSynchronizationManager.getCurrentTransactionName());
            testObjectRepository.save(new TestObject("부모"));
            transactionService.service(testObject);
        } catch (Exception e) {
            System.out.println("에러 잡아버림");
        }
    }

익셉션을 잡았으니 롤백 안되고 부모 로직의 한개는 db에 저장 되겠지 라는 생각을 했지만.....

이게 무람??.....ㅠㅠㅠ

아래는 우아한 형제들 기술블로그입니다.

https://woowabros.github.io/experience/2019/01/29/exception-in-transaction.html

 

응? 이게 왜 롤백되는거지? - 우아한형제들 기술 블로그

이 글은 얼마 전 에러로그 하나에 대한 호기심과 의문으로 시작해서 스프링의 트랜잭션 내에서 예외가 어떻게 처리되는지를 이해하기 위해 삽질을 해본 경험을 토대로 쓰여졌습니다. 스프링의

woowabros.github.io

내용 간단하게 정리하면 tx에서 Exception이 발생하면 rollback-only를 마킹하고 최종 커밋할려고 할때 마킹 되어 있어서 롤백시켜 버립니다. 그래서 익셉션 발생하면 해당 트랜잭션을 재사용할 수 없다는 의미이다. 

 

그렇다면 트랜잭션이 다르면 가능한건가? 라고 생각하셨다면 정답입니다 .ㅎ 이 부분은 REQUIRES_NEW에서 바로 다루도록 하겠습니다.

 

REQUIRES_NEW

- 무조건 새로운 트랜잭션을 생성합니다. 각각의 트랜잭션이 롤백되더라도 서로 영향을 주지 않습니다.

TransactionService에 적용해주고 바로 실행하면

@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void service(TestObject testObject) throws Exception {
        log.info("currentTransactionName : {}",
                TransactionSynchronizationManager.getCurrentTransactionName());
        testObjectRepository.save(testObject);
        throw new Exception();
    }

정보 주의!!!

만약 TransactionService에서 넘기는 에러가 RuntimeException이라면 어떻게 될까요?

    @Transactional()
    public void transactionTest(TestObject testObject) {
        try{
            log.info("currentTransactionName : {}",
                TransactionSynchronizationManager.getCurrentTransactionName());
        testObjectRepository.save(new TestObject("부모"));
        transactionService.service(testObject);
        } catch (RuntimeException e) {
            System.out.println("에러 잡아버림");
        }
    }

ㅎㅎ 동일합니다.

그럼 try catch문을 제거 하면 각각 어떻게 될까요?

@Transactional()
    public void transactionTest(TestObject testObject) {
        log.info("currentTransactionName : {}",
            TransactionSynchronizationManager.getCurrentTransactionName());
        testObjectRepository.save(new TestObject("부모"));
        transactionService.service(testObject);
    }

- RuntimeException일 때

> 트랜잭션둘다 롤백 

- Exception일 때

> 자식은 롤백 부모는 커밋

무슨 차이일까요?

아래는 Transactional 어노테이션의 rollbackFor 메서드부분인데 빨간색 칸을 해석이 아닌 파파고를 해보면

음.... 그냥 해석을 하자 .ㅎ

 

해석을 해보면 기본적으로 트랜잭션은 RuntimeException and Error 일 때 롤백을 checked exceptions일 때는 롤백 안한다고 합니다. checked exceptions인 Exception은 롤백 대상이 아니므로 부모의 내용은 커밋 되게 되는 것이죠 .ㅎ

그럼 checked exceptions과 unchecked exceptions은 뭘까요 ....? 다음 글에서 다루도록 하겠습니다 .ㅎ

 

NESTED

- 부모 트랜잭션이 존재한다면 중첩 트랜잭션을 생성합니다.

- 중첩된 트랜잭션 내부에서 롤백 발생시 해당 중첩 트랜잭션의 시작 지점 까지만 롤백됩니다.

- 중첩 트랜잭션은 부모 트랜잭션이 커밋될 때 같이 커밋됩니다.

- 부모 트랜잭션이 존재하지 않는다면 새로운 트랜잭션을 생성합니다.

@Transactional(propagation = Propagation.NESTED)
    public void service(TestObject testObject) {
        log.info("currentTransactionName : {}",
                TransactionSynchronizationManager.getCurrentTransactionName());
        testObjectRepository.save(testObject);
        //throw new RuntimeException();
    }

 

????

위 에라를 쉽게 설명하면 하이버 네이트에서 지원 안해준다!! 입니다.ㅎ 

참고로 JpaTransactionManager를 보면 jpa에서는 지원하는데 하이버네이트가 안되는 겁니다 .ㅎ

(왜 안해주는지 해주는 orm은 무엇인지 등 추가적으로 찾아봤지만 쉽게 찾기 어려워서 찾게 되면 추가하도록 하겠습니다 ..)

 

MANDATORY

- 부모 트랜잭션에 합류합니다. 만약 부모 트랜잭션이 없다면 예외를 발생시킵니다.

 @Transactional(propagation = Propagation.MANDATORY)
    public void service(TestObject testObject) {
        log.info("currentTransactionName : {}",
                TransactionSynchronizationManager.getCurrentTransactionName());
        testObjectRepository.save(testObject);
    }

어노테이션 지우고 자식 메서드만 부르면 확인 완료!

public void transactionTest(TestObject testObject) {
        //log.info("currentTransactionName : {}",
        //    TransactionSynchronizationManager.getCurrentTransactionName());
        //testObjectRepository.save(new TestObject("부모"));
        transactionService.service(testObject);
    }

NEVER

- 트랜잭션을 생성하지 않습니다. 부모 트랜잭션이 존재한다면 예외를 발생시킵니다.

위에서한 주석 푸시고 설정 바꿔주면 확인 완료!

@Transactional(propagation = Propagation.NEVER)
    public void service(TestObject testObject) {
        log.info("currentTransactionName : {}",
                TransactionSynchronizationManager.getCurrentTransactionName());
        testObjectRepository.save(testObject);
    }

 

하위 두 부분도 그냥 부모 트랜잭션 주석했다가 말았다가 하면서 확인하시면 됩니다. 아래 두 경우 트랜잭션 없는 형태로 실행될 수 있습니다. 

 

SUPPORTS

- 부모 트랜잭션이 있다면 합류합니다. 진행중인 부모 트랜잭션이 없다면 트랜잭션을 생성하지 않습니다.

 

NOT_SUPPORTED

- 부모 트랜잭션이 있다면 보류시킵니다. 진행중인 부모 트랜잭션이 없다면 트랜잭션을 생성하지 않습니다.

'Spring' 카테고리의 다른 글

트랜잭션 격리 수준  (0) 2021.05.30
Spring에서 사용하는 어노테이션 정리  (0) 2021.05.29
springboot에서 redis 다루기  (0) 2021.05.23
Spring Interceptor  (0) 2021.04.12
Spring Filter  (0) 2021.04.10