졸업 캡스톤 프로젝트를 하며 Jsoup을 이용해 학교 비교과 및 리쿠르팅 정보를 크롤링해와 그걸 저장하는 로직을 구현했었다.
그런데 이 크롤링 작업을 스케쥴러에 등록하여 매일 1번씩 데이터를 갱신하는 작업을 하려 했는데 스케쥴러도 정상적으로 동작하고 크롤링도 정상적으로 동작하는데 delete나 save가 모두 작동하지 않았다.
좀 찾아보니 신기한 답이 나왔다.
우선 나는 CrawlService위에 @Transactional(readonly=true)를 걸어줬다.
그리고 스케쥴러가 실행할 메소드를 정의했고
해당 메소드 내에서 크롤링에 필요한 메소드들을 호출했고
해당 메소드들은 @Transactional이 붙어있는 메소드들이다.
자, 여기서 우린 @Transactional 어노테이션이 하나가 아니란 사실을 알아야 한다.
import jakarta.transaction.Transactional;
import org.springframework.transaction.annotation.Transactional;
이렇게 두개의 트랜잭션 어노테이션이 존재하는데 jakarta의 경우 자바 표준이고,
springframework은 스프링 표준이고 컨테이너가 관리한다.
직접 해당 어노테이션을 자세히 들어가서 들여다보면
jakarta의 경우 롤백, dontRollback, 트랜잭션 타입정도의 간단한 기능들만 제공한다.
반면, 스프링 프레임워크의 트랜잭션은 readonly, rollback, isolation등등 정말 많은 (실제 RDBMS에서 사용 하는) 기능들이 탑재되어 있다.
이 글을 보면 PlatformTransactionManager의 구현체가 DataSourceTransactionManager인 경우 AbstractTransactionImpl안의 begin메소드를 호출하지 않아 커스텀하게 트랜잭션 매니저를 만들어서 사용해야 된다고 한다.
스프링의 트랜잭션은 내부에서 이렇게 트랜잭션 매니저를 알아서 잡고 구현하는데 여기서 익숙한 이름이 보인다.
PlatformTransactionManager이다.
위 글대로면 우리가 직접 트랜잭션 매니저를 만들어 트랜잭션 어노테이션에 넣어 트랜잭션 매니저를 설정해주어야 한다는 것이다.
근데 이건 너무 간거 아닌가란 생각이 들어 다른 방법을 고민해봤다.
우린 스프링에서 제공하는 Transactional말고도 jakarta에서 제공하는 트랜잭션도 사용할 수 있다. (물론 기능도 부족하고 deprecated된 부분도 있는 것 같다..)
그렇다면 jakarta의 트랜잭션은 어떤 매니저를 사용할까?
어노테이션 안을 직접 들여다 봐도 트랜잭션매니저를 설정하거나 관련된 정보가 나오지 않아 직접 찾아봤다.
스프링 공식문서에서 보니 PlatformTransactionManager의 구현체 JtaTransactionManager를 기본적으로 사용하는 것 같았다.
즉, 얘도 결국 스프링 트랜잭션과 같은 유형의 매니저를 기본값으로 사용한다.
그렇다면 반드시 트랜잭션 매니저를 새로 만들어 어노테이션에 붙여 해당 매니저를 적용하는 방법밖에 없을까?
아니다.
이 부분을 내가 정확하게 알고 사용하지 않았기에 오류를 만든 것이라 생각된다.
바로 트랜잭션의 범위 및 전이다.
우리가 트랜잭션을 열면 트랜잭션이 커밋될 때까지 해당 트랜잭션이 유지된다.
(이걸 몰랐어서 다른 부분에서 fetch join을해도 Proxy객체가 계속 리턴되는 오류가 또 발생했었다...)
트랜잭션은 AOP와 같이 Proxy를 이용하여 동작되는데, 자세한 설명은 여기를 참고해보자..
aop처럼 해당 메소드 전후로 tx.begin, tx.commit을 추가해서 메소드를 실행하는 것이다.
만약 여기서, A메소드에서 트랜잭션이 처음 열리고 해당 메소드에서 B메소드를 호출하고 B메소드 또한 트랜잭션 어노테이션이 붙어있다면
트랜잭션은 몇개일까?
정답은 1개이다. 왜냐하면 A메소드를 호출할 때 트랜잭션이 열리고 해당 트랜잭션 메소드 내에서 다른 트랜잭션 메소드를 호출해도 추가로 프록시를 만들어 처리하는게 아닌 같은 프록시 객체안에서 수행한다.
이렇게 프록시를 이용해 트랜잭션을 구현하기에 트랜잭션을 사용할 메소드는 private을 허용하지 않고
상위 메소드에서 트랜잭션이 열리면 하위 메소드에 트랜잭션을 적용해도 따로 트랜잭션이 생성되지 않는다.
자 처음 문제의 상황을 다시 정리해보면
난, Service계층 클래스에 readonly 트랜잭션을 열어놨다.
그 후 트랜잭션 어노테이션을 따로 열지 않은 스케쥴러 메소드를 실행했다.
즉, service의 readonly트랜잭션이 열렸고 해당 트랜잭션 내에서 스케쥴러 메소드를 실행 -> 스케쥴러 안에서 트랜잭션이 필요한 메소드를 실행 -> 이미 readonly트랜잭션이므로 insert, delete가 당연히 수행되지 않는다.
그러므로, 내가 겪은 문제는 사실 트랜잭션을 제대로 사용하지 못해 생긴 문제였다.
해당 부분에서 스케쥴러에 바로 트랜잭션 어노테이션을 붙여주거나 서비스 계층의 트랜잭션의 readonly를 false로 돌리거나 등등 트랜잭션이 정상적으로 처리되게 마음가는대로 바꾸기만 하면 해결되는 문제라는 뜻이다.
실제로도, 스케쥴러에 트랜잭션을 걸어주고 크롤러 부분에서 트랜잭션을 제거하니(제거안해도 상관없지만 의미가 없는 어노테이션이니 제거했다.) 정상적으로 insert,delete가 수행되었다.
아, 참고로 아예 어노테이션을 다 빼도 정상적으로 수행되긴 한다.
우리가 JpaRepository를 상속받아 만든 repository를 DI받을 때 SimpleJpaRepository를 구현체로 주입받는데 이 구현체 내부의 메소드를 살펴보면 ( 공식문서 를 참고해보면 좋다.) save, delete등에 트랜잭션 어노테이션이 이미 붙어있기에 따로 설정안해도 수행되긴 한다.
뭐, 그래도 작업 단위를 짧게 잡으면 DB 부하가 늘어나기도 하고 작업 오류 시 롤백하기 까다로워지니 원하는 작업 단위만큼 "트랜잭션 범위를 생각하고 " 메소드를 구현하는게 좋을 듯 싶다.
또, 인프런 김영한 강사님이 조회 서비스와 CUD를 분리해서 사용하는게 좋다고 하셨는데 그 이유도 좀 알 것 같았다..
앞으론 트랜잭션 범위를 잘 생각해보고 구현하고 서비스 로직을 좀 분리하는 것도 좋을 듯 싶다.
참고
https://www.baeldung.com/transaction-configuration-with-jpa-and-spring
https://blogshine.tistory.com/291
https://interconnection.tistory.com/123
https://cjlee38.github.io/post/tech/spring/2022-08-21-spring-scheudeld-with-transactional/
https://velog.io/@chullll/Transactional-%EA%B3%BC-PROXY
https://onejunu.tistory.com/146
'Spring' 카테고리의 다른 글
부하 테스트 하기 (2) (0) | 2023.07.12 |
---|---|
부하 테스트 하기 (1) (1) | 2023.07.08 |
Jsoup으로 로그인 후 웹 사이트 크롤링 하기 (1) | 2023.05.09 |
웹소캣이란? (0) | 2023.05.04 |
spring boot v3에 스웨거 적용하기 (0) | 2023.04.18 |