Spring, Transaction을 위한 Self-Injection 활용하기

#spring#java#backend#transaction#self-injection

2022-12-21 16:07

대표 이미지

Spring을 이용해서 비즈니스를 구현하다보면 Bean에서 내부 메소드를 호출해야할 때가 있습니다. AOP 기술을 사용 중인 메소드인 경우, self- Injection이 이런 비즈니스 문제를 해결할 수 있습니다. 예제와 함께 어떻게 사용하는지 살펴봅시다.

개요

Spring을 이용해서 비즈니스 코드를 작성하다보면 Bean에서 Inner 메소드를 호출해야할 때가 있다. AOP 기술을 사용중인 메소드인 경우에는 ProxyBean의 특성 때문에 Inner 메소드 호출시 AOP가 적용되지 않는 경우가 존재한다. self-Injection은 이러한 비즈니스 문제를 해결하기 위해 사용할 수 있는 패턴이다. 아래에서 여러 예제를 통해 self-injection을 어떻게 사용하는지 알아보도록 하자.

무엇이 문제가 되는가?

Transactional과 같은 AOP 기술을 사용할 때, 외부에서 메소드를 호출하게 되면 Spring은 ProxyBean을 호출한다.

프록시 빈이 없는 경우

blog.jpg

프록시 빈을 쓰는 경우

blog2.jpg

In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation (in effect, a method within the target object calling another method of the target object) does not lead to an actual transaction at runtime even if the invoked method is marked with @Transactional. Also, the proxy must be fully initialized to provide the expected behavior, so you should not rely on this feature in your initialization code — for example, in a @PostConstructmethod.

- Spring 공식 문서 중 Data Access

위의 내용에 따라 Proxy 메소드는 인스턴스 외부에서 호출한 경우에만 적용된다.

  • Public 메소드에만 Transaction이 적용된다.
  • 이미 Proxy 메소드가 호출된 경우라면, Inner 메소드를 호출해도 ProxyBean이 재적용되지 않는다.

왜 Inner로 호출하나요?

다양한 이유가 존재하지만 대부분 기존의 코드를 재사용하는 경우 이러한 상황을 경험하게 된다. 아래 코드는 이런 상황의 극단적인 예시라고 볼 수 있다.

@Service
public class SomeService {

  public void someFacadeBusiness() {
     Some some = txMethod();
     //do Something
  }
  @Transactional  
  public Some txMethod() {
    . . . . . .
  }
  
}

위의 코드는 다음과 같은 요구사항을 만족하기 위해 작성된 코드라고 볼 수 있다.

  1. someFacadeBusiness 메소드에 Transactional 적용을 하고 싶지 않다.
  2. txMethod 에는 Transactional을 적용하여 비즈니스 Entity가 동시에 저장되길 원한다.
  3. someFacadeBusiness는 txMethod의 결과를 받아 또 다른 트랜잭션 비즈니스를 수행하고 싶다.

해결책 하나 - Service 분리

문제를 해결하기 위해서 기존의 전통적인 방법에서는 아래와 같이 구현했다. Spring Proxy 패턴이 지원하는 범위 때문에 호출 서비스 자체를 분리하는 것이 자칫 너무 큰 작업처럼 보일 수 있다. 그럼에도 이 방법이 빠르게 문제를 해결할 수 있는 좋은 해결책인 것은 확실하다.

@Service
public class FacadeService {

  @Autowired
  private SomeService someService;

  public void someFacadeBusiness() {
     Some some = someService.txMethod();
     //do Something
  }  
}

@Service
public class SomeService {
  @Transactional  
  public Some txMethod() {
    . . . . . .
  }
}

해결책 둘 - Self-injection

Self-injection을 통해 내부 서비스에서 @Transactional 을 포함하는 Proxy 패턴의 Public 메소드를 호출할 수 있다.

@Service
public class SomeService {

  private SomeService self;
  
  @Autowired
  public setSomeService(SomeService someService) {
    this.self = someService;
  }

  public void someFacadeBusiness() {
     self.txMethod();
     //do Something
  }
  @Transactional  
  public Some txMethod() {
    . . . . . .
  }
  
}

여기서 주의할 점은 아래와 같이 어노테이션을 사용하게 되면 Spring에서 Bean 등록을 지원해주지 않는다는 것이다. 따라서 Setter 방식의 Injection을 사용하면 자기 자신을 DI로 전달 받을 수 있다.

@Autowired
private SomeService someService;

정리하면서

이번 주제에 대한 해결책은 여러 곳에서 많은 설명을 찾아볼 수 있다. 그러나, 왜? 라는 질문에 대한 만족스러운 답변을 찾지 못해서 필자 스스로 찾아보면서 정리한 내용이다. 실제로 실무에서 복잡한 비즈니스를 구현하다보면 위와 같은 문제를 자주 경험하게 된다. 충분한 리소스가 있다면 기존의 전통적인 방법인 "Service 분리" 방식을 사용할 수 있겠지만, 최소한의 노력으로 빠르게 결과를 내야 하는 상황이라면 "Self-Injection" 방식을 사용해보는건 어떨까?

Ref.

https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction https://stackoverflow.com/a/5251930