728x90
목차
1. Validation
■ Validation이란?
Validation은 올바르지 않은 데이터를 걸러내고 보안을 유지하기 위해 여러 계층에 걸쳐서 적용되는 것을 말합니다. Client의 요청 데이터가 모두 정상적인 방식으로 들어오는 것도 아니기 때문에 데이터의 유효성 검사를 해야 할 필요가 있습니다.
■ 설치
- MVN에서 Validation을 버전에 상관없이 dependency에 작성 후 버전을 지우고 설치
2. Validation Exception 처리
@Valid를 사용하지 않고 직접 Exception을 만들어 조건을 주어 예외 처리를 진행하도록 하는 방법입니다.
■ 예시
a. CustomException
- 메세지를 매개변수로 가지는 생성자
- 메세지와 각 error들의 정보를 가질 수 있는 errorMap을 매개변수로 받는 생성자
- RuntimeException 상속
package com.web.study.exception;
import java.util.Map;
import lombok.Getter;
@Getter
public class CustomException extends RuntimeException{
private static final long serialVersionUID = 2658314737117138818L;
private Map<String, String> errorMap;
public CustomException(String message) {
super(message);
}
public CustomException(String message, Map<String, String> errorMap) {
super(message);
this.errorMap = errorMap;
}
}
b. ApiControllerAdvice
- @RestControllerAdvice를 적용하여 예외가 발생했을 때 동작
- @ExceptionHandler는 괄호안에 발생할 Exception의 Class를 작성하여 해당 Exception이 발생할 시 추적하여 동작
package com.web.study.controller.advice;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.web.study.dto.ErrorResponseDto;
import com.web.study.exception.CustomException;
// 예외 처리용 controller
@RestControllerAdvice
public class ApiControllerAdvice {
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponseDto> error(CustomException e) {
return ResponseEntity.badRequest().body(
ErrorResponseDto.of(HttpStatus.BAD_REQUEST, e, e.getErrorMap()));
}
}
c. CourseController
- 요청받은 매개변수의 값이 조건을 만족하지 않을 때 각 매개변수에 대한 error를 errorMap에 등록
- errorMap에 값이 있으면 Exception 처리
package com.web.study.controller.lecture;
import java.util.HashMap;
import java.util.Map;
import javax.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.web.study.aop.annotation.CheckNameAspect;
import com.web.study.aop.annotation.ParamsAspect;
import com.web.study.aop.annotation.TimerAspect;
import com.web.study.aop.annotation.ValidAspect;
import com.web.study.dto.DataResponseDto;
import com.web.study.dto.ResponseDto;
import com.web.study.dto.request.Course.CourseReqDto;
import com.web.study.dto.request.Course.SearchCourseReqDto;
import com.web.study.exception.CustomException;
import com.web.study.service.CourseService;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
public class CourseController {
private final CourseService courseService;
@PostMapping("/course")
public ResponseEntity<? extends ResponseDto> register(@RequestBody CourseReqDto courseReqDto) {
courseService.registeCourse(courseReqDto);
return ResponseEntity.ok().body(ResponseDto.ofDefault());
}
@CheckNameAspect
@TimerAspect
@GetMapping("/courses")
public ResponseEntity<? extends ResponseDto> getCourseAll() {
return ResponseEntity.ok().body(DataResponseDto.of(courseService.getCourseAll()));
}
// @ValidAspect
@ParamsAspect
@GetMapping("/search/courses")
public ResponseEntity<? extends ResponseDto> searchCourse(int type, String searchValue) {
// 처음 예외 처리
Map<String, String> errorMap = new HashMap<>();
if(type < 1 || type > 3) {
errorMap.put("type","type은 1에서 3의 사이값만 사용할 수 있습니다.");
}
if(searchValue == null) {
errorMap.put("searchValue","searchValue는 필수입니다.");
} else {
if (searchValue.isBlank()) {
errorMap.put("searchValue","searchValue는 공백일 수 없습니다.");
}
}
if(!errorMap.isEmpty()) {
throw new CustomException("유효성 검사 실패", errorMap);
}
return ResponseEntity.ok().body(
DataResponseDto.of(courseService.searchCourse(type, searchValue)));
}
}
- 결과
3. @Valid
@Valid는 빈 검증기를 이용해 객체의 제약 조건을 검증하도록 지시하는 어노테이션이다. 즉, @Valid를 사용하여 유효성 검사를 진행하며 기존에 만들어 놓은 예외 처리용 클래스인 CustomException, ApiControllerAdvice는 그대로 사용 가능
■ 예시
a. SearchCourseReqDto
- 해당 메소드의 요청을 처리할 수 있는 RequestDto를 생성
- 기존에 요청을 받던 매개변수 type, searchValue를 필드로 선언하여 사용
- @Min은 최소값을 정하고, @Max는 최대값을 설정
- @NotBlank는 값이 빈 값일 때 해당 메세지를 전달
- 괄호안의 message에는 기본값이 정해져 있고 값을 주어 수정 가능
package com.web.study.dto.request.Course;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Setter
@Getter
@ToString
public class SearchCourseReqDto {
@Min(value = 1)
@Max(value = 3)
private int type;
@NotBlank(message = "검색 내용을 입력해주세요.")
private String searchValue;
}
b. CourseController
- 요청을 받기 위해 매개변수에 RequestDto를 작성한 뒤 유효성 검사를 위해 @Valid를 앞에 적용
- @Valid를 적용하면 매개변수에 BindingResult도 같이 적용 (둘이 세트로 생각)
- BindingResult는 검증 오류를 보관하는 객체
- 유효성 검사 실패시 BindingResult에는 error가 존재하기 때문에 hasErrors메소드를 사용하여 조건식 적용
- RequestDto 필드에 오류가 발생하면 각각의 필드에 대한 오류를 errorMap에 등록
- Exception을 던져 Exception 처리
package com.web.study.controller.lecture;
import java.util.HashMap;
import java.util.Map;
import javax.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.web.study.aop.annotation.CheckNameAspect;
import com.web.study.aop.annotation.ParamsAspect;
import com.web.study.aop.annotation.TimerAspect;
import com.web.study.aop.annotation.ValidAspect;
import com.web.study.dto.DataResponseDto;
import com.web.study.dto.ResponseDto;
import com.web.study.dto.request.Course.CourseReqDto;
import com.web.study.dto.request.Course.SearchCourseReqDto;
import com.web.study.exception.CustomException;
import com.web.study.service.CourseService;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
public class CourseController {
private final CourseService courseService;
@PostMapping("/course")
public ResponseEntity<? extends ResponseDto> register(@RequestBody CourseReqDto courseReqDto) {
courseService.registeCourse(courseReqDto);
return ResponseEntity.ok().body(ResponseDto.ofDefault());
}
@CheckNameAspect
@TimerAspect
@GetMapping("/courses")
public ResponseEntity<? extends ResponseDto> getCourseAll() {
return ResponseEntity.ok().body(DataResponseDto.of(courseService.getCourseAll()));
}
// @ValidAspect
@ParamsAspect
@GetMapping("/search/courses")
public ResponseEntity<? extends ResponseDto> searchCourse(
@Valid SearchCourseReqDto searchCourseReqDto, BindingResult bindingResult) {
// 두번째 예외 처리
if(bindingResult.hasErrors()) {
Map<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach(error -> {
errorMap.put(error.getField(), error.getDefaultMessage());
});
throw new CustomException("유효성 검사 실패",errorMap);
}
// 처음 예외 처리
// Map<String, String> errorMap = new HashMap<>();
//
// if(type < 1 || type > 3) {
// errorMap.put("type","type은 1에서 3의 사이값만 사용할 수 있습니다.");
// }
//
// if(searchValue == null) {
// errorMap.put("searchValue","searchValue는 필수입니다.");
// } else {
// if (searchValue.isBlank()) {
// errorMap.put("searchValue","searchValue는 공백일 수 없습니다.");
// }
//
// }
//
// if(!errorMap.isEmpty()) {
// throw new CustomException("유효성 검사 실패", errorMap);
// }
return ResponseEntity.ok().body(
DataResponseDto.of(courseService.searchCourse(searchCourseReqDto.getType(),
searchCourseReqDto.getSearchValue())));
}
}
- 결과
4. ValidationAop
요청받은 RequestDto를 코드에서 사용하지 않기 때문에 부가적인 기능으로 AOP 가능합니다.
■ 예시
a. ValidationAop
- 조건식의 bindingResult를 적용하기 위해서 매개변수에서 bindingResult를 가져옴
- 해당 bindingResult의 클래스명은 BeanPropertyBindingResult로 확인
- 해당 메소드의 매개변수들을 foreach문을 사용하여 꺼낸 뒤 BeanPropertyBindingResult의 클래스와 일치하는지 확인
- 해당 메소드의 getArgs()는 Object[]을 리턴하므로 bindingResult에 대입 시 다운캐스팅
package com.web.study.aop;
import java.util.HashMap;
import java.util.Map;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import com.web.study.exception.CustomException;
@Aspect
@Component
public class ValidationAop {
@Pointcut("@annotation(com.web.study.aop.annotation.ValidAspect)")
private void pointCut() {}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
BeanPropertyBindingResult bindingResult = null;
for(Object obj : joinPoint.getArgs()) {
if(obj.getClass() == BeanPropertyBindingResult.class) {
bindingResult = (BeanPropertyBindingResult) obj;
}
}
if(bindingResult.hasErrors()) {
Map<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach(error -> {
errorMap.put(error.getField(), error.getDefaultMessage());
});
throw new CustomException("유효성 검사 실패", errorMap);
}
return joinPoint.proceed();
}
}
b. ValidAspect
package com.web.study.aop.annotation;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(RUNTIME)
@Target(METHOD)
public @interface ValidAspect {
}
c. CourseController
package com.web.study.controller.lecture;
import java.util.HashMap;
import java.util.Map;
import javax.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.web.study.aop.annotation.CheckNameAspect;
import com.web.study.aop.annotation.ParamsAspect;
import com.web.study.aop.annotation.TimerAspect;
import com.web.study.aop.annotation.ValidAspect;
import com.web.study.dto.DataResponseDto;
import com.web.study.dto.ResponseDto;
import com.web.study.dto.request.Course.CourseReqDto;
import com.web.study.dto.request.Course.SearchCourseReqDto;
import com.web.study.exception.CustomException;
import com.web.study.service.CourseService;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
public class CourseController {
private final CourseService courseService;
@PostMapping("/course")
public ResponseEntity<? extends ResponseDto> register(@RequestBody CourseReqDto courseReqDto) {
courseService.registeCourse(courseReqDto);
return ResponseEntity.ok().body(ResponseDto.ofDefault());
}
@CheckNameAspect
@TimerAspect
@GetMapping("/courses")
public ResponseEntity<? extends ResponseDto> getCourseAll() {
return ResponseEntity.ok().body(DataResponseDto.of(courseService.getCourseAll()));
}
@ValidAspect
@ParamsAspect
@GetMapping("/search/courses")
public ResponseEntity<? extends ResponseDto> searchCourse(
@Valid SearchCourseReqDto searchCourseReqDto, BindingResult bindingResult) {
return ResponseEntity.ok().body(
DataResponseDto.of(courseService.searchCourse(searchCourseReqDto.getType(),
searchCourseReqDto.getSearchValue())));
}
}
- 결과