
서론
현재 배달과 관련 된 토이프로젝트를 진행하고 있다.
필자가 개발을 진행하면서, 주문 관련 로직을 만든게 있다. 위 링크에서 OrderService 를 보면 있는데, OrderService 와 OrderTransactionService 이렇게 2개로 나눠놓은 것을 확인할 수 있다.
기존에는 OrderService 하나에서 주문과 결제로직이 모두 처리되도록 만들었지만, 주문과 결제 로직을 분리하여 별도의 물리 트랜잭션을 할당해야 할 경우도 생길 수 있을거란 생각이 들었다.
그렇게되면 OrderService 안에서 @Transactional 이 붙은 주문 메서드와, 결제 관련 메서드를 불러와서 진행시켜야 되는데, 같은 서비스 내에서 그렇게 불러오게되면 발생하는 문제가 바로 Self-Invocation 문제이다.
참고로, 필자는 이 Self-Invocation 문제점을 이해하다보니 프록시 개념과, AOP 에 대해서 더 잘 이해할 수 있게 되었다. 이 부분을 꼭 공부해보는 것을 추천한다.
본론
1. Self Invocation 이 뭘까?
일단 아래 코드를 보자.
@Service
public class OrderService {
// ...
@Transactional
public void registerOrder() {
// 주문 로직
// 결제 로직
}
// ...
}
이건 필자가 기존에 개발한 방식의 코드이다.
그런데 이렇게 개발해놓고 보니,
주문 로직과, 결제 로직을 별도의 트랜잭션으로 처리해야 할 수도 있을 수 있다는 생각이 들었고,
재사용성을 고려하여 SRP 도 지키고 싶었기 때문에 분리를 해야겠다는 생각이 들었다.
@Service
public class OrderService {
// ...
@Transactional
public void registerOrder() {
order();
payment();
}
@Transactional
public void order() {
// 주문 로직
}
@Transactional
public void payment() {
// 결제 로직
}
// ...
}
이게 위 내용들을 고려하여 수정한 코드이다.
그런데, 여기서 문제점이 있다.
바로 Self-invocation 문제가 발생한다는 것이다.
Self-Invocation 을 이해하려면, Spring AOP 동작과정을 생각해보면 쉽다. Spring AOP 는 AOP 적용 여부를 판단하여 프록시 객체를 생성한다. 그리고 이렇게 생성된 프록시 객체의 핵심적인 기능은 바로 지정된 메서드가 호출 될 때, 그걸 가로채어 앞 뒤로 부가 기능을 부여할 수 있다는 것이다.
AOP 도 그러한 방식으로 공통된 횡단 관심사를 처리하게 된다.
그리고 본 문제인 @Transactional 어노테이션도 그러한 프록시 객체로 생성이 된다.
메서드 앞 뒤로, 트랜잭션의 시작과 끝을 확인할 수 있는 기능을 추가하고, 그로 인해 commit 혹은 rollback 기능을 수행한다. 위 코드를 보면, registerOrder 메서드에 @Transactional 이 붙어있고, 그 안에서 @Transactional 이 붙은 order 와 payment 라는 메서드를 호출한다.
이렇게 될 경우 처음 registerOrder 는 트랜잭션이 잘 붙어서 동작한다.
그러나, order 와 payment 는 그렇지 않다.
왜냐하면, registerOrder 는 프록시 객체로 실행되지만, 그 안에서 부른 order 와 payment 는 this.order, this.payment 이므로, 원본 객체의 메서드를 불러오기 때문이다.
물론 위 코드에서는 어차피 기본 트랜잭션이므로, registerOrder 에 트랜잭션이 붙어있어서, 그 트랜잭션을 따라가면 된다.
그러나, 앞에서 이야기한 것처럼 Propagation.REQUITES_NEW 를 활용하여 각각 별도의 물리 트랜잭션을 부여할 것이라면...?
그건 적용이 되지 않게된다.
그리고 당연한 이야기지만, registerOrder 에 Transactional 을 빼버리면, order payment 에 Transactional 이 붙어있다고 해도 트랜잭션은 없는 거나 마찬가지가 된다.
그게 Self-Invocation 문제이다.
더 자세히 정리는 필자의 TIL 블로그에 정리하였다. >>> Transaction 정리
2. 해결 방법
사실 AspectJ 나, 클래스 내 로직을 AOP로 완전히 묶는 방법등이 있지만, 둘 다 추천하는 방식은 아니다.
그래서 제일 좋은건 Self-Invocaton 이 발생하는 경우 자체를 막는 것이다.
그래서 필자는 OrderTransactionService 라는 별도의 서비스 클래스를 만들었고,
위의 order 나 payment 와 같은 메서드를 해당 서비스로 분리해내었다.
@Service
public class OrderService {
// ...
@Transactional
public void registerOrder() {
orderTransactionService.order();
orderTransactionService.payment();
}
// ...
}
@Service
public class OrderTransactionService {
// ...
@Transactional
public void order() {
// 주문 로직
}
@Transactional
public void payment() {
// 결제 로직
}
// ...
}
결론
스프링으로 개발을 하면서 제일 중요하다고 느끼는 것들이 있는데,
그 중 하나가 AOP 와 Transaction 이다.
특히 Transaction 은 필자가 처음 개발을 공부할 때는 단순히 서비스 로직 위에 그냥 붙이는 어노테이션 정도로만 알고있었다.
하지만, 개발자로서 품질 높은 서비스를 개발하기 위해서라면, Transaction 은 A ~ Z 까지 확실히 공부하고 알아야 할 것이다.
필자도 아직 부족한게 많지만, 더욱 공부하여 나아갈 것이다.
'개발 회고록' 카테고리의 다른 글
[ 개발 회고록 ] @RestAdviceController 와 @ExceptionHandler 로 예외 처리하기 (1) | 2024.01.24 |
---|---|
[ 개발 회고록 ] Redis Session으로 분산 환경에서 세션 관리하기 (0) | 2024.01.24 |
[ 개발 회고록 ] Redis INCR 를 활용하여 선착순 쿠폰 발급 기능 만들기 (0) | 2024.01.23 |
[ 개발 회고록 ] 낙관적 Lock 에서 @Transacional 을 사용하면 안되는 이유 + Lock 을 통한 라이더 배차 서비스 개발 (2) | 2024.01.23 |
[ 개발 회고록 ] 테스트 코드 작성 시, @Transactional 어노테이션을 사용하는 것에 대한 짧은 생각 (1) | 2024.01.10 |