Validation, 정규식

duckbill413·2023년 1월 21일
0

Spring boot

목록 보기
1/8
post-thumbnail

Validation

validation 이란 프로그래밍에 있어서 가장 중요.

에러를 방지 하기 위해서 미리 검증을 하는 과정을 validation이라고 한다.

  1. 검증해야 할 값이 많을 경우 코드의 길이가 길어 진다.
  2. 구현에 따라서 달라 질 수 있지만 Service Logic과의 분리가 필요하다.
  3. 흩어져 있는 경우 어디에서 검증을 하는지 알기 어려우며, 재사용의 한계가 있다.
  4. 구현에 따라 달라 질 수 있지만, 검증 Logic이 변경 되는 경우 테스트 코드 증 참조하는 클래스에서 Logic이 변경되어야 하는 부분이 발생 할 수 있다.

Validation Annotation

@Size문자 길이 측정Int Type 불가
@NotNullnull 불가
@NotEmptynull, “”불가
@NotBlanknull, “”, “{space}“ 불가
@Past과거 날짜
@PastOrPresent오늘이나 과거 날짜
@Future미래 날짜
@FutureOrPresent오늘이거나 미래 날짜
@Pattern정규식 적용regexp message
@Max최댓값
@Min최솟값
@AsserTrue / False별도 Logic 적용
@Valid해당 object validation 실행

Spring Dependency

gradle dependecies

implementation "org.springframework.boot:spring-boot-starter-validation"

bean validation spec

https://beanvalidation.org/2.0-jsr380/

정규식

점프 투 파이썬

정규 표현식의 기초 메타 문자

메타 문자란 원래 그 문자가 가진 뜻이 아닌 특별한 용도로 사용하는 문자를 의미한다.

. ^ $ * + ? { } [ ] \ | ( )

문자 클래스 [ ]

문자 클래스로 만들어진 정규식은 “[ ] 사이의 문자들과 매치”라는 의미를 가진다.

즉, 정규 표현식이 [abc]라면 이 표현식의 의미는 “a, b, c중 한 개의 문자와 매치”를 뜻한다.

  • “a”는 문자 “a”를 포함 므로 매치
  • “before”은 문자 “b”를 포함하므로 매치
  • “dude”는 a, b, c를 포함하지 않으모로 매치되지 않음

[ ] 안의 두 문자 사이에 하이픈(-)을 사용하면 두 문자 사이의 범위를 의마한다.

  • [a-zA-Z] : 알파벳 대소문자 모두
  • [0-9] : 숫자

^ 는 Not의 의미를 가진다. [^0-9] 이면 숫자를 제외한 모든 문자에 매치된다.

  • 자주 사용되는 문자 클래스
    • \d - 숫자와 매치, [0-9]와 동일한 표현식
    • \D - 숫자가 아닌 것과 매치, [^0-9]와 동일
    • \s - whitespace 문자와 매치, [ \t\n\r\f\v]와 동일한 표현식이다. 맨 앞의 빈 칸은 공백문자(space)를 의미한다.
    • \S - whitespace 문자가 아닌 것과 매치, [^ \t\n\r\f\v]와 동일한 표현식이다.
    • \w - 문자+숫자(alphanumeric)와 매치, [a-zA-Z0-9_]와 동일한 표현식이다.
    • \W - 문자+숫자(alphanumeric)가 아닌 문자와 매치, [^a-zA-Z0-9_]와 동일한 표현식이다.

Dot(.)

정규 표현식의 Dot(.)은 줄바꿈 문자인 \n 를 제외한 모든 문자와 매치됨을 의미한다.

  • 핸드폰 번호 정규식
"^\\d{2,3}-\\d{3,4}-\\d{4}$"

반복(*)

ca*t 이 정규식에는 반복을 의미하는 * 메타 문자가 사용되었다. 여기에서 사용한 ** 바로 앞에 있는 문자가 0부터 무한대로 반복될 수 있다는 의미이다.

반복(+)

ca+t * 가 0번 이상 반복이라면 +는 최소 1번 이상 반복될 때 사용한다.

