본문으로 바로가기

1. 트랜잭션의 시작과 종료 및 전파 속성(Transaction Propagation)

 

[트랜잭션 전파 속성(Transaction Propagation)이란? ]

 

"트랜잭션 전파 속성"을 이해하려면, 우리가 '트랜잭션'이라는 것이 무엇인지 알아야 합니다.

트랜잭션은 마치 '작업의 단위'와 같습니다.

 

예를 들어, 당신이 집에서 방 청소를 하는 것을 생각해봅시다.
방 청소는 크게 세 가지 작업으로 나눌 수 있습니다: 1) 먼지 제거, 2) 가구 정리, 그리고 3) 바닥 닦기.

이제 여기서 '트랜잭션 전파 속성'은 이런 작업들을 어떻게 조합할 것인가에 대한 규칙입니다.

예를 들어, "REQUIRED"라는 전파 속성은 이미 진행 중인 청소 작업(예: 먼지 제거)에 다른 작업(예: 가구 정리)을 추가하는 것입니다. 만약 아무도 청소를 하고 있지 않다면, 새로운 청소 작업을 시작합니다.

반면에 "REQUIRES_NEW"라는 전파 속성은 무조건 새로운 청소 작업을 시작하는 것입니다. 이미 진행 중인 다른 사람의 청소와 상관 없이 자신만의 새로운 청소(예: 바닥 닦기)를 시작합니다.

따라서 트랜잭션 전파 속성은 여러 개의 작업(트랜잭션)들이 어떻게 서로 관계를 맺고 진행될 지 결정하는 규칙입니다.

 

Spring이 제공하는 선언적 트랜잭션(트랜잭션 어노테이션, @Transactional)의 장점 중 하나는 여러 트랜잭션을 묶어서 커다란 하나의 트랜잭션 경계를 만들 수 있다는 점이다. 작업을 하다보면 기존에 트랜잭션이 진행중일 때 추가적인 트랜잭션을 진행해야 하는 경우가 있다. 이미 트랜잭션이 진행중일 때 추가 트랜잭션 진행을 어떻게 할지 결정하는 것 전파 속성(Propagation)이다. 

전파 속성에 따라 기존의 트랜잭션에 참여할 수도 있고, 별도의 트랜잭션으로 진행할 수도 있고, 에러를 발생시키는 등 여러 선택을 할 수 있다. 이렇게 하나의 트랜잭션이 다른 트랜잭션을 만나는 상황을 그림으로 나타내면 다음과 같다.

 

 

 

[ 물리 트랜잭션과 논리 트랜잭션 ]

 

물리 트랜잭션과 논리 트랜잭션의 차이는 "실제 일을 하는 사람들"과 "그 일들을 추적하고 관리하는 사람들" 사이의 차이

 

트랜잭션은 데이터베이스에서 제공하는 기술이므로 커넥션 객체를 통해 처리한다. 그래서 1개의 트랜잭션을 사용한다는 것은 하나의 커넥션 객체를 사용한다는 것이고, 실제 데이터베이스의 트랜잭션을 사용한다는 점에서 물리 트랜잭션이라고도 한다.

앞서 설명하였듯 트랜잭션 전파 속성에 따라서 외부 트랜잭션과 내부 트랜잭션이 동일한 트랜잭션을 사용할 수도 있다. 하지만 스프링의 입장에서는 트랜잭션 매니저를 통해 트랜잭션을 처리하는 곳이 2군데이다. 그래서 실제 데이터베이스 트랜잭션과 스프링이 처리하는 트랜잭션 영역을 구분하기 위해 스프링은 논리 트랜잭션이라는 개념을 추가하였다. 예를 들어 다음의 그림은 외부 트랜잭션과 내부 트랜잭션이 1개의 물리 트랜잭션(커넥션)을 사용하는 경우이다.

 

 

이 경우에는 2개의 트랜잭션 범위가 존재하기 때문에 개별 논리 트랜잭션이 존재하지만, 실제로는 1개의 물리 트랜잭션이 사용된다. 만약 트랜잭션 전파 없이 1개의 트랜잭션만 사용되면 물리 트랜잭션만 존재하고, 트랜잭션 전파가 사용될 때 논리 트랜잭션 개념이 사용된다. 이러한 물리 트랜잭션과 논리 트랜잭션을 정리하면 다음과 같다.

  • 물리 트랜잭션: 실제 데이터베이스에 적용되는 트랜잭션으로, 커넥션을 통해 커밋/롤백하는 단위
  • 논리 트랜잭션: 스프링이 트랜잭션 매니저를 통해 트랜잭션을 처리하는 단위

 

 

