pooney
article thumbnail

 

 

 

오늘은 Transaction Propagation에 대해 알아 보려고 합니다. Propagation은 말그래도 "전파" 라는 의미를 가집니다. 간단하게 설명하면 rollback여부의 범위를 정한다라고도 볼수 있습니다. Propagation은 여러개가 존재합니다. 

 

 

 

 

Tranaction Propagation의 속성은 아래와 같습니다. 더 다양한 속성들이 존재하는데 나머지 부분들은 아래 Do

c을 확인 해주세요. 오늘은 REQUIRED, REQUIRES_NEW가 어떻게 동작하는지 알아 보려고 합니다.

 

 

 

Tranaction Propagation

 

 

REQUIRED (Default)

기존 트랜잭션이 존재한다면 해당 트랜잭션에 참여한다.  

 

REQUIRES_NEW

새로운 트랜잭션을 생성한다. 기존 트랜잭션이 존재한다면 일시중단한다.

 

MANDATORY

현재 트랜잭션을 지원하고 존재하지 않는 경우 예외를 던집니다.

 

NEVER

비트랜잭션으로 실행하고 트랜잭션이 있으면 예외를 던집니다.

 

 

 

 

 

 

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Propagation.html

 

Propagation (Spring Framework 5.3.22 API)

Support a current transaction, execute non-transactionally if none exists. Analogous to EJB transaction attribute of the same name. Note: For transaction managers with transaction synchronization, SUPPORTS is slightly different from no transaction at all,

docs.spring.io

 

 

 

 

 

 

혹시 Transaction 설명, rollback이 어떻게 동작되는지 모르겠다면 이전에 작성한 트랜잭션을 참고해주세요. 

 

2022.07.01 - [Spring boot] - spring boot - 트랜잭션

 

 

 

 

 

 

그러면 이번에는 Game을 등록 하는 method와 Game History를 등록하는 inner method를 구현하여 Transaction Propagation를 테스트해보는 간단한 예제를 진행해 보도록  하겠습니다. 

 

 

 

 

 

Game

@Entity
@Setter
@Getter
@NoArgsConstructor
public class Game {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long idx;

    @Column(name = "name")
    private String name;

    @Builder
    public Game(String name) {
        this.name = name;
    }
}

 

 

GameHistory

@Builder
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class GameHistory {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long idx;

    @JoinColumn(name = "game_idx")
    @ManyToOne
    Game game;

    @Column(name = "content")
    private String content;
}

 

 

 

 

 

 

 

Game 과 GameHistory의 관계는 1:M으로 설정했습니다. 

 

 

 

 

 

 

 

 

 

 

TransService

@Slf4j
@Service
@RequiredArgsConstructor
public class TransService {
    private final InnerTransService innerTransService;
    private final GameRepository gameRepository;
    private final PlatformTransactionManager platformTransactionManager;


    //게임의 전체 목록 조회
    public List<Game> getGameAllList(){
        return gameRepository.findAll();
    }

    //게임 저장
    @Transactional
    public void saveGame(ReqGaveSave reqGaveSave){
        TransactionStatus status = platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
        log.info("First isRollbackOnly : {}" , status.isRollbackOnly());
        Game game = Game.builder().name(reqGaveSave.getName()).build();
        //Game 저장
        gameRepository.save(game);
        //Game History 저장
        innerTransService.saveGameHistory(game, "등록");
        log.info("Last isRollbackOnly {}", status.isRollbackOnly());
    }
}

 

 

 

 

TranService에는 saveGame을 통하여 Game을 등록하고 InnerTransService의 sabeGameHistory를 호출하여 game history를 등록하는 간단한 구조로 만들어져 있습니다.

 

 

 

 

 

InnerTransService

@Slf4j
@Service
@RequiredArgsConstructor
public class InnerTransService {
    private final PlatformTransactionManager platformTransactionManager;
    private final GameHistoryRepository gameHistoryRepository;
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void saveGameHistory(Game game, String content){
        TransactionStatus status = platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
        log.info("Inner Transaction isRollbackOnly : {}" , status.isRollbackOnly());
        GameHistory gameHistory = GameHistory.builder()
                .game(game)
                .content(content)
                .build();
        gameHistoryRepository.save(gameHistory);
        throw new RuntimeException();
    }
}

 

 

TransService.saveGame  ->  InnerTransService.saveGameHistory를 호출하는 구조입니다.  저는 Transaction을 파악하기 위하여 PlatformTransactionManager을 주입 받았습니다. 

 

 