반복({m, n}, ?)

{ } 메타 문자를 사용하면 반복 횟수를 고정할 수 있다. {m, n}정규식을 사용하면 반복 횟수가 m부터 n까지로 매치할 수 있다. 또한 m 또는 n을 생략할 수도 있다. 만약 {3, } 처럼 사용한다면 반복횟수가 3이상을 의미한다.

  • {m} 반드시 m번 반복
  • {m, n} m번 이상 n번 이하로 반복
  • ? == {0, 1} 있어도 되고 없어도 된다.

정규 표현식 지원 모듈

  • Java Spring
    implementation "org.springframework.boot:spring-boot-starter-validation"
    스프링 프로젝트에 임포트하여 @PatternAnnotation을 사용하여 정규식을 사용할 수 있다. 자세한 자바 스프링 validation은 요기를 참조
  • Python
    import re
    p = re.compile('[a-z]+')
    
    # match: match 객체를 돌려준다.
    # not match: None을 리턴
    
    ''' Match '''
    m = p.match('3 python')
    if m:
    	print('Match found: ', m.group())
    else:
    	print('No match')
    
    ''' Search '''
    >>> m = p.search("python")
    >>> print(m)
    <re.Match object; span=(0, 6), match='python'>
    
    >>> m = p.search("3 python")
    >>> print(m)
    <re.Match object; span=(2, 8), match='python'>
    
    ''' findall '''
    >>> result = p.findall("life is too short")
    >>> print(result)
    ['life', 'is', 'too', 'short'] # 매치되는 모든 값을 리스트로 리턴
    
    ''' finditer '''
    >>> result = p.finditer("life is too short")
    >>> print(result)
    <callable_iterator object at 0x01F5E390>
    >>> for r in result: print(r)
    ...
    <re.Match object; span=(0, 4), match='life'>
    <re.Match object; span=(5, 7), match='is'>
    <re.Match object; span=(8, 11), match='too'>
    <re.Match object; span=(12, 17), match='short'>
    # 반복 가능한 객체(iterator)로 리턴
    이외 파이썬 정규식 표현의 링크를 참조하자.

Custom Validation

  1. AssertTrue / False 와 같은 method 지정을 통해서 Custom Logic 적용 가능
  2. ConstraintValidator를 적용하여 재사용이 가능한 Custom Logic 적용 가능

AssertTrue / AssertFalse

AssertTrue / False같은 method를 사용하여 Custom Logic 적용이 가능하다 다만 재사용은 불가능 하다.

private String reqYearMonth;

@AssertTrue(message = "yyyyMM 형식에 맞지 않습니다.") // MEMO: Boolean Method 는 앞에 is가 붙어야 한다.
public Boolean isReqYearMonthValidation() {
    try {
        LocalDate localDate = LocalDate.parse(this.reqYearMonth+"01",
															DateTimeFormatter.ofPattern("yyyyMMdd"));
    } catch (Exception e) {
        return false;
    }
    return true;
}

ConstraintValidator

새로운 Annotation 생성을 통하여 CustomValidation의 재사용성을 높일 수 있다.

  • YearMonth Annotion 생성
    @Constraint(validatedBy = {YearMonthValidator.class})
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    public @interface YearMonth {
        String message() default "yyyyMM의 형식에 맞지 않습니다.";
    
        Class<?>[] groups() default { };
    
        Class<? extends Payload>[] payload() default { };
    
        String pattern() default "yyyyMMdd";
    }
    @Constraint에 제약 조건 클래스를 지정해 준다.
  • ConstraintValidator 제약 조건 인터페이스
    public class YearMonthValidator implements ConstraintValidator<YearMonth, String> {
        private String pattern; // 정규 표현식 YearMonth Annotion의 패턴이 들어온다.
        @Override
        public void initialize(YearMonth constraintAnnotation) {
            this.pattern = constraintAnnotation.pattern();
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            // yyyyMM01~31
            try {
                LocalDate localDate = LocalDate.parse(value+"01", 
    																	DateTimeFormatter.ofPattern(this.pattern));
            } catch (Exception e) {
                return false;
            }
            return true;
        }
    }
    AssertTrue / False를 Annotation을 이용하여 보다 재사용성을 높인 것이다.
  • User Class
    @Getter
    @Setter
    @ToString
    @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
        @NotBlank
        private String name;
    
        @Min(value = 0)
        @Max(value = 90)
        private int age;
    
        @Email
        private String email;
    
        @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$",
                message = "핸드폰 번호의 양식과 맞지 않습니다. xxx-xxx(x)-xxxx")
        private String phoneNumber;
    
        @YearMonth // MEMO: Custom Annotation
        private String reqYearMonth;
    
        @Valid // Validation을 적용하기 위해서는 하위 클래스에 @Valid 적용이 필요
        private List<Car> cars;
    }

