개요

사내 공통 예외 처리 프로세스에서 중복을 처리한 방식을 소개합니다. 구체적인 코드는 컨셉 코드로 대체하였습니다.

요약

Aspect로 잡힌 예외에 대해 비동기 스레드에 태워 알림을 발생시키는 상황입니다. 이때 레이어드 아키텍처 구조에서 하위에서 발생한 예외가 전파되면서 동일한 예외가 중복으로 포착되는 문제가 발생했습니다. 한 번 처리된 예외는 다시 알림이 발생하지 않도록 하려면 어떻게 해야 할까요? 이 글에서는 마킹 방식으로 중복 알림을 방지하면서, 이때 발생 가능한 OOM(Out of Memory)을 예방하기 위해 WeakHashMap을 활용한 방식에 대해 소개합니다.

문제 상황

Spring의 AOP를 활용해 애플리케이션의 예외를 횡단으로 포착하여 중앙집중식으로 예외를 관리할 수 있습니다. 다음과 같은 Aspect를 사용하여 예외를 잡도록 구현이 되어 있습니다.

@Slf4j
@Aspect
@Component
public class ErrorReportAspect {

  @Pointcut("execution(public * *(..))")
  void publicMethods() {
  }

  @Pointcut("within(@org.springframework.stereotype.Service *)")
  void serviceComponents() {
  }

@Aspect 어노테이션을 사용하여 에러 리포팅 관련 로직을 Aspect로 구현했습니다. AspectJ의 포인트컷 표현식을 통해 다양한 조건에서 메소드 실행 후 예외를 포착하고, 이를 처리하는 로직을 정의합니다.

  @AfterThrowing(pointcut = "serviceComponents() && publicMethods()", throwing = "throwable")
  public void reportServiceError(JoinPoint jp, Throwable throwable) {
      log.debug("(예외!!!) ... );
      예외알림서비스.publishErrorEvent(...);
  }

이외에도 다양한 포인트컷(publicMethodsserviceComponentsfacadeComponentsallComponentsapplicationComponentshandlerComponents)을 선언함으로써, 서비스, 파사드, 애플리케이션, 핸들러 컴포넌트 등 애플리케이션의 다양한 레이어에서 발생하는 예외를 포착합니다. 여러가지 상황에서 발생가능한 포인트컷을 선언해준 것인데요.

문제는, 이렇게 포인트컷이 다양하다 보니 동일한 예외가 여러 레이어를 통과하며 여러 번 포착되어 중복 알림을 발생시킬 수 있다는 점입니다. 예를 들어, 하위 서비스 레이어에서 발생한 예외가 파사드 레이어, 그리고 애플리케이션 레이어로 전파되며 각 레이어에서 동일한 예외에 대해 알림이 발생 가능한 상황이 되었습니다.

해결 방법

이를 해결하기 위해 마킹 방식을 사용했습니다.

markedExceptions라는 WeakHashMap을 사용하여 이미 처리한 예외를 마킹합니다. 포인트컷이 호출된 코드에서는 다음과 같은 프로세스로 진행될 것을 기대해볼 수 있을 것입니다.

@AfterThrowing(pointcut = "allComponents() && applicationComponents()", throwing = "throwable")
  public void reportApplicationError(JoinPoint jp, Throwable throwable) {
    if (!isMarkedException(throwable)) {
      log.debug("(예외!!!) ... );
      예외알림서비스.publishErrorEvent(...);
      markException(throwable);
    }
  }

이제 isMarkedException() 메서드와 markException()만 적절하게 구현해주면 될 것 같습니다! 어떻게 구현할까요 ? 다양한 방식이 있겠지만 전역 변수로 자료구조를 두는 방식을 택했습니다.

private final Set<Throwable> markedExceptions= ... ;

중복을 허용하지 않는 Set 자료구조이기 때문에 markedExceptions가 해당 예외가 존재한다면, isMarkedException() false를 판별하기가 쉽습니다. 구현이 참 간단합니다.

사이드 이펙트

문제는 서버가 365일 24시간 돌아가는데, 발생하는 모든 예외가 markedExceptions 에 쌓일 것이라는 부분입니다. 서버가 구동되고 얼마 지나지 않아 OOM 알림을 받을 것 같습니다. 😓