기록하며 성장한다 - 개발, 회고

개발 회고록

[ 개발 회고록 ] Self - Invocation 에 대한 간단한 이야기

전대홍 2024. 1. 23. 23:34

 


서론

현재 배달과 관련 된 토이프로젝트를 진행하고 있다.

토이 프로젝트 링크 <<<

필자가 개발을 진행하면서, 주문 관련 로직을 만든게 있다. 위 링크에서 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 까지 확실히 공부하고 알아야 할 것이다.

필자도 아직 부족한게 많지만, 더욱 공부하여 나아갈 것이다.