기존의 트랜잭션이 진행중일 때 또 다른 트랜잭션이 사용되면 복잡한 상황이 발생한다. 스프링은 논리 트랜잭션이라는 개념을 도입함으로써 상황에 대한 설명을 쉽게 만들고, 다음과 같은 단순한 원칙을 세울수 있었다.

  • 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋됨
  • 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백됨

 

더 이해하기 쉽게 청소를 한다고 생각했을때로 가정해보자.

 

먼저 '물리 트랜잭션'과 '논리 트랜잭션'의 개념을 명확히 이해해야 합니다.

물리 트랜잭션은 실제로 데이터베이스에 변화를 주는 작업 단위입니다. 예를 들어, 당신이 방 청소를 한다고 가정하면, 청소가 완전히 끝나고 방이 깨끗해진 상태가 '커밋'되는 것입니다. 만약 청소 도중에 문제가 생겨서 중단한다면, 그 전 상태로 돌아가는 것이 '롤백'입니다.

논리 트랜잭션은 스프링에서 관리하는 작업 단위입니다. 이것은 실제 데이터베이스에 변화를 주지 않지만, 어떤 작업들을 수행할 것인지 결정합니다. 예를 들어, 방 청소 프로젝트에서 누가 어떤 일을 할지, 어떻게 진행될지 등을 계획하는 것입니다.

그런데 여기서 복잡한 상황이 발생할 수 있습니다. 이미 진행 중인 청소(트랜잭션)에 다른 사람들도 참여하게 되면 어떻게 될까요? 이런 경우 스프링은 각각의 참여자(논리 트랜잭션)를 따로 관리합니다.

예를 들어 A와 B 두 사람이 함께 방 청소를 하는 경우라고 생각해보세요. A와 B 모두 자신의 업무가 완료되야만 (모든 논리 트랜잭션이 커밋되야만) 전체적으로 방 청소(물리 트랜잭션)가 완료됩니다. 하지만 A나 B 중 한 명이라도 문제가 생겨서 업무를 완료하지 못한다면 (하나의 논리 트랜잭션이 롤백된다면), 전체적으로 방 청소는 실패하고 처음부터 다시 시작해야 합니다 (물리 트랜잭션은 롤백됨).

이렇게 스프링은 논리 트랜잭션 개념을 도입함으로써 복잡한 상황을 쉽게 관리하고, 어떤 상황에서도 일관된 규칙을 적용할 수 있습니다.

 

 

논리 트랜잭션을 기반으로 단순한 원칙을 세움으로써 2개 이상의 트랜잭션을 다루는 경우에 대한 이해가 상당히 쉬워진다. 실제로 트랜잭션들이 마주하는 상황에서 어떠한 전파 속성들이 있는지 살펴보도록 하자.

2. 다양한 스프링의 트랜잭션 전파 속성


[ REQUIRED 속성과 REQUIRES_NEW 속성 ]

 스프링에는 7가지 전파 속성이 존재하는데, REQUIRED와 REQUIRES_NEW를 바탕으로 어떻게 진행되는지 살펴보도록 하자.  REQUIRED와 REQUIRES_NEW를 이해하면 나머지는 응용이 가능하므로, 두 케이스만 자세히 살펴보도록 하자.

 

두 가지를 알기전에 우리가 초등학생에게 설명한다면 어떻게 설명할 것인가??

 

chatGpt에게 도움을 얻어 정리하였는데 이해가 쏙쏙 되었다.

 

트랜잭션이라는 것을 생각해보면,

우리가 보드 게임(부루마불, 모놀로피)을 할 때의 차례(turn)와 비슷하다고 생각하면 됩니다.
"한 사람이 차례를 가질 때까지 다른 사람들은 기다려야 해요."
"REQUIRED"   당신의 차례가 왔고 아직 주사위를 굴리지 않았다면 (즉, 현재 진행중인 '차례' 또는 '트랜잭션'이 없다면), 
당신은 주사위를 굴려 (새로운 '트랜잭션'을 생성해) 자신의 차례를 시작하게 됩니다.

"REQUIRES_NEW"는 이렇게 말합니다: 무조건 새로운 트랜잭션을 시작합니다. 
부루마불에서 본다면, 현재 진행 중인 다른 사람의 차례와 상관없이 자신만의 새로운 차례(주사위를 굴림)를 시작하는 것과 같습니다.

