[Spring Boot] Transactional REQUIRES_NEW로 트랜잭션 분리하기

Jae_0·2025년 3월 3일
0
post-thumbnail

[Spring Boot] Transactional REQUIRES_NEW로 트랜잭션 분리하기


서론

현재 사내에서 개발중인 프로젝트는 특정 메소드에 대한 로그를 AOP를 통해 DB에 저장하고있다.

로그 저장 메소드 코드

아래 코드는 실제 코드가 아닌 게시물 작성용으로 작성한 예시 코드이다.

@Around("execution(public * com.example.*..*.service.create*(..))")
    public Object logCreate(ProceedingJoinPoint joinPoint) throws Throwable {

        StopWatch stopWatch = new StopWatch();
        String logId = "";
        String errorDescription = "";
        boolean hasException = false;
        Object result;

        try {
            stopWatch.start();

            result = joinPoint.proceed();
        } catch (Exception e) {
            logId = logRepository.createLogId();
            errorDescription = e.toString();
            hasException = true;

            throw e;
        } finally {

            stopWatch.stop();

            Map<String, Object> map = makeLogMap(joinPoint, "C", stopWatch);
            if (logId.trim().isEmpty()) {
                logId = logRepository.createLogId();
            }
            map.put("logId", logId);
            map.put("errorDescription", errorDescription);
            systemLogService.insertSysLog(map);

            if (hasException) {
                errorLogService.insertErrorLog(map);
            }
        }

        return result;
    }

@Around로 설정한 특정 Point Cut에 대해 로그를 저장한다.
로그는 메소드 호출에 관한 로그와 예외 혹은 에러가 발생했을 때 발생한 에러에 대한 로그를 저장한다. (이 둘은 서로 로그 아이디를 공유하고있다.)

그러나 AOP가 적용된 서비스 메소드에서 예외가 발생 되었을 때 로그 저장도 롤백이 되는 문제가 발생했다.

트랜잭션 분리하기

@Transactional

스프링의 @Transactional의 기본 전파 옵션은 REQUIRED로 설정되어있다.
따라서 @Transactional이 붙은 외부 메소드의 내부에서 호출한 메소드에 @Transactional이 붙어있다면 기존 트랜잭션에 참여하여 작동한다.

위 사진과 같이 외부에서 트랜잭션을 시작한(외부 Service) 쪽을 물리 트랜잭션, 내부에서 기존 트랜잭션에 참여하는 쪽을 논리 트랜잭션이라고 한다.

트랜잭션 완료는 각각의 시점에서 완료되고, 전파의 의미는 트랜잭션이 commit, rollback이 되는 범위를 뜻하는 것이지 트랜잭션이 완전히 하나가 되는 것은 아니다.

따라서 기존 본인이 작성한 코드는 하나의 물리 트랜잭션(특정 서비스 메소드)에서 두 개의 논리 트랜잭션(로그 서비스)를 호출하기 때문에 예외가 발생했을 때 트랜잭션 전파 옵션에 의해 롤백되었던 것이었다.

Propagation.REQUIRES_NEW

여러 블로그, 공식 문서를 뒤적거려본 결과 전파옵션을 Propagation.REQUIRES_NEW로 설정하여 해결하기로 결정했다.
메소드 상단에 @Transactional(propagation = Propagation.REQUIRES_NEW) 을 선언해주면 해당 메소드는 기존 트랜잭션에 참여하지 않고 새로운 트랜잭션 즉, 물리 트랜잭션을 수행하게 된다.

REQUIRES_NEW

public static final Propagation REQUIRES_NEW

Create a new transaction, and suspend the current transaction if one exists. Analogous to the EJB transaction attribute of the same name.

NOTE: Actual transaction suspension will not work out-of-the-box on all transaction managers. This in particular applies to JtaTransactionManager, which requires the jakarta.transaction.TransactionManager to be made available to it (which is server-specific in standard Jakarta EE).
(공식문서 내용 발췌)

예외 발생 시

그러나 내부에서 throw 하는 예외는 외부에 여전히 전파되기 때문에, try-catch로 적절한 예외처리를 해주지 않으면 롤백된다.
본인의 코드에서는 try-catch-finally구문으로 외부 서비스에서 예외 발생 시 이를 catch하고 finally 블록에서 내부 서비스 호출을 통해 로그를 저장하기 때문에 의도한대로 외부 서비스의 롤백과 로깅이 정상적으로 수행된다.

ref

공식문서
응? 이게 왜 롤백되는거지? - 우아한 기술블로그

profile
거대한 세상에 발자취 남기기

0개의 댓글