@Transaction 속성을 보시면 Propagation.REQUIRED가 보이는데 해당 속성은 별도 입력하지 않더라도 Defaut로 설정되어있습니다. 때문에 위에서 설명한거 처럼 TransService.saveGame, InnerTransService.saveGameHistory는 같은 트랜잭션을 공유할 것 입니다. 

 

 

위내용을 간략하게 설명하면 TransService.saveGame(부모) 에서 Game을 저장 후, InnerTransService.saveGameHistory(자식) 호출 하였고 자식에서는 GameHistory 저장한 후 RuntimeException이 발생하는 상황입니다.  그러면 어떠한 결과를 출력할까요?

 

 

 

 

 

 

 

 

 

 

 

결과는 자식에서 발생한 에러가 부모로 전파되어 부모, 자식 모두 rollback을 진행하게 됩니다.

 

 

여기서 isRollbackOnly가 무엇인지 의문이 드실수도 있는데요. 트랜잭션이 시작되면 isRollbackOnly의 값에 따라 최종적으로 rollback 또는 commit을 할지를 결정하게 됩니다. 만약 Exception이 발생하면  해당 Tranaction의 isRollbackOnly의 값은 True로 변경이 됩니다. 때문에 해당 트랜잭션이 종료 될 때 isRollbackOnly가 true이면 rollback을 진행하고 그렇지 않으면 commit을 진행하게 됩니다. 

 

 

 

지금 진행한 예제에서는 Propagation.REQUIRED 이기 때문에 부모,자식이 같은 트랜잭션을 사용하게 됩니다. 이때 자식에서 Exception이 발생하면서 해당 트랜잭션에서 isRollbackOnly가 true로 변경이 되고 같이 트랜잭션을 공유하는 부모에서는 트랜잭션이 종료되면서 isRollbackOnly가 true인것을 보고 rollback을 진행하게 되는 것입니다. 

 

 

 

 

 

그러면 여기서 만약 자식 호출부분에 try~catch로 감싸면 어떻게 될까요?

 

 

 

 

 

 

 

 

 

 

그러면 위와 같이 try~catch로 감싼 후 catch에 로그를 찍어서 확인 해보 겠습니다. 

 

 

 

 

 

 

 

 

 

결과를 보시면 아래와 같은 에러가 발생하는 것을 확인 할 수 있습니다.

 

 

트랜잭션은 무조건 rollback 또는commit 둘중 하나만 진행 할 수 있습니다. 하지만 지금 진행한 예제에서는 try ~ catch로 예외를 잡았지만  Propagation.REQUIRED 로 설정했기 때문에 부모,자식이 트랜잭션을 공유합니다.

 

 

여기서 자식에서는 예외가 발생하여 해당 트랜잭션에 rollbackOnly를 true변경을 시켰지만 부모에서는 try ~ catch로인해 자식에서 예외가 발생한 것을 모르고 commit을 시도합니다. 그러나 공유하는 트랜잭션은 자식에 의해 rollbackOnly가 ture로 변경되었기 때문에 rollback을해야하는데 commit을 하라고 하니 exception을 발생시키는 것입니다. 

 

 

 

org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:752) ~[spring-tx-5.3.7.jar:5.3.7]
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711) ~[spring-tx-5.3.7.jar:5.3.7]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654) ~[spring-tx-5.3.7.jar:5.3.7]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407) ~[spring-tx-5.3.7.jar:5.3.7]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.7.jar:5.3.7]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.7.jar:5.3.7]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.7.jar:5.3.7]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) ~[spring-aop-5.3.7.jar:5.3.7]
	at com.example.trans.service.TransService$$EnhancerBySpringCGLIB$$f57bb3be.saveGame(<generated>) ~[main/:na]
	at com.example.trans.controller.TranController.saveGame(TranController.java:30) ~[main/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]

 

 

 

 

catch부분에 break point를 찍고 확인한 transaction의 상태입니다. 값이 rollbackOnly가 true인 것을  확인 하 실 수 있습니다. 이렇듯 Propagation.REQUIRED는 서로 트랜잭션을 공유를 하는것을 확인 할 수 있었습니다.

 

 

 

 

 

 

 

 

 

 

 

 

자 이번에는  Propagation.REQUIRES_NEW 로 속성을 변경 해 보겠습니다. 

 

 

 

 

 

 

 

 

 

결과는 과연 어떻게 나올까요?