그래서 REQUIRED와 REQUIRES_NEW의 주된 차이점은
'현재 진행 중인 트랜잭션(차례)에 어떻게 반응하느냐'입니다.
REQUIRED는 현재 트랜잭션에 합류하거나 필요한 경우 새로운 트랜잭션을 생성합니다. 반대로 REQUIRES_NEW는 항상 새로운 트랜잭션을 생성합니다.

 

이제 전문가 적으로 분석해 보겠습니다.

REQUIRED

REQUIRED는 스프링이 제공하는 기본적인(DEFAULT) 전파 속성으로, 기본적으로 2개의 논리 트랜잭션을 묶어 1개의 물리 트랜잭션을 사용하는 것이다. 앞선 예시로 살펴본 경우가 REQUIRED에 해당하며, 내부 트랜잭션은 기존에 존재하는 외부 트랜잭션에 참여하게 된다.

 

 

여기서 참여한다는 것은 외부 트랜잭션을 그대로 이어간다는 뜻이며, 외부 트랜잭션의 범위가 내부까자 확장되는 것이다. 그러므로 내부 트랜잭션은 새로운 물리 트랜잭션을 사용하지 않는다.

하지만 트랜잭션 매니저에 의해 관리되는 논리 트랜잭션이 존재하므로 커밋은 내부 1회, 외부 1회해서 총 2회 실행된다. 물론 내부 트랜잭션은 논리 트랜잭션이기 때문에 커밋을 호출해도 즉시 커밋되지는 않고, 외부 트랜잭션이 최종적으로 커밋될 때 실제로 커밋이 된다. 롤백 역시 비슷한데, 내부 트랜잭션에서 롤백을 하여도 즉시 롤백되지 않는다. 물리 트랜잭션이 롤백될 때 실제 롤백이 처리되는데, 논리 트랜잭션들 중에서 1개라도 롤백되었다면 롤백된다. 물리 트랜잭션은 실제 커넥션에 롤백/커밋을 호출하는 것이므로 해당 트랜잭션이 끝나는 것이다.

이러한 REQUIRED를 정리하면 다음과 같다.

 

 

 

 

 

REQUIRES_NEW

REQUIRES_NEW는 외부 트랜잭션과 내부 트랜잭션을 완전히 분리하는 전파 속성이다. 그래서 2개의 물리 트랜잭션이 사용되며, 각각 트랜잭션 별로 커밋과 롤백이 수행된다. 이를 그림을 표현하면 다음과 같다.

 

 

두 개는 서로 다른 물리 트랜잭션이므로, 내부 트랜잭션 롤백이 외부 트랜잭션 롤백에 영향을 주지 않는다. 그러므로 내부 트랜잭션이 롤백 호출은 실제 커넥션에 롤백을 호출하는 것이므로 트랜잭션이 끝나게 된다.

서로 다른 물리 트랜잭션을 별도로 가진다는 것은 각각의 디비 커넥션이 사용된다는 것이다. 즉, 1개의 HTTP 요청에 대해 2개의 커넥션이 사용되는 것이다. 내부 트랜잭션이 처리 중일때는 꺼내진 외부 트랜잭션이 대기하는데, 이는 데이터베이스 커넥션을 고갈시킬 수 있다. 그러므로 조심해서 사용해야 하며, 만약 REQURES_NEW 없이 해결 가능하다면 대안책(별도의 클래스를 두기 등)을 사용하는 것이 좋다.

REQUIRED와 REQUIRES_NEW를 이해했다면 나머지는 응용이므로, 간단히 어떻게 동작하는지 살펴보도록 하자.

 

[ 다양한 트랜잭션 전파 속성 ]

앞서 설명하였듯 스프링은 총 7가지 전파 속성을 제공한다. 각각에 대해 요약해서 정리하면 다음과 같다.

  • REQUIRED
  • SUPPORTS
  • MANDATORY
  • REQUIRES_NEW
  • NOT_SUPPORTED
  • NEVER
  • NESTED

 

