개요
사내 공통 예외 처리 프로세스에서 중복을 처리한 방식을 소개합니다. 구체적인 코드는 컨셉 코드로 대체하였습니다.
요약
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(...);
}
이외에도 다양한 포인트컷(publicMethods
, serviceComponents
, facadeComponents
, allComponents
, applicationComponents
, handlerComponents
)을 선언함으로써, 서비스, 파사드, 애플리케이션, 핸들러 컴포넌트 등 애플리케이션의 다양한 레이어에서 발생하는 예외를 포착합니다. 여러가지 상황에서 발생가능한 포인트컷을 선언해준 것인데요.
문제는, 이렇게 포인트컷이 다양하다 보니 동일한 예외가 여러 레이어를 통과하며 여러 번 포착되어 중복 알림을 발생시킬 수 있다는 점입니다. 예를 들어, 하위 서비스 레이어에서 발생한 예외가 파사드 레이어, 그리고 애플리케이션 레이어로 전파되며 각 레이어에서 동일한 예외에 대해 알림이 발생 가능한 상황이 되었습니다.
해결 방법
이를 해결하기 위해 마킹 방식을 사용했습니다.
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
알림을 받을 것 같습니다. 😓
찾아 보니 자바에서 이와 같은 상황을 대비한 자료구조를 구비해놓고 있었습니다.
WeakHashMap
바로 WeakHashMap
입니다. 자바에서 제공하는 자동화된 메모리 관리 기법의 덕을 볼 수 있었습니다. 이 자료구조는 키(여기서는 예외 객체)에 대해 약한 참조를 유지합니다. 약한 참조는 가비지 컬렉터가 해당 객체를 수집 대상으로 판단할 때, 그 객체에 대한 강한 참조가 없다면 회수할 수 있도록 합니다. 이를 통해 애플리케이션에서 발생한 예외 객체를 마킹하고 관리하면서도, 메모리 누수 없이 자동으로 불필요한 객체를 정리할 수 있게 됩니다.
WeakHashMap
에 작성된 공식 자바독을 보면 WeakHashMap
은 키에 대한 약한 참조(Weak Reference)를 사용하는 Map
구현체입니다. 이는 WeakHashMap
에 저장된 키가 다른 곳에서 더 이상 사용되지 않고 GC에 의해 회수될 수 있다면, 자동으로 해당 키와 관련된 엔트리를 맵에서 제거할 수 있도록 설계되었습니다. 이러한 특성 덕분에, 명시적으로 키를 제거하는 로직 없이도 메모리 누수를 방지할 수 있습니다.
WeakHashMap
의 내부에서는 각 키를 WeakReference
로 감싸서 저장하며, 키 객체가 GC에 의해 회수되면 해당 키를 참조하는 WeakReference
객체가 자동으로 ReferenceQueue
에 추가됩니다. WeakHashMap
은 주기적으로 이 큐를 확인하여, 참조가 더 이상 존재하지 않는 키와 그에 대응하는 값들을 맵에서 제거합니다.
OOM 예방하기
중복 방지 프로세서를 위해서 markedExceptions
라는 Set<Throwable>
을 WeakHashMap
으로 초기화하여 사용합니다. 이 Set
에 예외를 마킹함으로써, 같은 예외가 다시 발생했을 때 이미 처리되었는지 쉽게 확인할 수 있습니다. 예외가 마킹되면, 해당 예외에 대한 추가적인 알림 처리는 수행되지 않습니다. 이렇게 하면 동일한 예외에 대해 여러 번 알림을 보내는 문제를 방지할 수 있습니다.
private final Set<Throwable> markedExceptions = Collections.newSetFromMap(new WeakHashMap<>());
이 코드는 애플리케이션에서 발생하는 예외들을 마킹하고 관리하는 메커니즘을 구축합니다. WeakHashMap
의 특성 덕분에, 마킹된 예외 객체들이 더 이상 애플리케이션의 다른 곳에서 참조되지 않게 되면 자동으로 가비지 컬렉터에 의해 수집됩니다. 띠라서 애플리케이션의 메모리 사용량을 효과적으로 관리할 수 있게 해주며, 장기 실행 중에 발생할 수 있는 OOM
위험을 줄여주게 되겠죠!
WeakHashMap
자료구조를 활용하여 예외를 Marking하는 구현은 다음과 같습니다.
private boolean isMarkedException(Throwable throwable) {
return markedExceptions.contains(throwable);
}
private void markException(Throwable throwable) {
markedExceptions.add(throwable);
}
isMarkedException
메소드는 주어진 예외가 이미 마킹되었는지 확인하는 역할을 하며, markException
메소드는 새로 발생한 예외를 마킹하는 역할을 합니다.
결론
이 방식을 통해 예외가 처음 발생하고 처리될 때만 알림이 발생하며, 동일한 예외가 다시 발생해도 이미 마킹되었기 때문에 불필요한 알림을 발생시키지 않습니다. 예외 객체가 애플리케이션의 다른 부분에서 강하게 참조되지 않는 한, 본 Aspect
를 통과하여 Marking
되는 예외들은 Gc
대상이 되므로 OOM
도 방지할 수 있었습니다.
'이슈와해결' 카테고리의 다른 글
자바에서 동시성 문제를 다루는 n가지 방법들(feat. 주식 매수) (3) | 2024.03.28 |
---|---|
티켓 서비스 백엔드 시스템에서 중복 요청 이슈를 멱등하게 처리하기 (0) | 2024.03.01 |
Gson 베이스 프로젝트에서 LocalDateTime 컨버팅 지옥 탈출하기 (0) | 2024.02.18 |
@RequestBody 컨텐츠 유실 문제 - 컨트롤러에도 디버깅이 찍히지 않으면 어디를 봐야할까? (1) | 2023.12.21 |
일부러 정규화를 하지 않는 스키마는 어떨까? - 인가 프로세스에서 권한 관련 스키마 최적 설계 탐구 (feat. EAV, JsonB) (0) | 2023.12.18 |