[F-Lab 모각코 챌린지 55일차] @Transactional : 내부 method로 사용하면 안된다? !

부추·2023년 7월 25일
0

F-Lab 모각코 챌린지

목록 보기
55/66

수요일 오후, 도서관에 앉아 평화롭게 Spring boot 프로젝트를 진행하고 있던 부추..

게시글에 좋아요 기능을 달기 위해 DB 수정 작업을 진행하던 도중, 무언가 이상함을 느끼게 된다.

해야하는 일은 getDataById()를 통해 얻은 DB data의 필드값 하나(좋아요 개수)를 바꾸는 일이었다. @Transactional 붙인 뒤에 data.increaseLikeNum() 이런거 하나 정의해서 뚝딱! 하면 되는거 아닌가 하고 별생각 없었는데, 이걸.. DB에 언제 어떻게 저장해야하는지.. 저 메쏘드를 언제 호출해야하는지 단계에서 막혔다.

Optional 처리를 위해 data 객체를 @Transactional이 붙은 다른 내부 private method를 통해 불러왔다.

어, 이렇게 되면 내부 method를 통해 불러온 객체는 해당 method에서의 영속성 context에 들어가는건가? 아니면 새로운 context가 만들어지는것인가? 아니, 애초에 transaction이 똑바로 일어나긴 하나?

모르니까 불안하다.. 뭘 할 수도 없다. 알아야 한다.

일단 내부 호출에서 @Transactional 처리가 어떻게 이뤄지는지 알아보자.

Spring

위의 블로그 글에선 @Transactional이 붙은 internal() method를 일반 external() method가 호출하는 예시를 들었다. 대충 아래 코드와 같다고 생각하면 된다.

void external() { log.info("external!"); internal(); } 

@Transactional void internal() { log.info("intrnal!") }

@Transactional 어노테이션이 붙은 method는 프록시를 통해 호출된다. 만약 internal 메쏘드 자체를 호출하게 된다면 프록시가 트랜잭션 처리를 한 후 실제 클래스의 internal 메쏘드를 호출할 것이다. 아래 그림과 같은 원리로 말이다.

이번엔 internal()을 내부 메쏘드로 이용하는 external()을 호출할 때를 알아보자. external을 호출할 때, 프록시는 따로 트랜잭션 처리를 하지 않는다. @Transactional 어노테이션이 붙지 않았기 때문이다. 이제 별다른 Aspect 없이 바로 실제 클래스의 external이 호출되고, external 내부의 internal method 가 호출될 차례다.

근데! 여기서 호출되는 실제 internal() method는 트랜잭션 처리가 되지 않는다. 트랜잭션 관련 aop 처리를 하는 프록시 객체가 아니라 실제 객체가 internal 호출을 진행하기 때문이다.

도표로 보니 확실히 이해가 된다. 결국 transaction 처리를 위해선 "프록시가 해당 method를 call"해야한다.

그러면 여기서 의문이 든다. 아아 트랜잭션이 아닌 메쏘드 external() 내부로 트랜잭션 메쏘드를 호출하면 트랜잭션 처리가 안된다는 걸 알았어. 그러면 external()에도 @Transaction을 붙이면 되는거 아닌가.

어, 그러면 언제 dirty checking을 하는거지? internal() 내부의 엔티티들이 save되는 시점은 언제지? 트랜잭션(부모) 진행중 내부 메쏘드로 또다른 트랜잭션(자식)이 진행되면 어떻게 되는거지?

[Spring] 트랜잭션의 전파 설정별 동작

트랜잭션의 전파 설정이란 Spring에서 사용하는 어노테이션 '@Transactional'은 해당 메서드를 하나의 트랜잭션 안에서 진행할 수 있도록 만들어주는 역할을 합니다. 이때 트랜잭션 내부에서 트랜잭

deveric.tistory.com
갓.. 데브님들. 역시 찾아보니 답이 있었다.

트랜잭션을 진행중에 또다른 트랜잭션이 호출되었을 때 어떻게 처리할 것인지 @Transaction(propagation = "") 형태로 트랜잭션 전파 설정을 붙여 옵션 조정을 할 수 있었다. 자세한 사항은 위의 블로그 글에 있고, 간단하게 각 옵션을 정리해보면.

  • REQUIRED (default) : 부모의 트랜잭션에 자식 트랜잭션이 합류한다. 부모 트랜잭션에서 커밋이 일어날 때 자식 트랜잭션의 커밋도 함께 일어난다. 만약 부모 트랜잭션이 없다면 새로 트랜잭션을 생성한다.

  • REQUIRES_NEW : 자식만의 새로운 트랜잭션을 새로 생성한다. 각각 트랜잭션의 커밋과 롤백은 따로 일어난다. 서로 영향을 주지 않는다.

  • MANDATORY : 부모의 트랜잭션에 자식 트랜잭션이 합류한다. 부모 트랜잭션에서 커밋이 일어날 때 자식 트랜잭션의 커밋도 함께 일어난다. 만약 부모 트랜잭션이 없다면 예외를 던진다.

  • NESTED : 자식 트랜잭션 기준 커밋은 함께, 롤백은 중첩 이전까지만 진행된다. 자식 트랜잭션 진행 중 롤백이 일어나면 자식 트랜잭션의 시작 지점까지만 롤백이, 부모 트랜잭션 진행중 롤백이 일어나면 모든 진행사항이 롤백된다.

  • NEVER : 트랜잭션을 생성하지 않는다. 부모 트랜잭션이 존재하면 예외를 던진다.

  • SUPPORTS : 부모 트랜잭션이 존재할 때만 합류한다.

propagation 옵션을 따로 지정하지 않으면 REQUIRED로 설정되고, 이말은 nested transaction이 발생할 때 최초의 트랜잭션에 모두 합류한다는 뜻이 된다.

중첩된 트랜잭션 메쏘드를 통해 엔티티값을 요리저리 바꾸면 커밋은 결국 최초의 트랜잭션 메쏘드가 끝날 때 일어난다는 것이다.

물론!! 가독성과 복잡도의 증가로 이어질 혹시모를 참사를 위해 내부 메쏘드는 지양하는게 옳을 것 같다.

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글