Transactional과 Spring AOP 스쳐가보기

Soondol·2024년 5월 10일

문제발생 !

public class Exam() {
  public void A () {
      ~~~
      B ();
      ~~~
  }

  @Transactional
  public void B () {
      ~~~
  }
}

위와같이 한 클래스 내에 A, B 2개의 메소드를 작성했고,
B에 트랜잭션 어노테이션을 붙인후에 A의 내부에서 호출되도록 했다.

저렇게 작성한 이유는
분산락을 이용해 동시성 문제를 해결해보고자하는 과정에서
분산환경에서의 프로세스를 아래와 같은 순서로 진행하게끔 하고싶어서였다.

  • 데이터, 트랜잭션의 일관성 유지
  • 트랜잭션이 실행되고 이후에 락 획득에 실패하게되는 무의미한 트랜잭션 실행 방지

락 획득 > 트랜잭션 실행 > 비즈니스로직 > 트랜잭션 커밋 > 락 해제

하지만 무지성으로 트랜잭션이 걸려있는 메소드를 분산락을 가진 분기의 내부로 넣어봤을때 테스트는 올바르게 실행되지 않았다..

왜..?

  • Spring의 AOP (Aspect Oriented Programing) 관점지향형 프로그래밍
    - 반복사용되는 로직들을 모듈화 하여 필요할때 호출해서 사용하는 방법
    - 다이나믹 프록시 혹은 CGLib(Spring boot, JPA hibernate default)로 생성된 proxy 객체를 사용

      proxy?
    	어떤 객체를 사용하고자 함 
    	-> 객체를 직접적으로 참조하는것이 아닌 해당 객체를 대행하는 객체(proxy)를 이용해 대상 객체에 접근
  • Transactional은 대표적인 AOP가 적용되는 사례.

    AOP에서 proxy의 동작과정
    proxy를 통해 들어오는 외부 호출을 인터셉트해 작동하게됨

    	위의 이슈케이스의 경우    
    	exam class의 A 호출 
    	-> proxy! A호출해줘! ( proxy.A(); )
    	-> proxy 내부의 A(), B() 에서 A()가 호출됨
    	-> A() 로직에서 proxy내부의 B()를 호출하게됨 = self-invocation
  • @Transactional 어노테이션은 proxy외부에서 접근해야 AOP가 적용됨 !

해결해보기

  1. 트랜잭션을 수동으로 관리
public class Exam() {
  public void A () {
  	  TransactionDefinition def = new DefaultTransactionDefinition();
      TransactionStatus status = transactionManager.getTransaction(def);
      ~~~
      try {
        B ();
        transactionManager.commit(status);
      } catch (Exception e) {
      	transactionManager.rollback(status);
      }
      ~~~
  }

  public void B () {
      ~~~
  }
}
  • 관리의 복잡성을 증가시키며, 코드의 가독성 저하..
  1. 분리된 컴포넌트 사용
@Service
public class ExamB {
    @Transactional
    public void B() {
        ~~~
    }
}

@Component
@RequiredArgsConstructor
public class Exam {
    private final ExamB examB;

    public void A() {
        ~~~
        examB.B(); // B 메소드 호출
        ~~~
    }
}
  • 분리된 컴포넌트를 통해 B() 메소드가 호출될 때 Spring AOP proxy를 통해 트랜잭션 기능이 적용될 수 있다.

0개의 댓글