본문 바로가기
👨‍💻 프로그래밍/📦 Backend

트랜잭션 전파 (Transaction Propagation) - Transaction이 서로 만날 때 스프링은 어떻게 처리할까?

by 개발자 진개미 2024. 3. 7.
반응형

트랜잭션 (Transaction)이 뭔지 간단히

쉽게 말해서 여러 개의 다른 동작들을 1개로 묶는 역할을 합니다. 그래서 여러 개의 동작 중에 1개라도 실패하면 다 실패해야 해서 성공한 동작도 실패로 되돌릴 수 있는 기능인 롤백 (Rollback)이 상당히 중요한 역할을 합니다.

여기서 큰 그림에서 트랜잭션을 바라보면 비즈니스 로직에서 필요한 여러 동작을 하나로 묶는 것처럼 생각할 수 있는데요. 이걸 논리적 트랜잭션 (Logical Transaction)이라고 합니다. 그 반대는 당연히 물리적 트랜잭션 (Physical Transaction)이겠죠? 무슨 말이냐 하면, 송금을 생각해 보겠습니다. 논리적 트랜잭션 관점에서 큰 그림으로 보면 아래의 동작들을 1개의 트랜잭션으로 묶는 것으로 생각할 수 있습니다.

  • 송금하는 유저의 계좌에서 돈 빼기
  • 회사의 모계좌에 돈 넣기
  • 송금할 유저의 계좌에 돈 넣기
  • 회사의 모계좌에 돈 빼기

하지만 생각해 보면 이 각각의 동작을 파고 들어가면 DB 단위에서 트랜잭션을 걸고 있을 겁니다. 이걸 물리적 트랜잭션이라 하고, 여러 개의 물리적 트랜잭션을 묶는 단위를 논리적 트랜잭션이라 합니다.


자연스럽게 궁금한 Transaction의 동작들

Transaction이 뭔지 이해하면 자연스럽게 다양한 상황에서 Transaction이 어떻게 동작할지 궁금해 집니다. 드라마나 영화에 빠지면 이 캐릭터가 이런 상황에는 어떻게 할지, 다른 캐릭터랑 만나면 어떤 식으로 대화할지 궁금하지 않으신가요? 비슷하게 생각해서 Transaction이라는 아이를 알게 됐으니 Transaction의 핵심 속성들을 알아봅시다!

 

🐜 Transaction의 핵심 속성은 4가지가 있습니다.

  1. Transaction이 서로 동일한 곳을 수정하면 어떻게 할지 (격리, Isolation)
  2. Transaction이 서로 만나면 어떻게 하는지 (전파, Propagation)
  3. Transaction이 어느 정도 지속될 수 있는지 (Timeout)
  4. Transaction이 Read만 하는지, Write도 하는지 (Read-Only)

1번은 다른 글에서 알아봤고 3, 4번은 너무 뻔해서 2번만 이번 글에서 다루겠습니다!


Transaction이 다른 Transaction과 만나면 어떻게 될까?

정답은... 내 마음대로 설정할 수 있다입니다. Spring 기준으로 기본값은 REQUIRED라는 친구인데, 기존 Transaction이 있으면 그거 쓰고 없으면 새로 만드는 방식입니다. 하지만 이 방식이 마음에 들지 않고 다른 방식을 쓰고 싶을 수 있습니다. 그럴 때 Spring에서는 여러 방식의 전파 속성 (Propagation Options)을 지원하기 때문에 골라 쓰면 됩니다.


Spring에서 Transaction을 다루는 방법

  단어의 한국어 뜻 기존 Transaction ⭕ 기존 Transaction ❌
REQUIRED 필수 (예외는 안 던짐) 기존꺼 쓰기 새로 만들어서
REQUIRES_NEW 꼭 새로운 트랜잭션 필요 새로 만들어서 새로 만들어서
MANDATORY 트랜잭션 필수 기존꺼 쓰기 예외
SUPPORTS 없어도 되지만 지원함 기존꺼 쓰기 없이 진행
NOT_SUPPORTED 지원하지 않음 없이 진행 없이 진행
NEVER 절대 X 예외 없이 진행
NESTED 중첩해서 자식 트랜잭션 만듬 중첩 새로 만들어서

사실 이걸 외울 필요는 없고 결국 기존 트랜잭션이 있을 때와 없을 때 동작이 5가지 라는 것에 집중해야 합니다.

  • 예외
  • Transaction 새로 만들어서 쓰기
  • 기존 Transaction 쓰기
  • 없이 진행
  • 중첩

여기서 단순 조합만 생각하면 25가지지만, 의미 없는 것들을 제외한게 (예를 들어 기존 트랜잭션이 없는데 기존 꺼를 쓰는 거나 중첩을 하는 게 말이 안 됩니다) 스프링에서 제공하는 저 7개의 조합입니다. 그러니깐 저 표를 보고 뭘 쓸까 고민하는게 아니라, 내 Transaction이 어떤 동작을 해야 할지 고민하고 그게 뭔지 저 표에서 찾는 느낌으로 가야 합니다.


사실 잘 안 쓰인다...

솔직히 말해서 기본값인 REQUIRED외에의 속성을 쓸 일은 거의 없습니다. 대부분의 경우 코드로 명시적으로 나타내지 않고 Transaction 전파 속성으로 처리하는 건 안티패턴입니다. 나는 Transaction 전파 속성을 열심히 공부해서 알고 있는데 내가 쓴 코드를 앞으로 유지보수 할 모두가 이걸 잘 알아서 장애 없이 작업한다고 장담할 수 있을까요? 🤔

예를들어 참가자 수를 세는 로직이 있고, 이게 결제와 연결돼 있다고 하겠습니다. 요구사항이 결제가 실패해도 참가자 수는 무조건 올라가게 해야 합니다. 이럴 때 REQUIRES_NEW 옵션을 쓰면 결제 Transaction이 실패해도 참가자 수 세는 Transacion은 성공하게 처리할 수 있습니다.

fun main() {
    pay()
}

@Transactional
fun pay(user: User) {
    // 생략
    countParticipant()
}

@Transactional(propagation = REQUIRES_NEW)
fun countParticipant(user: User) {
    // 생략
}

하지만 굳이 이렇게 안 하고 아래와 같이 명시적으로 해도 됩니다.

fun main() {
    try {
        pay()
    } catch(e: Exception) {
        // 예외 처리 생략
    }
    
    countParticipant()
}

@Transactional
fun pay(user: User) {
    // 생략
}

@Transactional
fun countParticipant(user: User) {
    // 생략
}

 

비슷한 이유로 JPA에서 @Transactional를 붙여서 Dirty Checking 방식으로 업데이트하는 것도 잘 안 쓰려고 합니다. 이것만을 위해 Transaction을 거는 게 불필요하기도 하고, 명시적이지 않기 때문입니다.


반응형