validation 이란 프로그래밍에 있어서 가장 중요.
에러를 방지 하기 위해서 미리 검증을 하는 과정을 validation이라고 한다.
@Size | 문자 길이 측정 | Int Type 불가 |
---|---|---|
@NotNull | null 불가 | |
@NotEmpty | null, “”불가 | |
@NotBlank | null, “”, “{space}“ 불가 | |
@Past | 과거 날짜 | |
@PastOrPresent | 오늘이나 과거 날짜 | |
@Future | 미래 날짜 | |
@FutureOrPresent | 오늘이거나 미래 날짜 | |
@Pattern | 정규식 적용 | regexp message |
@Max | 최댓값 | |
@Min | 최솟값 | |
@AsserTrue / False | 별도 Logic 적용 | |
@Valid | 해당 object validation 실행 | |
gradle dependecies
implementation "org.springframework.boot:spring-boot-starter-validation"
https://beanvalidation.org/2.0-jsr380/
메타 문자란 원래 그 문자가 가진 뜻이 아닌 특별한 용도로 사용하는 문자를 의미한다.
. ^ $ * + ? { } [ ] \ | ( )
문자 클래스로 만들어진 정규식은 “[ ] 사이의 문자들과 매치”라는 의미를 가진다.
즉, 정규 표현식이 [abc]
라면 이 표현식의 의미는 “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(.)은 줄바꿈 문자인 \n
를 제외한 모든 문자와 매치됨을 의미한다.
"^\\d{2,3}-\\d{3,4}-\\d{4}$"
ca*t
이 정규식에는 반복을 의미하는 *
메타 문자가 사용되었다. 여기에서 사용한 *
은 *
바로 앞에 있는 문자가 0부터 무한대로 반복될 수 있다는 의미이다.
ca+t
*
가 0번 이상 반복이라면 +
는 최소 1번 이상 반복될 때 사용한다.
{ }
메타 문자를 사용하면 반복 횟수를 고정할 수 있다. {m, n}
정규식을 사용하면 반복 횟수가 m부터 n까지로 매치할 수 있다. 또한 m 또는 n을 생략할 수도 있다. 만약 {3, } 처럼 사용한다면 반복횟수가 3이상을 의미한다.
{m}
반드시 m번 반복{m, n}
m번 이상 n번 이하로 반복?
== {0, 1}
있어도 되고 없어도 된다.implementation "org.springframework.boot:spring-boot-starter-validation"
스프링 프로젝트에 임포트하여 @Pattern
Annotation을 사용하여 정규식을 사용할 수 있다. 자세한 자바 스프링 validation은 요기를 참조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)로 리턴
이외 파이썬 정규식 표현의 링크를 참조하자.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;
}
새로운 Annotation 생성을 통하여 CustomValidation의 재사용성을 높일 수 있다.
@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
에 제약 조건 클래스를 지정해 준다.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을 이용하여 보다 재사용성을 높인 것이다.@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;
}
@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에 만 적용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 내부에서만 적용
@Valid
: 외부에서 들어오는 모든 요청은 디스패처 서블릿을 통해 Controller로 전달됩니다. 이때 컨트롤러에서 JSON 형식의 데이터를 받는 @ResponseBody 어노테이션을 사용하는 경우 유효성 검증을 진행합니다.
@Validated
: @Valid와 달리 @Validated는 AOP를 기반으로 메소드의 요청을 가로채서 유효성 검증을 진행합니다.
@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 검증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
에서 처리