
최주호 강사님의 인프런 강좌 정리 및 실습한 기록
개발에 빠질수가 없는 유효성 검사
AOP 를 적용해 글로벌하게 적용시켜보자.
@Validation 관련해서는 아주 간단한 처리만 되어 있는 상태이고, 목적은 다음과 같다.
CustomValidationAdvice@Pointcut, @Around 적용ProceedingJoinPoint 에서 args 추출args 를 CustomValidationException 에 전달CustomValidationExceptionRuntimeException 를 상속CustomExcetpionHandlerResponseEntity 로 Presentation Layer 에 매번 동일한 형식의 에러를 내보낼 수 있게 된다.@Aspect 가 적용되면 무슨 일을 할 수 있을까?
org.aspectj.lang.annotation.Aspect
org.aspectj.lang.annotation.Around
org.aspectj.lang.annotation.Pointcut
@Aspect
@Component
public class CustomValidationAdvice {
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void postMapping() {
}
@Pointcut("@annotation(org.springframework.web.bind.annotation.PutMapping)")
public void putMapping() {
}
// @Around -> joinPoint 전후 제어
@Around("postMapping() || putMapping()")
public Object validationAdvice(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs(); // joinPoint 의 매개변수들
for (Object arg : args) {
if (arg instanceof BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Map<String, String> errorMap = new HashMap<>();
for (FieldError error : bindingResult.getFieldErrors()) {
errorMap.put(error.getField(), error.getDefaultMessage());
}
throw new CustomValidationException("유효성 검사 실패", errorMap);
}
}
}
return pjp.proceed(); // 정상 실행
}
}
@Around 로 인해서 @Pointcut 의 타겟 메서드의 실행 전, 후로 로직을 추가할 수 있다 (이거 완전 Proxy 패턴이잖아?)
BindingResult 인 경우, 즉 에러가 터진 경우 어떻게 하면 좋을까?
throw new ....exception 해서 RuntimeException 을 그냥 내보내는 경우 개발자가 예쁘게 포장해서 프론트로 보낼 수가 없다. 매번 오류 형태가 달라질 것이고 일관성이란게 존재하지 않는 상황이 펼쳐진다.
일단 자바 객체로 만들고, JSON 형태로 한번 감싸서 보내야 나중에 로그 찾아볼 때도 편하고, 오류가 터진 경우 대응이 훨씬 수월할 것이다. (에러에 관한 처리를 하지 않는다면 프론트에서 생긴 문제를 DB 관련 에러로 인식해 한세월 낭비할 수 있다.)
@Getter
public class CustomValidationException extends RuntimeException {
// 에러 정보가 담긴다.
private final Map<String, String> errorMap;
public CustomValidationException(String message, Map<String, String> errorMap) {
super(message);
this.errorMap = errorMap;
}
}
발생한 에러를 담기만 하면 되는 간단한 클래스이다. JSON 형태로 만들어야 프론트와 원활한 통신이 가능하므로 Map<String, String> 형태로 에러에 관한 내용을 담는다.
Map 에 담는 로직은 위의 CustomValidationAdvice 에서 담당한다.
지금까지 코드로 에러가 발생하는 경우, RuntimeException 객체에 저장되어 있는 오류에 관한 내용을 내가 직접 만든 에러 객체에 담았다.
그 다음 에러 객체를 프론트로 내보내기만 하면 된다. AOP 를 적용하지 않았다면, 모든 컨트롤러에 에러 객체를 담은 ResponseEntity 를 리턴하는 코드를 작성해야할 것이다.
AOP 를 적용했기 때문에, @ControllerAdvice 가 적용된 클래스 안에서 해당 에러가 발생한 경우, 대응할 로직을 작성할 수 있게된다.
@ResponseBody 와 @ControllerAdvice 를 합해서 @RestControllerAdvice 를 사용한 CustomExceptionHandler 에서 말이다.
파싱된 에러를 프론트로 내보낸다.
@RestControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(CustomValidationException.class)
// @ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<?> validationApiException(CustomValidationException e) {
logger.error(e.getMessage());
return new ResponseEntity<>(new ResponseDTO<>(
-1,
e.getMessage(),
e.getErrorMap()
), HttpStatus.BAD_REQUEST);
}
}