이것 또한 바로 이해하기 어려울 수 있어 초등학생에게 설명하는것으로 정리하고 넘어가자.

  1. REQUIRED (필요한 경우): 친구가 과자를 먹고 있는데, 당신도 그 친구와 함께 과자를 먹을 수 있습니다. 만약 아무도 과자를 먹고 있지 않다면, 당신은 새로운 팩의 과자를 열어서 먹을 수 있습니다.
  2. SUPPORTS (지원하는 경우): 당신은 친구가 과자를 먹고 있다면 같이 먹을 수 있지만, 아무도 안 먹는다 해도 상관없습니다. 그냥 현재 상황에 맞춰서 행동합니다.
  3. MANDATORY (필수적인 경우): 반드시 이미 누군가가 과자를 열어서 먹고 있어야 합니다. 아무도 안 먹는다면, 당신은 문제에 직면하게 됩니다(오류 발생).
  4. REQUIRES_NEW (새로 필요한 경우): 답사는 다른 사람이 어떤 종류의 과자를 열어서 나눠주더라도, 반드시 자기만의 새로운 팩의 과자를 열어서 자기 혼자만의 시간을 가집니다.
  5. NOT_SUPPORTED (지원하지 않음): 여러분은 절대로 다른 사람들과 함께 나눠진 과자를 함께 나눠갖거나 새롭게 팩을 꺼내어 서로 나눠갖지 않아야 합니다.
  6. NEVER (절대 없음): 여러분은 점심시간에만 간식을 준비할 수 있는 규칙이 있는 학교에서 점심시간 외에 간식 시간을 가집니다(오류 발생).
  7. NESTED (중첩된 경우): 답사는 이미 시작된 간식 시간에 참여하지만, 자신만의 작은 "부" 간식 시간(예: 트렌치 코트 안에서 스틱 과자를 먹음)을 가집니다. 만약 부 간식 시간이 문제가 생긴다면(예: 과자가 다 떨어짐), 그것은 자신의 작은 간식 시간에만 영향을 주고, 전체 간식 시간에는 영향을 주지 않습니다.

REQUIRED

  • 의미: 트랜잭션이 필요함(없으면 새로 만듬)
  • 기존 트랜잭션 없음: 새로운 트랜잭션을 생성함
  • 기존 트랜잭션이 있음: 기존 트랜잭션에 참여함

REQUIRED는 디폴트 속성으로써 모든 트랜잭션 매니저가 지원하는 속성이다. 별도의 설정이 없다면 REQUIRED로 트랜잭션이 진행된다.

 

 

SUPPORTS

  • 의미: 트랜잭션이 있으면 지원함(트랜잭션이 없어도 됨)
  • 기존 트랜잭션 없음: 트랜잭션 없이 진행함
  • 기존 트랜잭션이 있음: 기존 트랜잭션에 참여함

 

 

MANDATORY

  • 의미: 트랜잭션이 의무임(트랜잭션이 반드시 필요함)
  • 기존 트랜잭션 없음: IllegalTransactionStateException 예외 발생
  • 기존 트랜잭션이 있음: 기존 트랜잭션에 참여함

 

REQUIRES_NEW

  • 의미: 항상 새로운 트랜잭션이 필요함
  • 기존 트랜잭션 없음: 새로운 트랜잭션을 생성함
  • 기존 트랜잭션이 있음: 기존 트랜잭션을 보류시키고 새로운 트랜잭션을 생성함

 

NOT_SUPPORTED

  • 의미: 트랜잭션을 지원하지 않음(트랜잭션 없이 진행함)
  • 기존 트랜잭션 없음: 트랜잭션 없이 진행함
  • 기존 트랜잭션이 있음: 기존 트랜잭션을 보류시키고 트랜잭션 없이 진행함

 

NEVER

  • 의미: 트랜잭션을 사용하지 않음(기존 트랜잭션도 허용하지 않음)
  • 기존 트랜잭션 없음: 트랜잭션 없이 진행
  • 기존 트랜잭션이 있음: IllegalTransactionStateException 예외 발생

 

NESTED

  • 의미: 중첩(자식) 트랜잭션을 생성함
  • 기존 트랜잭션 없음: 새로운 트랜잭션을 생성함
  • 기존 트랜잭션이 있음: 중첩 트랜잭션을 만듬

NESTED는 이미 진행중인 트랜잭션에 중첩(자식) 트랜잭션을 만드는 것으로, 독립적인 트랜잭션을 만드는 REQUIRES_NEW와 다르다. NESTED에 의한 중첩 트랜잭션은 부모 트랜잭션의 영향(커밋과 롤백)을 받지만, 중첩 트랜잭션이 외부에 영향을 주지는 않는다.

즉, 중첩 트랜잭션이 롤백 되어도 외부 트랜잭션은 커밋이 가능하지만 외부 트랜잭션이 롤백되면 중첩 트랜잭션은 함께 롤백되는 것이다. NESTED는 JDBC의 savepoint 기능을 사용하는데, DB 드라이버가 이를 지원하는지 확인이 필요하며 JPA에서 사용이 불가능하다.

 

 

3. 트랜잭션의 전파 속성 요약


[ 트랜잭션의 전파 속성 요약 ]

 

참조 링크: https://mangkyu.tistory.com/269