Spring Boot Exception

  • @ControllerAdvice : Global 예외 처리 및 특정 package / Controller 예외 처리

  • @ExceptionHandler : 특정 Controller 예외 처리

  • @RestControllerAdvice(Rest Api) — @ControllerAdvice(View Controller)

    //INFO: basePackages경로 지정을 통하여 원하는 패키지만 Exception Handle을 할 수 있다.
    //INFO:지정하지 않을 경우 Global하게 적용
    //@RestControllerAdvice(basePackageClasses = ApiController.class) // 해당하는 Controller 에만 적용
    @RestControllerAdvice(basePackages = "com.example.springexception.controller")
    public class GlobalControllerAdvice {
        @ExceptionHandler(value = Exception.class)
        public ResponseEntity exception(Exception e){
            System.out.println(e.getClass().getName());
            System.out.println(e.getLocalizedMessage());
    
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
        }
    
        @ExceptionHandler(value = MethodArgumentNotValidException.class)
        public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e){
            System.out.println("---------------global--------------------");
            System.out.println(e.getClass().getName());
            System.out.println(e.getLocalizedMessage());
    
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
        }
    }
    • basePackages 경로 지정 ← 지정된 경로의 controller에만 local 적용
    • basePackages 경로 미지정 ← global하게 적용
    • basePackageClassses ← 지정된 controller에 만 적용
    • Exception 처리 하려는 예외만 별도로 처리 하는 것 또한 가능 (ex: MethodArgumentNotValidException)
  • Controller

    @RestController
    @RequestMapping("/api")
    public class ApiController {
        @GetMapping("")
        public User get(@RequestParam(required = false) String name,
                        @RequestParam(required = false) Integer age){
            User user = new User();
            user.setName(name);
            user.setAge(age);
            System.out.println(user);
    
            int testAge = age + 10;
    
            return user;
        }
    
        @PostMapping("")
        public User post(@Valid @RequestBody User user){
            System.out.println(user);
            return user;
        }
    
        //INFO: ExceptionHandler를 Controller안에 지정하면 해당 Controller에 대해서만 작동
    //INFO: Global Exception Handler는 작동되지 않는다.
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
        public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e){
            System.out.println("------------ApiController Local----------------");
            System.out.println(e.getClass().getName());
            System.out.println(e.getLocalizedMessage());
    
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
        }
    }

    Controller 내부에 @ExceptionHandler 부착시 해당 Controller 내부에서만 적용

Spring Valiation Uses

@Valid : 외부에서 들어오는 모든 요청은 디스패처 서블릿을 통해 Controller로 전달됩니다. 이때 컨트롤러에서 JSON 형식의 데이터를 받는 @ResponseBody 어노테이션을 사용하는 경우 유효성 검증을 진행합니다.

