현재 사내에서 개발중인 프로젝트는 특정 메소드에 대한 로그를 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
의 기본 전파 옵션은 REQUIRED로 설정되어있다.
따라서 @Transactional이 붙은 외부 메소드의 내부에서 호출한 메소드에 @Transactional이 붙어있다면 기존 트랜잭션에 참여하여 작동한다.
위 사진과 같이 외부에서 트랜잭션을 시작한(외부 Service) 쪽을 물리 트랜잭션, 내부에서 기존 트랜잭션에 참여하는 쪽을 논리 트랜잭션이라고 한다.
트랜잭션 완료는 각각의 시점에서 완료되고, 전파의 의미는 트랜잭션이 commit, rollback이 되는 범위를 뜻하는 것이지 트랜잭션이 완전히 하나가 되는 것은 아니다.
따라서 기존 본인이 작성한 코드는 하나의 물리 트랜잭션(특정 서비스 메소드)에서 두 개의 논리 트랜잭션(로그 서비스)를 호출하기 때문에 예외가 발생했을 때 트랜잭션 전파 옵션에 의해 롤백되었던 것이었다.
여러 블로그, 공식 문서를 뒤적거려본 결과 전파옵션을 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 블록에서 내부 서비스 호출을 통해 로그를 저장하기 때문에 의도한대로 외부 서비스의 롤백과 로깅이 정상적으로 수행된다.