결제 서비스를 구현하면서 여러 PG사들을 통합해서 운영하기 위해 아임포트
라는 서비스를 사용하고 있다.
아임포트는 클라이언트 단에서 결제 모듈을 통해 결제가 된 후 서버로 데이터를 전송하는 방식이다.
여기서 문제는 결제 금액이 위변조 되었을 때, 서버가 그것을 확인하는 타이밍이 결제 후 라는 것이다.
이를 위해 내가 원했던 방식은 다음과 같다.
에러를 클라이언트에게 보내지만, 그 과정에서 외부 API와 소통했던 데이터를 저장하고 싶은 것이다.
트랜 잭션을 사용할 때 특정 Exception
이 발생해도 그 전까지는 데이터를 저장할 수 있도록 해당 옵션을 사용하게 되었다.
@Override
@Transactional(noRollbackFor = ClassRoomException.class)
public void rollBackTest(Long checkoutId) {
CheckoutEntity checkoutEntity = checkoutEntityService.getCheckoutEntity(checkoutId);
checkoutEntity.setMixpanelDistinctId("UPDATED");
try {
classRoomEnrolmentService.rollbackTest();
} catch (ClassRoomException e){
log.error("CATCH EXCEPTION");
throw e;
}
log.info("END");
}
@Transactional(noRollbackFor = ClassRoomException.class)
을 사용해서 try~catch 내부에서 에러가 발생하더라도 checkoutEntity.setMixpanelDistinctId("UPDATED");
는 적용이 될 것이라고 생각했다.
@Override
@Transactional
public void rollbackTest() throws ClassRoomException{
throw new ClassRoomException(ErrorMessage.CLASS_ALREADY_ENROLMENT, CriticalLevel.NON_CRITICAL);
}
내부에서 호출하는 메소드는 위와같이 Exception
을 던진다.
실행 결과는 UPDATE 쿼리가 보내지지 않고, 다음과 같은 에러 메세지를 던진다.
Transaction silently rolled back because it has been marked as rollback-only
왜일까? 상위 메소드에서 트랜잭션이 열렸고, 하위 메소드는 해당 트랜잭션을 사용하게 될 것이라고 생각했다.
때문에 상위 트랜잭션에 noRollbackFor
옵션을 사용해주면 될 것이라고 생각했다.
그렇다면 하위 메소드에서도 트랜잭션 옵션에 noRollbackFor을 추가해보자.
@Override
@Transactional(noRollbackFor = ClassRoomException.class)
public void rollbackTest() throws ClassRoomException {
throw new ClassRoomException(ErrorMessage.CLASS_ALREADY_ENROLMENT, CriticalLevel.NON_CRITICAL);
}
결과는 의도대로 UPDATE쿼리가 나가고, 클라이언트에게는 exception
이 전달된다.
Hibernate:
update
checkout
set
(생략)
{
"className": "ClassRoomException",
"code": "CLASS_ALREADY_ENROLMENT",
"criticalLevel": "NON_CRITICAL",
"errorMessage": "이미 구입한 강의입니다.",
"singleTrace": "me.schoolDots.boot.serviceImpl.classroom.ClassRoomEnrolmentServiceImpl.rollbackTest(ClassRoomEnrolmentServiceImpl.java:204)"
}
그렇다면 궁금해진다. 한가지 더 궁금해지는 것이 있다. 하위 메소드에서 트랜잭션 propagation
옵션을 NESTED
로 설정해보면 어떻게 될까?
@Override
@Transactional(propagation = Propagation.NESTED, noRollbackFor = ClassRoomException.class)
public void rollbackTest() throws ClassRoomException {
throw new ClassRoomException(ErrorMessage.CLASS_ALREADY_ENROLMENT, CriticalLevel.NON_CRITICAL);
}
바로 도전해본다.
JpaDialect does not support savepoints - check your JPA provider's capabilities
이번엔 다른 에러 메세지가 발생한다.
기존 ClassRoomException
은 RuntimeException
을 상속하기 때문에 UncheckedException
으로 분류된다.
그렇다면 CheckedException
을 던지면 어떻게 될까?
@Override
@Transactional(noRollbackFor = SQLException.class)
public void rollbackTest() throws SQLException {
throw new SQLException();
}
CheckedException
으로 분류되는 SQLException
을 던져본다.
{
"className": "SQLException",
"code": "UNDEFINED_EXCEPTION",
"criticalLevel": "CRITICAL",
"singleTrace": "me.schoolDots.boot.serviceImpl.classroom.ClassRoomEnrolmentServiceImpl.rollbackTest(ClassRoomEnrolmentServiceImpl.java:206)"
}
된다. 업데이트 쿼리도 다음과 같이 날아간다.
Hibernate:
update
checkout
set
(생략)
그리고 상위 메소드와 하위 메소드에서 모두 noRollbackFor
옵션을 제외해봤다.
결과는 업데이트 쿼리가 날아가고, 에러도 잘 표시된다.
UnCheckedException
일 때
noRollbackFor
옵션을 사용한 경우noRollbackFor
옵션을 사용하지 않은 경우propagation
옵션을 NESTED
로 설정한 경우 CheckedException
일 때
noRollbackFor
유무에 관계 없이 전부 update 쿼리 동작UnCheckedException
일 때, 왜 위와 같은 일이 벌어지는지 다음 편에서 더 알아보기로 했다.