티스토리 뷰

Spring

[Spring] 스프링의 트랜잭션 전파 속성(Transaction propagation) 완벽하게 이해하기

망나니개발자 2022. 8. 22. 10:00
반응형

아래의 내용은 김영한님의 디비 접근 기술 2편 강의와 토비의 스프링 등을 바탕으로 정리한 내용입니다.

 

 

 

 

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


[ 트랜잭션의 시작과 종료 ]

트랜잭션은 시작 지점과 끝나는 지점이 존재한다. 시작하는 방법은 1가지이지만 끝나는 방법은 2가지이다. 트랜잭션이 끝나는 방법에는 모든 작업을 확정짓는 커밋(commit)과 모든 작업을 무효화하는 롤백(rollback)이 있다.

 

 

트랜잭션의 시작

트랜잭션은 하나의 Connection을 가져와 사용하다가 닫는 사이에서 일어난다. 트랜잭션의 시작과 종료는 Connection 객체를 통해 이뤄지기 때문이다. JDBC의 기본 설정은 DB 작업을 수행한 직후에 바로 커밋을 하는 자동 커밋 옵션이 활성화되어 있다. 그러므로 JDBC에서 트랜잭션을 시작하려면 자동 커밋 옵션을 false로 해주어야 하는데, 그러면 새로운 트랜잭션이 시작되게 만들 수 있다.

public void executeQuery() throws SQLException {
    Connection connection = dataSource.getConnection();
    connection.setAutoCommit(false);
    // 트랜잭션 시작
    
    ...
}

 

 

스프링을 이용하면 내부적으로 커넥션을 갖고 있는 추상화된 트랜잭션 매니저를 이용하게 된다. 이 때에는 다음과 같이 트랜잭션을 시작하게 되고, 자동 커밋 옵션을 변경하는 등의 작업은 트랜잭션 매니저 내부에서 진행된다.

public void executeQuery() throws SQLException {
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    // 트랜잭션 시작
    
    ...
}

 

 

 

 

트랜잭션의 종료

하나의 트랜잭션이 시작지면 commit() 또는 rollback() 호출될 때 까지가 하나의 트랜잭션으로 묶인다. 이렇게 setAutoCommit(false)로 트랜잭션의 시작을 선언하고 commit()또는 rollback()으로 트랜잭션을 종료하는 작업을 트랜잭션의 경계설정이라고 한다.

트랜잭션의 경계는 하나의 Connection을 통해 진행되므로 트랜잭션의 경계는 하나의 커넥션이 만들어지고 닫히는 범위 안에 존재한다.

public void executeQuery() throws SQLException {
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    // 트랜잭션 시작
    
    try {
        // 쿼리 실행
        ...
            
        transactionManager.commit(status);
    } catch (Exception e) {
        transactionManager.rollback(status);
    }
}

 

 

 

 

 

 

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

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

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

 

 

 

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

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

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

 

 

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

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

 

 

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

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

 

 

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

 

 

 

 

 

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


[ REQUIRED 속성과 REQUIRES_NEW 속성 ]

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

 

 

REQUIRED

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

 

 

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

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

하지만 외부 트랜잭션에서 롤백이 되는지 혹은 내부 트랜잭션에서 롤백이 되는지에 따라 동작 방식에 차이가 존재한다. 먼저 로직1에서 예외가 발생하여 외부 트랜잭션의 롤백이 필요한 경우에는 내부 트랜잭션의 커밋/롤백 여부와 무관하게 물리 트랜잭션에서 롤백을 시킨다. 마찬가지로  로직1은 커밋하고 로직2에서 예외가 발생하여 내부 트랜잭션에서 롤백되는 경우에도, 최종적으로 물리 트랜잭션에 롤백이 된다는 결과는 동일하다. 하지만 실제로 물리 트랜잭션을 관리하는 외부 트랜잭션에서는 커밋을 기대했지만 내부 트랜잭션이자 논리 트랜잭션에서 롤백을 필요로 하기 때문에, 외부 트랜잭션이자 물리 트랜잭션 입장에서는 예상치 못한 상황이라고 볼 수 있다. 따라서 내부 트랜잭션에서는  UnexpectedRollbackException 예외를 발생시켜 롤백 필요성을 알리고, 외부 트랜잭션에서는 이를 바탕으로 롤백을 처리한다.

 

 

 

 

 

 

REQUIRES_NEW

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

 

 

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

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

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

 

 

 

 

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

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

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

 

 

REQUIRED

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

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

 

 

 

SUPPORTS

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

 

 

 

MANDATORY

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

 

 

REQUIRES_NEW

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

 

 

NOT_SUPPORTED

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

 

 

NEVER

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

 

 

NESTED

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

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

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

 

 

 

 

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


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

 

 

 

 

 

 

위의 내용은 김영한님의 디비 접근 기술 2편 강의와 토비의 스프링 등을 바탕으로 정리한 내용입니다. 김영한님 강의를 듣지 않으셨다면 직접 들어보시는 것을 추천드립니다! 감사합니다:)

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함