[Spring] @Transactional 클래스 내부 호출 미작동 이슈

donghyeok·2022년 5월 13일
4

Spring

목록 보기
1/9

문제 상황

@Transactional 사용시, 아래와 같은 클래스에서 àddBook메서드를 Books 클래스 내부에서 사용할 때 발생할 수 있다. (프록시를 이용한 Spring AOP에서 공통적으로 발생하는 문제)

public class BookImpl implements Books {
	public void addBooks(List<String> bookNames) {
    	bookNames.forEach(bookName -> this.addBook(bookName));
    }
    
    @Transactional
    public void addBook(String bookName) {
    	Book book = new Book(bookName);
        bookRepository.save(book);
        book.setFlag(true);
    }
}

위와 같은 상황에서 addBooks메서드의 내부에서 호출하는 addBook 메서드의 @Transactional 어노테이션이 적용되지 않는 다는 것이다.

왜 동작하지 않았는지는 Spring에서 @Transactional 어노테이션을 제공하는 방식에 대해 살펴볼 필요가 있다.

Spring @Transactional 기능 제공 방식

Spring은 AOP를 이용해@Transactional 어노테이션을 선언한 메서드가 실행되기 전 transaction begin을 삽입하고, 메서드 실행 후에 Transaction commit 코드를 삽입하여, 객체 변경감지를 수행하도록 유도한다.

Spring의 코드 삽입 방식은 크게 2가지가 있다.

  • 바이트 코드 생성(CGLIB)
  • 프록시 객체 사용

2가지 방법 중에 Spring은 기본적으로 프록시 객체 사용 방식을 선택한다. (SpringBoot는 기본적으로 바이트 코드 생성 방식 사용)

원리는 아래 코드와 같다. 프록시 객체로 우리가 만든 메서드를 한번 감싸서, 메서드 위 아래로 코드를 삽입한다.

public class BooksProxy {
  private final Books books;
  private final TransactonManager manager = TransactionManager.getInstance();
  
  public BooksProxy(Books books) {
    this.books = books;
  }
  
  public void addBook(String bookName) {
    try {
      manager.begin();
      books.addBook(bookName);
      manager.commit();
    } catch (Exception e) {
      manager.rollback();
    }
  }
}

위와 같은 코드로 인해 우리가 BookImpl클래스를 사용할 때, 스프링이 제공하는 BookProxy 객체를 사용하게 되며, 프록시 객체가 제공하는 addBook 메서드를 사용해야만 트랜잭션 처리가 수행되게 된다.

@Transactional이 수행되지 않은 이유

BooksProxyaddBooks 메서드를 수행하면, 아래와 같은 순서로 작동된다.
BookProxy::addBooks -> BooksImpl::addBook

즉, 프록시 객체가 아닌 실제 BookImpl 객체의 함수를 호출하게 되므로 해당 메서드는 트랜잭션으로 감싸지지 않은 상태로 @Transactional 어노테이션 기능이 수행되지 않는 것이다.

해결 방법

1. @Transactional 어노테이션 메서드는 클래스 내부적으로 사용하지 말고, 밖에서 사용. (권장)

2. 굳이 내부적으로 사용하려면, 자기 자신의 Proxy 객체를 사용하여 처리

@Service
public class BooksImpl implements Books {
  @Autowired
  private Books self;
  
  public void addBooks(List<String> bookNames) {
    bookNames.forEach(bookName -> self.addBook(bookName)); // this 가 아닌 변수 self 로
  }
  
  @Transactional
  public void addBook(String bookName) {
    Book book = new Book(bookName);
    bookRepository.save(book);
    book.setFlag(true);
  }
}

위 코드는 Books 인터페이스를 이용하여 BooksProxy 인스턴스를 주입할 수 있도록 유도한다.
그 후, 프록시 객체의 addBook 메서드 사용을 통해 @Transactional 어노테이션 기능을 사용할 수 있게 된다.

(위 예제에서 생성자 주입 사용하면, 순환 에러 발생. @Autowired 사용)

0개의 댓글