안녕하세요. 오늘은 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
클래스 내부에서 사용할 때 발생 할 수 있습니다.
xxxxxxxxxx
public 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 인터페이스
없이 봐도 무방합니다.
원리는 이렇습니다. 프록시 객체로 우리가 만든 메서드를 한번 감싸서, 메서드 위 아래로 코드를 삽입 해줍니다.
xxxxxxxxxx
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();
}
}
}
그렇기에, 우리는 BooksImpl
자료형을 사용 할 때, 스프링이 제공하는 BooksProxy
객체를 사용하게 되며,
BooksProxy
객체가 제공하는 addBook
메서드를 사용해야만 transaction
처리가 수행되는것입니다.
@Transactonal 이 수행되지 않은 이유.
다시 아래의 예제를 살펴봅니다.
xxxxxxxxxx
public 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 인스턴스
를 자체적으로 가져와 사용하는 방법이 있습니다.
xxxxxxxxxx
public 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 |
포스팅이 도움 되셨다면, 커피 한잔 후원해주세요!
더 좋은 포스팅 작성에 큰 힘이 됩니다.