@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은 AOP를 이용해@Transactional
어노테이션을 선언한 메서드가 실행되기 전 transaction begin
을 삽입하고, 메서드 실행 후에 Transaction commit
코드를 삽입하여, 객체 변경감지를 수행하도록 유도한다.
Spring의 코드 삽입 방식은 크게 2가지가 있다.
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
메서드를 사용해야만 트랜잭션 처리가 수행되게 된다.
BooksProxy
가 addBooks
메서드를 수행하면, 아래와 같은 순서로 작동된다.
BookProxy::addBooks
-> BooksImpl::addBook
즉, 프록시 객체가 아닌 실제 BookImpl
객체의 함수를 호출하게 되므로 해당 메서드는 트랜잭션으로 감싸지지 않은 상태로 @Transactional
어노테이션 기능이 수행되지 않는 것이다.
@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 사용)