사내에서 Event를 도입을 하면서 Event 예외 상황에 대해서 생각을 하며 구현을 했던 것을 정리하려고 합니다.
Event 기술 결정
좋은 Event 기반 기술들이 있는 와중에 러닝커브가 크지 않는 점 과 인프라 에서 의 추가 설치 관리 를 고려하여
Spring에서 기본으로 제공하는 EventPublisher을 활용하기로 했습니다.
Event 구독 코드
예외 처리 코드는 반복 사용 가능해 보이는 템플릿으로 보여졌기에 Custom Annotation(@HandleFailure)을 만들어 AOP 기능을 활용하여 을 만들어서 catch 문을 분리했습니다.
@EventListener
@HandleFailure
@Async
public void handleCustomEvent(CustomEvent event) {
// 비즈니스 로직...
}
Event 실패 처리 코드
@Aspect
@Component
public class FailureHandlingAspect {
private static final Logger log = LoggerFactory.getLogger(FailureHandlingAspect.class);
private final FailedEventRepository failedEventRepository;
public FailureHandlingAspect(FailureLogRepository failureLogRepository) {
this.failureLogRepository = failureLogRepository;
}
@AfterThrowing(pointcut = "@annotation(HandleFailure)", throwing = "e")
public Object handleFailure(ProceedingJoinPoint joinPoint, Exception e) throws Throwable {
CustomEvent event = (CustomEvent)joinPoint.getArgs()[0];
handleFailedEvent(event);
return joinPoint.proceed();
}
private void handleFailedEvent(CustomEvent event) {
try {
FailedEvent failedEvent = new FailedEvent(event.getMessage(), event.getTimestamp());
failedEventRepository.save(failedEvent);
} catch (Exception e) {
log.error("dbException = {}", e.getMessage());
FailedEventMemorySave(event);
}
}
private void FailedEventMemorySave(CustomEvent event) {
try {
FailedEventMemoryRepository.put(event.getEventType(), event);
} catch (Exception e) {
log.error("memoryStore put Exception = {}", e.getMessage());
log.error("event id = {} ,value = {} , time ={}", event.getId(), event.getValue(), event.getTime);
}
}
}
이벤트가 실패할 수 있는 상황에 대비해, 2 차 이중 장치를 만들어 놓았습니다.
Kafka를 사용하는 방식은 디스크 기반으로 데이터를 저장하기 때문에 안전합니다. 하지만 Spring의 EventPublisher 방식은 메모리 기반이라 데이터 유실의 위험이 큽니다. 이를 보완하기 위해 이벤트 실패 시 DB에 데이터를 저장하는 로직을 추가했으며, DB 서버나 외부 네트워크 장애 발생 시 이벤트가 유실되지 않도록 임시로 메모리에 저장하는 방식을 구현했습니다. 메모리에 저장된 데이터는 스케줄러나 API를 통해 각 비즈니스에 맞게 처리할 수 있도록 설계했습니다.
Event 도입 자체에서는 크게어려운 것이 없고 pub/sub 패턴을 알고 있다면 간단하게 코드로 구현을 할 수 있습니다.
하지만 가장 중요한건 예외 처리이기에 발행한 이벤트가 실패할 경우를 대비해 구현한 것을 정리했습니다.
사용하는 방식이 Queue 방식은 아니기에 DLQ라고 하기에는 무리가 있어서 이벤트 실패라고 표현했습니다.
Retry 정책도 아직 필요성을 느끼지 않아 구현하지 않았고 구현한다면 Spring Retry를 사용하여 구현을 할 것 같습니다.