Spring으로 서버 사이드 렌더링을 할때 예외상황에서 리디렉트를 해야할 상황이 생긴다.

Spring AOP와 Annotation을 조합하여 특정 예외가 던져졌을 시 리디렉트를 할 수 있는 코드 스니펫이다.

먼저 체크할 예외와 리디렉트할 경로를 설정하는 어노테이션이다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedirectOnException {
    // 체크할 예외
    Class<? extends Exception>[] exceptions() default {Exception.class};

    // 리디렉트 경로
    String value();
}

기본적으로, 동일한 어노테이션을 중복해서 쌓을 수 없다. 만약 예외별로 리디렉트 경로를 다르게 바꾸고 싶다면 어노테이션 컨테이너를 정의하면 된다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(RedirectOnExceptionContainer.class) // Repeatable Annotation
public @interface RedirectOnException {
    Class<? extends Exception>[] exceptions() default {Exception.class};
    String value();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedirectOnExceptionContainer {
    RedirectOnException[] value();
}

이제 Annotation을 감시하는 Aspect를 정의한다.

@Aspect
@Component
public class RedirectAspect {
    @Pointcut("@annotation(com.flowerfulfort.annotation.RedirectOnException)")
    public void redirectCut(){}

    // 다중 RedirectOnException 적용했을 경우, 
    // 실제로 RedirectOnExceptionContainer 가 붙어있는 것으로 취급함.
    @Pointcut("@annotation(com.flowerfulfort.annotation.RedirectOnExceptionContainer)")
    public void redirectContainerCut(){}

    // RedirectOnException 어노테이션을 읽어 예외가 발생했을때
    // 해당 경로로 메세지와 함께 throw 하여
    // ExceptionControllerAdvice 에서 캐치하게 해줌.
    @Around("redirectCut() || redirectContainerCut()")
    public Object redirectThrowable(ProceedingJoinPoint joinPoint) throws Throwable {
        Object value;
        try {
            value = joinPoint.proceed();
        }catch (Exception e) {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            // @RedirectOnException 을 여러개 붙일수 있음!
            RedirectOnException[] r = 
                method.getAnnotationsByType(RedirectOnException.class);
            if (r.length == 0) {
                throw new IllegalArgumentException();
            }
            for (RedirectOnException redirect: r) {
                Class<? extends Exception>[] target = redirect.exceptions();
                // annotation 에서 설정한 class 의 하위 클래스이면
                // RedirectRuntimeException 으로 재포장해서 throw.
                for (Class<? extends Exception> clazz: target) {
                    // 예외를 clazz로 assign 할 수 있는지 체크
                    if (clazz.isAssignableFrom(e.getClass())) {
                        throw new RedirectException(e, redirect.value());
                    }
                }
            }
            // 아니면 그냥 throw.
            throw e;
        }
        return value;
    }
}

joinPoint.proceed()를 try 문으로 감싸 예외를 체크한다. 만약 Annotation에서 넘겨받은 예와타입으로 취급할 수 있는 경우, RedirectException으로 감싸 다시 던진다.

다음은 다시 던지는 예외와 해당 예외를 처리하는 ControllerAdvice 이다.

@Getter
public class RedirectException extends RuntimeException {
    private final String redirect;
    public RedirectRuntimeException(Exception e, String redirectTo) {
        super(e);
        this.redirect = redirectTo;
    }

    // 성능 개선을 위한 fillInStackTrace 재정의
    // 참조: https://meetup.nhncloud.com/posts/47
    @Override
    public synchronized Throwable fillInStackTrace() {
        return this;
    }
}

@ControllerAdvice
public class ExceptionControllerAdvice {
    @ExceptionHandler({RedirectException.class})
    public ModelAndView redirectExceptionHandler(RedirectException e) {
        ModelAndView mv = new ModelAndView("error/alertMessageRedirect");
        mv.addObject("exception", ExceptionUtils.getRootCauseMessage());
        mv.addObject("redirect", e.getRedirect());
        return mv;
    }
}

예외에서 redirect 주소와, Apache commons lang3 패키지의 ExceptionUtils를 이용해 Cause exception의 메시지를 꺼내 Model attribute로 사용한다.

fillInStackTrace()를 재정의 하였는데, 자바의 Exception은 생성될때 스택 추적 로그를 생성한다. 이 예외에서 스택로그를 추적할 이유는 없고 자바는 스택 로그를 쌓을 때 많은 비용을 소모하므로 재정의하여 성능을 개선한다.

Thymeleaf 기준으로, 다음과 같은 간단한 Alert 메시지와 함께 리디렉트를 할 수 있다.

<script th:inline="javascript">
    alert([[${exception}]]);
    window.location.href=[[${redirect}]];
</script>