@Validated : @Valid와 달리 @Validated는 AOP를 기반으로 메소드의 요청을 가로채서 유효성 검증을 진행합니다.

  • Controller
    @RestController
    @RequestMapping("/api")
    @Validated
    public class ApiController {
    		@GetMapping("") //MEMO: GET Method에 대한 Validation
    		public User get(
            @Size(min = 2)
            @RequestParam String name,
    
            @NotNull
            @Min(1)
            @RequestParam Integer age) {
            User user = new User();
            user.setName(name);
            user.setAge(age);
            System.out.println(user);
    
            return user;
        }
    
        @PostMapping("")
        public User post(@Valid @RequestBody User user) {
            System.out.println(user);
            return user;
        }
    }
    @Validated 어노테이션으로 Get Method 검증
  • Validation Advice
    • MethodArgumentNotValidException

      @ExceptionHandler(value = MethodArgumentNotValidException.class)
      public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e,
                                                            HttpServletRequest httpServletRequest) {
          List<Error> errorList = new ArrayList<>();
          BindingResult bindingResult = e.getBindingResult();
      
          System.out.println("MethodArgumentNotValidException Error");
          bindingResult.getAllErrors().forEach(objectError -> {
              FieldError fieldError = (FieldError) objectError;
      
              String fieldName = fieldError.getField();
              String message = fieldError.getDefaultMessage();
              String value = fieldError.getRejectedValue().toString();
      
              System.out.println(fieldName + "\t" + message + "\t" + value);
      
              Error error = new Error(fieldName, message, value);
              errorList.add(error);
          });
      
          ErrorResponse errorResponse = new ErrorResponse();
          errorResponse.setMessage("");
          errorResponse.setCode("");
          errorResponse.setRequestUrl(httpServletRequest.getRequestURI());
          errorResponse.setStatusCode(HttpStatus.BAD_REQUEST.toString());
          errorResponse.setResultCode("FAIL");
          errorResponse.setErrorList(errorList);
          return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
      }
    • ConstraintiolationException

      @ExceptionHandler(value = ConstraintViolationException.class)
      public ResponseEntity constraintViolationException(ConstraintViolationException e,
                                                         HttpServletRequest httpServletRequest) {
          System.out.println("ConstraintViolationException Error");
          List<Error> errorList = new ArrayList<>();
          e.getConstraintViolations().forEach(constraintViolation -> {
              Stream<Path.Node> stream = StreamSupport.stream(constraintViolation.getPropertyPath().spliterator(), false);
              List<Path.Node> list = stream.collect(Collectors.toList());
      
              String fieldName = list.get(list.size()-1).getName();
              String message = constraintViolation.getMessage();
              String invalidValue = constraintViolation.getInvalidValue().toString();
              System.out.println(fieldName + "\t" + message + "\t" + invalidValue);
      
              Error error = new Error(fieldName, message, invalidValue);
              errorList.add(error);
          });
          ErrorResponse errorResponse = new ErrorResponse();
          errorResponse.setMessage("");
          errorResponse.setCode("");
          errorResponse.setRequestUrl(httpServletRequest.getRequestURI());
          errorResponse.setStatusCode(HttpStatus.BAD_REQUEST.toString());
          errorResponse.setResultCode("FAIL");
          errorResponse.setErrorList(errorList);
          return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
      }
    • MissingServletRequestParameterException

          @ExceptionHandler(value = MissingServletRequestParameterException.class)
          public ResponseEntity missingServletRequestParameterException(MissingServletRequestParameterException e,
                                                                        HttpServletRequest httpServletRequest) {
              System.out.println("MissingServletRequestParameterException");
          
              List<Error> errorList = new ArrayList<>();
          
              String fieldName = e.getParameterName();
              String fieldType = e.getParameterType();
              String invalidValue = e.getMessage();
          
              System.out.println(fieldName + "\t" + fieldType + "\t" + invalidValue);
              errorList.add(new Error(fieldName, fieldType, invalidValue));
          
              ErrorResponse errorResponse = new ErrorResponse();
              errorResponse.setMessage("");
              errorResponse.setCode("");
              errorResponse.setRequestUrl(httpServletRequest.getRequestURI());
              errorResponse.setStatusCode(HttpStatus.BAD_REQUEST.toString());
              errorResponse.setResultCode("FAIL");
              errorResponse.setErrorList(errorList);
          
              return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
          }

      해당 Exception Throw시 @RestControllerAdvice에서 처리

profile
같이 공부합시다~

0개의 댓글