안녕하세요. 오늘은 Spring 의 @Transactional 어노테이션을 사용할 때, 주의점에 대해 포스팅합니다.
Spring 에서 JPA 기술을 쓸 때 빼놓을 수 없는 기능중 하나는 @Transactional 입니다.
아래와 같이 많은 편리성을 우리에게 제공해주기 때문이지요.
- transaction
begin,commit을 자동 수행해준다. - 예외를 발생시키면,
rollback처리를 자동 수행해준다.
그렇기에, 많은 보일러코드를 줄 일 수 있기 때문에, 아래와 같이 편리하게 코드를 작성할 수 있습니다.
Note.문제 상황을 억지로 만들기 위해 코드 효율성이 좋지 않을 수 있습니다.
public class BooksImpl implements Books { public void addBook(String bookName) { Book book = new Book(bookName); bookRepository.save(book); book.setFlag(true); }}
문제는 예시에서 쓴 addBook 메서드를 Books 클래스 내부에서 사용할 때 발생 할 수 있습니다.
xxxxxxxxxxpublic class BooksImpl implements Books { public void addBooks(List<String> bookNames) { bookNames.forEach(bookName -> this.addBook(bookName)); } public void addBook(String bookName) { Book book = new Book(bookName); bookRepository.save(book); book.setFlag(true); }}
혹시, addBooks 메서드를 사용할 때, 어떤것이 문제가 될 수 있는지 예상이 가시나요?
문제는 addBooks 메서드 내부에서 호출하는 addBook 메서드의 @Transactional 어노테이션이 적용되지 않는 것입니다.
그렇기에, 해당 코드를 실행하더라도 Database에는 저장된 Book 정보에 Flag 컬럼에 정상적으로 업데이트 되지 않습니다.
bookRepository 의 save 메서드는 자체적으로 @Transactional 어노테이션이 붙어있어, 정상적으로 코드가 수행이 되어 Database에 insert 를 수행합니다.
하지만, update 의 역할을 하는 book 객체의 변경감지가 동작하지 않아, Flag 컬럼에 업데이트가 되지 않습니다.
왜 @Transactional 이 동작하지 않았는지는 Spring 의 @Transactional 기능을 제공하는 방식에 대해 살펴볼 필요가 있습니다.
Spring @Transactonal 기능제공 방식
JPA 의 객체 변경감지는 transacton 이 commit 될 때, 작동합니다.
그렇기에 Spring 은 @Transactonal 어노테이션을 선언한 메서드가 실행되기전, transaction begin 코드를 삽입하며
메서드가 실행된 후, transaction commit 코드를 삽입하여, 객체 변경감지를 수행하게 유도합니다.
Spring 의 코드 삽입 방법은 크게 2가지 방법이 있습니다.
- 바이트 코드 생성 (
CGLIB사용) - 프록시 객체 사용
2가지 방법중
Spring은 기본적으로프록시 객체 사용이 선택됩니다. 그렇기에interface가 반드시 필요합니다.
SpringBoot는 기본적으로바이트 코드 생성이 선택됩니다. 그렇기에, 굳이interface가 필요없습니다.만약 개발환경이
SpringBoot라면Books 인터페이스없이 봐도 무방합니다.
원리는 이렇습니다. 프록시 객체로 우리가 만든 메서드를 한번 감싸서, 메서드 위 아래로 코드를 삽입 해줍니다.
xxxxxxxxxxpublic 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(); } }}
그렇기에, 우리는 BooksImpl 자료형을 사용 할 때, 스프링이 제공하는 BooksProxy 객체를 사용하게 되며,
BooksProxy 객체가 제공하는 addBook 메서드를 사용해야만 transaction 처리가 수행되는것입니다.
@Transactonal 이 수행되지 않은 이유.
다시 아래의 예제를 살펴봅니다.
xxxxxxxxxxpublic class BooksImpl implements Books { public void addBooks(List<String> bookNames) { bookNames.forEach(bookName -> this.addBook(bookName)); } public void addBook(String bookName) { Book book = new Book(bookName); bookRepository.save(book); book.setFlag(true); }}
BooksProxy 가 addBooks 메서드를 수행하면, 아래와 같은 순서로 작동됩니다.
BooksProxy::addBooks -> BooksImpl::Book
즉, BooksImpl 내부의 코드(addBook) 가 수행 되기 때문에 해당 메서드는 프록시로 감싸진 메서드가 아니라는 점에서
@Transactonal 어노테이션 기능이 수행되지 않는다는 것입니다.
해결방법
사실 @Transactional 메서드를 내부적으로 사용하지 않는것이 근본적인 해결책입니다.
하지만 만약 굳이 사용해야 겠다면, 의존성 주입을 이용하여 Proxy 인스턴스를 자체적으로 가져와 사용하는 방법이 있습니다.
xxxxxxxxxxpublic class BooksImpl implements Books { private Books self; public void addBooks(List<String> bookNames) { bookNames.forEach(bookName -> self.addBook(bookName)); // this 가 아닌 변수 self 로 } public void addBook(String bookName) { Book book = new Book(bookName); bookRepository.save(book); book.setFlag(true); }}
해당 코드는 Books 인터페이스를 이용하여 BooksProxy 인스턴스를 주입할 수 있도록 유도합니다.
생성자 주입을 사용하면, 순환 에러가 발생합니다. @Autowired 로만 해주세요.
Autowired 싫다.
그 후, 자기 자신 즉 this 가 가지고 있는 순수한 addBook 메서드가 아니라 proxy 로 감싸진 addBook 메서드를 통해@Transactional 어노테이션 기능을 사용할 수 있게 됩니다.
정리하자면
@Transactonal어노테이션 메서드는 클래스 내부적으로 사용하지 말고, 밖에서 사용하자. (프록시 객체 때문에)- 굳이 내부적으로 사용하려면, 자기 자신의
Proxy객체를 사용하여 처리하자.
마무으리
혹시 틀린 내용이 있어서, 알려주신다면 감사하게 듣겠습니다. (_ _)
긴글 읽어주셔서 감사합니다!
'Spring' 카테고리의 다른 글
| [Spring] Jackson 라이브러리 이해하기. (9) | 2018.12.10 |
|---|---|
| [SpringBoot] SpringBoot로 MongoDB 데이터 메서드로 주입하기 (0) | 2018.12.04 |
| [SpringBoot] application.properties 파일 읽기 (0) | 2018.11.29 |
| [SpringBoot] SpringBoot와 MongoDB 연동하기 (1) | 2018.11.27 |
포스팅이 도움 되셨다면, 커피 한잔 후원해주세요!
더 좋은 포스팅 작성에 큰 힘이 됩니다.