본문 바로가기
Back end/Spring

[Spring] Validation

by 더 이프 2023. 8. 11.
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())));
    	}
    }
    • 결과

     

    출처

     

    [SpringBoot] Spring Validation을 이용한 유효성 검증

    springboot에 validation을 적용해boja

    velog.io