예제는 깃허브에 있다.
트랜잭션을 공부했다고 생각하고 업무에 임했던 나였는데, 도저히 풀리지않는 느낌으로 예외를 받았던게 있다.
이전에 한번 포스팅했던 Stream Closed
에러였는데 이게 또 한번 나를 붙잡았다.
처음 개발환경에서 OpenFeign을 사용하면서 또 logging level에서 IOException이 나는줄알고 이부분으로 삽질을 했다.
근데 그게 아니어서 이 포스팅을 작성했다.
- HelloController.java
@RestController
@RequiredArgsConstructor
public class TestController {
private final TransactionParentService transactionParentService;
@GetMapping("/transaction/test")
public ResponseEntity<?> test() {
transactionParentService.size();
return ResponseEntity.ok().build();
}
}
- TransactionParentService.java
@Service
@RequiredArgsConstructor
public class TransactionParentService {
private final MemberService memberService;
private final TransactionErrorService transactionErrorService;
@Transactional
public int size() {
transactionErrorService.throwExceptionLog();
return memberService.allSize();
}
}
- TransactionErrorService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class TransactionErrorService {
private final MemberService memberService;
@Transactional
public void throwExceptionLog() {
try {
memberService.saveAndException();
} catch (RuntimeException e) {
log.error("error : {}", e.getMessage());
}
System.out.println("끝");
}
}
- MemberService.java
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
public void save() {
memberRepository.save(new Member(1L, "홍길동"));
}
@Transactional
public void saveAndException() {
memberRepository.save(new Member(null, "홍길동"));
throw new RuntimeException();
}
public int allSize() {
return memberRepository.findAll().size();
}
}
TransactionParentService -> TransactionErrorService -> MemberService 순서로 서비스는 동작하게 되고,
기본적으로 Service에는 메소드마다 @Transactional
어노테이션이 붙어있다.
아래는 application.yaml에
logging:
level:
org.springframework.transaction: trace
이와 같은 로그설정을 해주고 찍어본 결과이다.
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Getting transaction for [com.github.lsj8367.service.TransactionParentService.size]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Getting transaction for [com.github.lsj8367.service.TransactionErrorService.throwExceptionLog]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Getting transaction for [com.github.lsj8367.service.MemberService.saveAndException]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Completing transaction for [com.github.lsj8367.service.MemberService.saveAndException] after exception: java.lang.RuntimeException
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Completing transaction for [com.github.lsj8367.service.TransactionErrorService.throwExceptionLog]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Getting transaction for [com.github.lsj8367.service.MemberService.allSize]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Completing transaction for [com.github.lsj8367.service.MemberService.allSize]
TRACE 66119 --- [nio-8080-exec-1] o.s.t.i.TransactionInterceptor : Completing transaction for [com.github.lsj8367.service.TransactionParentService.size]
여기서 보면 정상적으로 트랜잭션을 처음엔 쭉 획득한다.
그리고나서 memberService.save()
를 통해 SimpleJpaRepository
까지 하기위한 transaction을 얻게된다.
에러가 나는 부분
우리의 코드에서는 save 이후 throw new RuntimeException()
이 있다.
여기서 부터 after Exception : java.lang ...으로 보이는 rollback 마킹이 존재한다.
org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:752) ~[spring-tx-5.3.22.jar:5.3.22]
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711) ~[spring-tx-5.3.22.jar:5.3.22]
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654) ~[spring-tx-5.3.22.jar:5.3.22]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407) ~[spring-tx-5.3.22.jar:5.3.22]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.22.jar:5.3.22]
...생략
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
이게 도대체 어떻게 동작하는것인가?
추상클래스인 TransactionAspectSupport.invokeWithinTransaction 메소드를 사용하는 TransactionInterceptor
의 invoke를 호출해서 안쪽에 추상클래스의 메소드를 사용한다.
해당 @Transactional
이 붙은 메소드에서 던져진 exception을 Throwable 객체로 catch절에서 받고있다.
- DefaultTransactionAttribute
- rollbackOn 메소드
이 스프링 기본 구성인 DefaultTransactionAttribute구현에서 rollbackOn 메소드를 사용하게 되는데
이 rollbackOn에서 그토록 얘기했던 말들이 나온다.
RuntimeException이 여기서 채택되어 instanceof로 체크하고 있다!!!
그래서 rollback 마크를 하나를 진행해두고 나머지 트랜잭션 어노테이션에 대해서도 commit을 할거냐 rollback을 할거냐에 대한 작업을 이 해당 aop를 통해 동작한다.
TransactionManager
각 트랜잭션 매니저들은 AbstractPlatformTransactionManager 추상클래스를 상속하여 사용한다.
그래서 트랜잭션 매니저가 전부 commit을 수행하는데,
rollback 전용 마크가 하나라도 붙게되면 UnexpectedRollbackException 이 에러가 나타나게 된다.
그래서 보이게되는 예외 메시지가 Transaction silently rolled back because it has been marked as rollback-only
로 나오게 된다.
그럼 Stream Closed 는?
ㅋㅋㅋㅋ.. 그러게 말이다. 이거 왜뜬건지 도저히 이해가 되지 않는데, dev환경에선 이 로그만 보였지 위의 로그가 안보였었다.
근데 로컬에서 이 부분을 수정하니 말끔히 해결됐다.
아무튼 원초적인 문제는 이 무지성 붙이기 @Transactional
이었다.
막판에 이게 떠올라서 지웠더니 해결되서 꿀잠각이다.
아무튼 @Transactional
에 대한 디버깅 정리를 해본다.
'디버깅' 카테고리의 다른 글
Kafka가 내 로직을 9번이나 재시도를 했다 (0) | 2023.11.02 |
---|---|
Stream 오류 제거 (2) | 2023.04.21 |
FeignClient Logging level 디버깅 (0) | 2022.12.17 |
@Async 사용시 에러 해결 (0) | 2022.11.04 |