catch블록의 트랜잭션에 대해

최창효·2024년 6월 1일
post-thumbnail

예외를 DB에 직접 저장하기

비즈니스 로직에서 예외가 발생했을 때 해당 예외를 데이터베이스에 저장하고 상위 계층에 같은 계열의 예외를 그대로 던져주고 싶다면 어떻게 해야할까요?

@Service
@RequiredArgsConstructor
public class MyService {
    private final ErrorRepository errorRepository;
    private final MyRepository myRepository;

    @Transactional
    public void go() {
        try {
            MyEntity myEntity = MyEntity.builder()
                    .name("entity")
                    .build();
            myRepository.save(myEntity);
            
			// 비즈니스 로직에서 예외가 발생했을 때
            throw new RuntimeException("somethingWrong");
        }catch (Exception e) {
        	// 예외를 데이터베이스에 저장하고
            ErrorEntity errorEntity = ErrorEntity.builder()
                    .err(e.getMessage())
                    .build();
            errorRepository.save(errorEntity);
			
            // 상위 계층에 예외를 던집니다
            throw new RuntimeException("somethingWrong?!");
        }
    }
}

위 코드를 실행하면 예외가 저장되지 않습니다. 그 이유는 예외를 저장하는 작업은 메서드의 트랜잭션에 합류하게 되고 메서드의 트랜잭션은 예외로 인해 롤백되기 때문입니다.

엔티티와 예외를 저장하기 위해 사용한 save메서드는 JpaRepository의 save메서드입니다.

JpaRepository의 save는 일반적으로 SimpleJpaRepository구현체를 사용합니다.

  • 제가 테스트할 때도 SimpleJpaRepository가 사용됐습니다.

  • SimpleJpaRepository의 save는 별다른 옵션을 설정하지 않은 기본 @Transactional을 사용합니다.

  • 기본적인 @Transactional의 Propagation설정은 Required입니다. Required설정은 부모 트랜잭션이 존재하면 합류하고 부모 트랜잭션이 존재하지 않다면 새롭게 트랜잭션을 생성합니다.

따라서 errorRepository.save()는 부모 메서드의 트랜잭션에 합류된 상태고, 해당 트랜잭션이 throw new RuntimeException("somethingWrong?!")에 의해 롤백되기 때문에 catch블록의 에러 역시 데이터베이스에 저장되지 않습니다.

Propagation 변경

트랜잭션이 합류하는게 문제라면 단순히 상위 트랜잭션에 합류하지 않도록 변경하면 될까요? 맞는 얘기긴 하지만 이때 내부호출에 주의해야 합니다.

@Service
@RequiredArgsConstructor
public class MyService {
    private final ErrorRepository errorRepository;
    private final MyRepository myRepository;

    @Transactional
    public void go() {
        try {
            MyEntity myEntity = MyEntity.builder()
                    .name("entity")
                    .build();
            myRepository.save(myEntity);

            throw new RuntimeException("somethingWrong");
        }catch (Exception e) {
            saveError(e);
            
            throw new RuntimeException("somethingWrong?!");
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveError(Exception e) {
        ErrorEntity errorEntity = ErrorEntity.builder()
                .err(e.getMessage())
                .build();
        errorRepository.save(errorEntity);
    }

}

go메서드에서 호출한 saveError는 Proxy가 아닌 원본 메서드이기 때문에 AOP에 의한 트랜잭션 처리가 일어나지 않습니다.

내부호출 해결1. ApplicationContext를 이용한 자기자신 호출

@Service
@RequiredArgsConstructor
public class MyService {
    private final ErrorRepository errorRepository;
    private final MyRepository myRepository;
    private final ApplicationContext applicationContext;

    @Transactional
    public void go() {
        try {
            MyEntity myEntity = MyEntity.builder()
                    .name("entity")
                    .build();
            myRepository.save(myEntity);

            throw new RuntimeException("somethingWrong");
        }catch (Exception e) {
        	// applicationContext를 이용해 자기자신을 호출합니다.
            applicationContext.getBean(MyService.class).saveError(e);

            throw new RuntimeException("somethingWrong?!");
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveError(Exception e) {
        ErrorEntity errorEntity = ErrorEntity.builder()
                .err(e.getMessage())
                .build();
        errorRepository.save(errorEntity);
    }

}

내부호출 해결2. 서비스 분리

@Service
@RequiredArgsConstructor
public class ErrorService {
    private final ErrorRepository errorRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveError(Exception e) {
        ErrorEntity errorEntity = ErrorEntity.builder()
                .err(e.getMessage())
                .build();
        errorRepository.save(errorEntity);
    }
}
@Service
@RequiredArgsConstructor
public class MyService {
    private final MyRepository myRepository;
    private final ErrorService errorService;

    @Transactional
    public void go() {
        try {
            MyEntity myEntity = MyEntity.builder()
                    .name("entity")
                    .build();
            myRepository.save(myEntity);

            throw new RuntimeException("somethingWrong");
        }catch (Exception e) {
            errorService.saveError(e);

            throw new RuntimeException("somethingWrong?!");
        }
    }
}

실제 서비스였다면

에러 정보를 반드시 데이터베이스에 남겨야 하는지를 고민했을거 같습니다. 마땅한 이유가 없다면 해당 방식보다 로그를 남기고 로그를 수집해 에러를 모니터링하는 방법으로 작업했을거 같습니다.

profile
기록하고 정리하는 걸 좋아하는 백엔드 개발자입니다.

0개의 댓글