Java & Spring

AOP를 적용하여 BindingResult 처리

ju_young 2024. 3. 2. 00:16
728x90

Spring validation을 사용할 경우 다음과 같이 bindingResult를 받아 처리할 수 있다.

@PostMapping  
public ResponseEntity<?> testMethod(  
        @Valid FormRequest formRequest,  
        BindingResult bindingResult) {  
    ...
}

그리고 validation을 수행해야하는 api 요청이 하나 둘씩 추가되면 추가된만큼 bindingResult를 처리하는 코드 또한 추가로 작성될 것이다. 이처럼 boilerplate 코드가 발생하게되면서 비효율적인 코드가 되는 것이다.

AOP

annotation 정의

annotation을 추가한 method만 bindingResult를 처리할 수 있게 annotation을 정의한다.

@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.RUNTIME)  
public @interface BindingResultHandler {  
    String message();  
}

Aspect 정의

@Slf4j  
@Component  
@Aspect  
public class BindingResultHandlerAspect {  

    @Pointcut("@annotation(BindingResultHandler)")  
    private void bindingResultPointcut() {}  

    @Around("bindingResultPointcut()")  
    public Object handle(ProceedingJoinPoint pjp) throws Throwable {  
        MethodSignature signature = (MethodSignature) pjp.getSignature();  
        BindingResult bindingResult = getBindingResult(signature, pjp.getArgs());  
        Method method = signature.getMethod();  
        //현재 method에 추가한 annotation (annotation에 지정한 message를 가져오기위함)
        BindingResultHandler annotation = method.getAnnotation(BindingResultHandler.class);  

        //bindingResult 공통 처리 부분 (개인에 따라 수정)
        if (bindingResult.hasErrors()) {  
            log.error("bindingResult: {}", bindingResult);  
            throw new ValidatedException(  
                    ErrorCode.INVALID_REQUEST,  
                    ExceptionResponse.fromBindingResult(annotation.message(), bindingResult));  
        }  

        return pjp.proceed();  
    }  

    /**
    * 현재 method의 parameter 중 bindingResult를 찾아 반환
    */
    private BindingResult getBindingResult(MethodSignature signature, Object[] arguments) {  
        String[] parameterNames = signature.getParameterNames();  
        for (int i = 0; i < parameterNames.length; i++) {  
            if (parameterNames[i].equals("bindingResult")) {  
                return (BindingResult) arguments[i];  
            }  
        }  

        throw new NoSuchParameterException("bindingResult");  
    }  
}

적용

@BindingResultHandler(message = "validation error during test")  
@PostMapping  
public ResponseEntity<?> testMethod(  
        @Valid FormRequest formRequest,  
        BindingResult bindingResult) {  
    ...
}

이로써 validation error가 발생하면 일관성있게 ValidatedException을 throw하게 될 것이고 ExceptionHandler(ControllerAdvice)를 통해 최종적으로 처리되게될 것이다.

@RestControllerAdvice  
public class GlobalExceptionHandler {

    @ExceptionHandler(value = ValidatedException.class)  
    public ResponseEntity<ExceptionResponse> resolveException(ValidatedException e) {  
        return ResponseEntity.status(e.getErrorCode().getStatus()).body(e.getExceptionResponse());  
    }
}

Appendix

@WebMvcTest를 사용한 API 테스트시 @ImportAopAutoConfiguration와 추가한 Aspect 클래스를 추가해주어야한다.

@Import({AopAutoConfiguration.class, BindingResultHandlerAspect.class})  
@WebMvcTest(Api.class)  
class ApiTest {
    ...
}
728x90