ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 놓치기 쉽지만 중요한 @Transactional Rollback
    Spring Framework 2023. 9. 7. 00:01

    개요

    트랜잭션에 대해서 놓치기 쉬운 개념들을 정리해보고자 합니다.

    최대한 많은 내용을 담기 위해 핵심개념위주로 글을 작성해보고자 합니다.

     

    Spring의 @Transactional 이란?

    트랜잭션이란 데이터베이스에서 주로 사용하는 개념으로 이 연산이 한 번에 묶여서 일어나야 하는지 전부 일어나지 말아야 하는지를 단위를 묶어줄 때 사용합니다.

     

    예를 들어 어떤 시스템에서 회원이 가입하면 이름과 나이를 저장하고 조회해서 사용해야 합니다.

    1. 회원가입을 수행할 때 첫 번째로 회원의 이름을 DB에 저장했습니다.

    2. 두 번째로 회원의 나이를 DB에 저장했습니다.

     

    이때 2번째 작업을 수행하다가 예상치 못한 오류가 발생했다면 회원의 이름만 남아있고 나이는 존재하지 않는데 이런 의도치 않는 상황에서 데이터 정합성을 지키기 위해 흔히 사용합니다.

     

    위에서 설명한것처럼 트랜잭션으로 묶인 연산은 다음과 같은 특성을 가집니다.

    • 전부 다 저장되었거나, 전부 다 저장되지 않았거나

     

    Spring에서는 이 개념을 @Transactional 어노테이션을 통해 선언적 트랜잭션을 제공합니다.

     

    어떻게 마법같이 어노테이션만 붙이면 해결될까요?

    그 이유는 바로 프록시 객체를 활용한 AOP로 구현되었습니다.

     

    AOP의 주의사항?

    AOP와 Proxy 패턴에 대해서는 어느 정도 안다고 가정하고, 쉽게 놓칠 수 있는 부분이여 기록만 해두려고 합니다.

    • 클래스에 final 키워드를 사용하면 상속이 불가능하고 Proxy 생성이 불가합니다.
    • private final 메서드는 AOP가 적용되지 않습니다.
    • 타깃 클래스 내에서 호출하는 타깃 메서드는 AOP가 적용되지 않습니다.

     

    Rollback은 언제 발생할까?

    기본적으로 RuntimException, Error만 롤백을 수행합니다.

    즉, Exception클래스의 IOException 같은 상황이 발생한다면 Rollback 되지 않습니다.

    rollbackedFor 파라미터를 통해 롤백을 수행할 Execption을 정의해 주는 방법이 있습니다.

     

    rollback에 대해 조금더 자세하게 알고 싶다면 다음글을 참고해 주세요

    https://junuuu.tistory.com/481

     

    @Transactional 롤백과 @TransactionalEventListener

    개요 비동기 EventListener를 다루면서 예외처리를 하던 중 고민했던 일들을 적어보고자 합니다. @Transactional을 사용하는 메서드 예시 @Transactional public void function(request: UploadRequest) { val registeredVideo =

    junuuu.tistory.com

     

    try-catch로 잡으면 rollback은 발생하지 않을까?

    일반적으로 try-catch로 예외를 잡아버리면 rollback은 발생하지 않을 것으로 예상할 수 있습니다.

    하지만 OuterService가 InnerService를 호출하는 구조에서 InnerService에서 발생한 RuntimeException을 OuterService에서 잡아서 처리하면 이미 rollback이 mark 되기 때문에 rollback을 수행하게 되는 부분을 조심해야 합니다.

    (트랜잭션의 전파 기본속성이 PROPAGATION_REQUIRED이기 때문에 NEW로 변경한다면 위의 문제를 막을 수 있습니다.)

     

    자세한 내용은 우아한 형제들 기술블로그에서 다루며, 실습을 진행해 보는 것도 재미있습니다.

    https://techblog.woowahan.com/2606/

     

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

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

    techblog.woowahan.com

     

    트랜잭션의 다양한 전파설정

    트랜잭션에는 다양한 전파설정이 있습니다.

    최상단 클래스에 트랜잭션을 선언하면 하위에서 호출하는 클래스로 트랜잭션이 쭉 전파가 됩니다.

     

    기본적으로 PROPAGATION_REQUIRED입니다.

    PROPAGATION_REQUIRES_NEW를 사용하면 새로운 트랜잭션을 열어서 사용할 수 있습니다.

     

    이외에도 여러 가지 옵션들이 존재합니다. (위의 2개 이외에 잘 사용은 안 해봤습니다)

     

    EventListener와 트랜잭션

    우리는 보통 비즈니스로직의 관계를 느슨하게 가져가기 위해 EventListener를 활용하곤 합니다.

    ApplicationEventListener로 이벤트를 발행하고, @EventListener를 활용하여 해당 이벤트를 구독하고 로직을 처리합니다.

     

    예를 들어 회원가입은 성공했지만 유저 가입 안내 메시지 발송에 실패하면 회원가입은 실패해야 할까요?

     

    대표적인 케이스가 회원가입을 수행하고 회원가입 이벤트를 발행하면, 회원가입에 영향을 주면 안 되는 부가적인 비즈니스인 신규 쿠폰 발급, 유저 가입 안내 메시지 발송 등을 @EventListener로 처리합니다.

     

    하지만 @EventListener에서 예외가 발생하면 그대로 예외가 전파됩니다.

    이를 방지하기 위해서는 @Async를 활용해 별도의 스레드를 사용하게 하는 방법을 사용할 수 있습니다.

    또는 회원가입이 실패되는 경우에는 신규 쿠폰 발행, 유저 가입 안내 메시지를 발행해야 하지 않는다면 @TransactionEventListener를 활용해 볼 수도 있습니다.

     

    조금 더 자세하게 보고 싶다면 Spring Event 활용해 보기 정리내용을 참고해 주세요

    https://junuuu.tistory.com/726

     

    Spring Event 사용하기

    개요 이벤트를 사용하는 이유는 무엇일까요? 가장 주된 이유는 서비스 간의 의존성을 줄이기 위해서입니다. 서비스 의존성을 줄이기 예를 들어 회원가입으로 보았을 때, 회원가입을 수행하고

    junuuu.tistory.com

     

    외부 API 호출과 롤백

    Spring 트랜잭션을 활용하다 보면 외부 API 호출은 빠질 수 없습니다.. (외부 호출이 없는 유토피아면 행복할 것 같네요)

    만약에 중간에 RuntimeException이 발생하여 rollback이 수행된다면 외부 호출은 rollback 될까요?

     

    데이터베이스와 관련 없이 이미 네트워크를 타고 가버렸기 때문에 rollback이 수행되지 않습니다.

    따라서 외부호출은 rollback 되지 않았지만 우리의 데이터들은 rollback 되었기 때문에 정합성을 고려해보아야 합니다.

     

    실패에 대한 보상 트랜잭션 - Saga Pattern을 활용할 수도 있고, 외부 호출을 가장 먼저 수행해서 데이터는 저장되지 않도록 해볼 수도 있습니다.

     

    또한 외부호출에 대한 멱등성에 따라서도 방법은 다양해질 수 있습니다.

     

    조금 더 자세하게 보고 싶다면 외부호출 API Rollback에 대해 정리한 글을 참고해 주세요

    https://junuuu.tistory.com/818

     

     

    트랜잭션과 커넥션

    보통 응답 지연은 네트워크에서 발생하며, 이를 방지하기 위해 미리 커넥션을 연걸해 놓는 데이터베이스 커넥션 풀 등을 활용하곤 합니다.

    트랜잭션과 데이터베이스 커넥션은 어떤 관계가 있을까요?

     

    트랜잭션 전파속성을 REQUIRE_NEW를 활용하여 새로운 트랜잭션을 만들면 새로운 Connection을 사용합니다.

    반면 새로운 트랜잭션을 만들지 않으면 기존 Connection을 이용합니다.

     

    조금 더 자세하게 보고 싶다면 @Transactional과 Database Connection에 대해 정리한 글을 참고해 주세요

    https://junuuu.tistory.com/814

     

    커넥션관리를 위해 외부 호출 트랜잭션 끊기

    커넥션풀의 개수가 10개이고 유저 10명이 동시에 요청하는 경우는 11번째 유저는 커넥션을 획득하지 못하여 대기하다가 timeout이 발생할 수 있습니다.

     

    이때 외부 API를 호출하는 부분이 트랜잭션에 포함되어 있고 시간이 5초가 걸리면 어떻게 될까요? 

    5초 동안은 커넥션을 점유하고 있어서 성능의 병목현상이 발생할 수 있습니다.

     

    이럴 때는 데이터베이스의 커넥션이 필요한 실제 Persistence Layer와 네트워크 호출을 수행하는 Client Layer를 분리하여 Service Layer에서 사용하는 방법을 취할 수 있습니다.

     

    이 경우에도 롤백에 대해 신중하게 고려해보아야 합니다.

     

    보상 트랜잭션과 이벤트

    마이크로서비스 아키텍처에서 트랜잭션을 관리하기 위해서 pub/sub 패턴을 활용하곤 합니다.

    예를 들어 주문과 배송이라는 마이크로 서비스가 존재하고, 주문이 정상적으로 생성되고 나서 배송정보를 생성할 때 문제가 발생하여 배송 쪽에서 주문취소 이벤트를 발행하면 주문은 해당 이벤트를 받아서 주문 생성에 대한 내용을 rollback 하는 내용을 구현할 수 있습니다.

    이때 이벤트 pub/sub을 위해서는 kafka, sns sqs 등의 기술을 사용할 수 있습니다.

     

    이벤트 드리븐 아키텍처와 트랜잭션 아웃박스 패턴

    보상트랜잭션과 이벤트에 대해 고민해보다 보면 Read대신, CUD가 발생할때 외부호출이 필요할까?라는 생각이 듭니다.

    대신 CUD에 대한 이벤트를 발행하여 관심 있는 서비스들이 이를 구독하여 자신 서비스에 입맛에 맞게 적용할 수 있습니다.

     

    이를 통해 다른 도메인, 모듈들과 결합도를 느슨하게 가져갈 수 있습니다.

    이때 고려해야 할 부분은 Eventually Consistency라는 개념으로 최종적 일관성을 보장해주어야 합니다. (즉, 실시간성이 매우 보장되어야 한다면 고려해보아야 합니다.)

     

    또한 현재 SNS-SQS를 활용하고 있는데 애플리케이션 -> SNS 구간에서는 HTTP 통신을 사용하기 때문에 이벤트를 발행하는 과정에 문제가 발생할 수 있습니다. (SNS에 장애가 발생한다면?)

     

    이때 메시징 시스템의 장애가 시스템의 장애로 이어져야 할까요?

    비즈니스 상황에 따라 다르게 적용하겠지만, 이벤트 발행을 기록하는 것이 도메인의 중요한 행위로 본다면 트랜잭션 아웃박스 패턴을 활용하여 메시징 시스템의 장애를 시스템의 장애와 격리할 수 있습니다.

     

    이벤트를 발행하기 전에 READY 상태로 이벤트를 기록해 놓고, 실제로 발행이 되면 COMPLETE 상태 등으로 변환합니다.

    추후 배치등을 사용하여 5분이 지나도 발행되지 않은 이벤트를 재발행합니다.

     

    결론

    트랜잭션과 관련된 다양한 개념 및 주의할 점에 대해서 알아보았고 더 나아가서 이벤트 기반 아키텍처까지 살펴보았습니다.

    여러 가지 개념을 기반으로 데이터 정합성을 맞추기 위해 상황에 맞게 가장 좋은 방법을 고려해 보는 것이 가장 중요할 것 같습니다.

    There is No Silver Bullet

    댓글

Designed by Tistory.