결과는 아래와 같이 "Lock wait timeout exceeded; try restarting transaction"이 발생하고 중단 되었습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

그러면 왜? timeout이 발생했을까요? 그 이유는 아래의 그림과 같습니다. 

 

 

 

 

 

 

 

 

 

 

 

 

REQUIRES_NEW  설정으로 인해 트랜잭션이 다르기 때문입니다. 같은 트랜잭션 내에서는 LOCK을 걸었어도 서로 레코드,테이블등 공유가 가능하지만 다른 트랜잭션에서 작업중인 레코드에 수정하거나 삭제를 하는 것은 LOCK을 걸었기 때문에 불가능합니다. 

 

위의 상황에서는 saveGame-tx에서는 saveGameHistory-tx가 끝나야 COMMIT과 동시에 game의 LOCK을 해제할 수 있고 saveGameHistory-tx에서는 saveGame-tx에서 game의 Lock을 해제 해야지만 game_history에 insert하고 자신의 트랜잭션을 종료 할수가 있습니다. 즉 saveGame-tx 와 saveGameHistory-tx 간에 교착상태가 발생하여 timeout이 발생한 것입니다. 

 

 

 

이렇듯 REQUIRES_NEW 를 함부러 사용하면 이러한 장애를 만날 수 있으니 설계를 잘 하셔야 합니다. 저는 장애를 해결 하기 위하여 연관관계를 지우도록 하겠습니다. 

 

 

 

 

GameHistory

@Builder
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class GameHistory {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long idx;

//    @JoinColumn(name = "game_idx")
//    @ManyToOne
//    Game game;

    @Column(name = "game_idx")
    private Long gameIdx;

    @Column(name = "content")
    private String content;
}

 

 

 

 

InnerTransService

@Slf4j
@Service
@RequiredArgsConstructor
public class InnerTransService {
    private final PlatformTransactionManager platformTransactionManager;
    private final GameHistoryRepository gameHistoryRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveGameHistory(Game game, String content){
        TransactionStatus status = platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
        log.info("Inner Transaction isRollbackOnly : {}" , status.isRollbackOnly());

        GameHistory gameHistory = GameHistory.builder()
                .gameIdx(game.getIdx()) //gameIdx로 변경
                .content(content)
                .build();
        gameHistoryRepository.save(gameHistory);
        throw new RuntimeException();
    }
}

 

 

 

 

 

 

gameIdx로 변경하시고 실행을 해보시면 기존에 timeOut을 발생시켰지만  아래와 같은 정상? 장애와 함께 request를 완료합니다.

 

 

 

 

 

 

 

 

 

 

 

위의 결과는 아래와 같이 정리 할 수 있습니다. 

 

 

 

 

 

[1번] game을 insert하고  saveGame-tx는 일시중단하고 새 트랜잭션 saveGameHistory-tx가 생성

 

[2번] saveGameHistory-tx는 RuntimeException이 발생하여 rollback을 진행합니다.

 

[3번] 중단되었던  saveGame-tx가 다시 실행됩니다. 

 

[4번] REQUIRES_NEW로 인한 saveGame-tx 와 saveGameHistory-tx는 서로 다른트랜잭션 으로 exception isRollbackOnly의 값은 false

 

[5번] 서로 다른 트랜잭션임으로 saveGameHistory-tx에서 RuntimeException가 발생했음에도 불구하고 [4번]에서 false아님으로 saveGame-tx는 commit을 진행 

 

[최종] 아래와 같이 game 테이블에서는 정상적으로 INSERT 가 되고 GAME_HISOTRY는 rollback이 진행 된 것을 확인 할 수 있습니다. 

 

 

 

 

 

 

 

 

 

 

 

이번에 트랜잭션 전파에 대해서 알아보았는데요. 트랜잭션을 사용하기는 하는데 정확하게 어떻게 동작하고 하는지 모르고 사용하는 경우가 많아 이번 기회에 정확하게 알고 사용하자 해서 이렇게 작성하게 되었습니다.  대부분 잘 모르고 사용하여 timeout과 같은 에러가 발생하는데 왜 걸리는지 이해 못하고 넘어가는 경우가 많은데 제글이 도움이 되었으면 합니다. 

그리고 공부 목적으로 만들어가고 있어 많이 부족합니다. 제가 작성한 글에 틀린부분이 있다면 댓글 달아주시면 감사하겠습니다. 

 

 

 

 

 

 

 

 

 

profile

pooney

@pooney

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!