도대체 왜 롤백이 되는거야? (1편 - transaction 실험)

enoch·2022년 12월 8일
2
post-thumbnail

서론

결제 서비스를 구현하면서 여러 PG사들을 통합해서 운영하기 위해 아임포트라는 서비스를 사용하고 있다.
아임포트는 클라이언트 단에서 결제 모듈을 통해 결제가 된 후 서버로 데이터를 전송하는 방식이다.
여기서 문제는 결제 금액이 위변조 되었을 때, 서버가 그것을 확인하는 타이밍이 결제 후 라는 것이다.
이를 위해 내가 원했던 방식은 다음과 같다.

  1. 결제 완료 후 클라이언트는 서버에 결제 정보 관련 id 값을 전달한다.
  2. 서버는 클라이언트에게 받은 id값으로 아임포트에 결제 정보를 요청한다.
  3. 아임포트에서 응답해준 결제 금액과, DB에 저장되어있는 결제되어야 하는 금액을 비교한다.
  4. 금액이 일치하지 않다면 즉, 데이터가 위변조되었다면 해당 결제건을 취소하고 3번에서 받았던 정보를 저장한다.

에러를 클라이언트에게 보내지만, 그 과정에서 외부 API와 소통했던 데이터를 저장하고 싶은 것이다.

본론

실험1. 상위 메소드에서만 noRollbackFor 옵션 사용

트랜 잭션을 사용할 때 특정 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옵션을 사용해주면 될 것이라고 생각했다.

실험2. 하위 메소드에도 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)"
}

실험3. NESTED 전파 옵션 사용

그렇다면 궁금해진다. 한가지 더 궁금해지는 것이 있다. 하위 메소드에서 트랜잭션 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

이번엔 다른 에러 메세지가 발생한다.

실험4. CheckedException 던지기

기존 ClassRoomExceptionRuntimeException을 상속하기 때문에 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일 때, 왜 위와 같은 일이 벌어지는지 다음 편에서 더 알아보기로 했다.

profile
🍣 초밥을 사랑하는 백엔드 개발자 입니다 :)

0개의 댓글