Java의 경우 null값에 대해 접근하려할 때 null Pointer Exception이 발생함
-> 이러한 부분을 방지하기 위해 미리 검증하는 과정 Validation
코드 예시
public void run(String account, String pw, int age){
if(account == null || pw == null){
return
}
if(age == 0){
return
}
// 정상 Logic -> 위에다가 검증을 하는 것은 바람직하지 않음
검증해야 할 값이 많은 경우 코드가 길어진다
구현에 따라서 달라 질 수 있지만 Service Logic과의 분리가 필요 하다
흩어져 있는 경우 어디에서 검증을 하는지 알기 어려우며, 재사용에 한계
검증 Logic이 변경되는 경우 테스트 코드 등 참조하는 클래스에서 Logic이 변경되어야 하는 부분 발생
grade dependencies에 추가 해야함
implementation("org.springframework.boot:spring-boot-starter-validation")
bean validation spec은 필요 시 찾아보기
Validation을 하지 않으면 유효하지 않은 값까지 들어옴
또한 생각 없이 그냥 적으면 ApiController코드에다가 무작정 if~else 사용 하게 됨
사용자의 HttpRequest에 대한 응답 데이터를 포함하는 클래스
프레임워크에서 제공하는 클래스중 HttpEntity(Http 요청, 응답) 클래스 존재
-> HttpHeader와 HttpBody를 포함하는 클래스
HttpEntity 클래스를 상속받아 구현한 클래스가 RequestEntity, ResponseEntity 클래스
package com.example.validation.dto;
import javax.validation.constraints.Email;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
public class User {
@NotBlank
private String name;
@Max(value = 90)
private int age;
@Email
private String email;
// 정규식 활용 : 필요한거 왠만하면 찾아쓰자!, 원하는 형태로 메세지 띄우고자한다면 인자로 message
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message =
"핸드폰 번호의 양식과 일치하지 않습니다. 01x-xxx-xxxx" )
private String phoneNumber;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
@Override
public String toString() {
return "user{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
", email='" + email + '\'' +
", phoneNumber='" + phoneNumber + '\'' +
'}';
}
}
package com.example.validation.controller;
import com.example.validation.dto.User;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
@RequestMapping("/api")
public class ApiController {
@PostMapping("/user")
// @Valid 가 붙은 객체는 어노테이션을 통해 검증을 진행하게 된다
// 예외처리 방법 말고 예외값을 다루고 싶다면 BindingResult 활용하게 됨
// Validation에 대한 결과가 bindingResult에 들어오게 됨
public ResponseEntity user(@Valid @RequestBody User user, BindingResult bindingResult){
if(bindingResult.hasErrors()){
StringBuilder sb = new StringBuilder();
bindingResult.getAllErrors().forEach(objectError -> {
FieldError field = (FieldError) objectError;
String message = objectError.getDefaultMessage();
System.out.println("field " + field.getField());
System.out.println(message);
sb.append("field: " + field.getField());
sb.append("message: " + message);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString());
}
// logic
System.out.println(user);
return ResponseEntity.ok(user);
}
}
AssertTrue/False
와 같은 method 지정을 통해서 Custom Logic 적용 가능
-> 이것은 재사용이 불가함
!!!!!
ConstraintValidator
를 적용하여 재사용이 가능한 Custom Logic 적용 가능
커스텀 애너테이션 생성
package com.example.validation.annotation;
import com.example.validation.validator.YearMonthValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@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";
}
커스텀 어노테이션 검증 하기 위한 Validator
package com.example.validation.validator;
import com.example.validation.annotation.YearMonth;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
// 커스텀한 어노테이션 활용하는 클래스 만들기 위해 벨리데이터 생성하였음
// 첫인자 : 어노테이션 두번째 인자 : 어노테이션에 들어가는 값 지정
public class YearMonthValidator implements ConstraintValidator<YearMonth, String> {
private String pattern;
@Override
public void initialize(YearMonth constraintAnnotation) {
this.pattern = constraintAnnotation.pattern();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
System.out.println("assert true");
//yyyyMM01 디폴트 값을 붙여서 검색하는 것 LocalDate 활용 위해
// 어노테이션에 지정된 패턴대로 값이 잘 들어가 있는 지 확인 하기 위해 this.pattern
// 사용자가 입력을 yyyyMM 형태로 하더라도 처리 로직에 01을 붙여 처리하였음
try{
LocalDate localDate = LocalDate.parse(value + "01", DateTimeFormatter.ofPattern(this.pattern));
}catch(Exception e){
return false;
}
return true;
}
}
api controller
package com.example.validation.controller;
import com.example.validation.dto.User;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
@RequestMapping("/api")
public class ApiController {
@PostMapping("/user")
// @Valid 가 붙은 객체는 어노테이션을 통해 검증을 진행하게 된다
// 예외처리 방법 말고 예외값을 다루고 싶다면 BindingResult 활용하게 됨
// Validation에 대한 결과가 bindingResult에 들어오게 됨
public ResponseEntity user(@Valid @RequestBody User user, BindingResult bindingResult){
if(bindingResult.hasErrors()){
StringBuilder sb = new StringBuilder();
bindingResult.getAllErrors().forEach(objectError -> {
FieldError field = (FieldError) objectError;
String message = objectError.getDefaultMessage();
System.out.println("field " + field.getField());
System.out.println(message);
sb.append("field: " + field.getField());
sb.append("message: " + message);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString());
}
// logic
System.out.println(user);
return ResponseEntity.ok(user);
}
}
car_list의 car 객체
package com.example.validation.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotBlank;
import java.util.List;
public class Car {
@NotBlank
private String name;
@NotBlank
@JsonProperty("car_number")
private String carNumber;
@NotBlank
@JsonProperty("TYPE")
private String type;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCarNumber() {
return carNumber;
}
public void setCarNumber(String carNumber) {
this.carNumber = carNumber;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public String toString() {
return "Car{" +
"name='" + name + '\'' +
", carNumber='" + carNumber + '\'' +
", type='" + type + '\'' +
'}';
}
}
user 객체
package com.example.validation.dto;
import com.example.validation.annotation.YearMonth;
import javax.validation.Valid;
import javax.validation.constraints.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
public class User {
@NotBlank
private String name;
@Max(value = 90)
private int age;
// @Email
// private String email;
//
// // 정규식 활용 : 필요한거 왠만하면 찾아쓰자!, 원하는 형태로 메세지 띄우고자한다면 인자로 message
// @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message =
// "핸드폰 번호의 양식과 일치하지 않습니다. 01x-xxx-xxxx" )
// private String phoneNumber;
//
// @YearMonth //(pattern = "yyyyMM")
// private String reqYearMonth; // yyyyMM
@Valid // 이거 꼭 붙여줘야 -> Car 요소들의 NotBlank가 적용이 됨
private List<Car> cars;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
// public String getEmail() {
// return email;
// }
//
// public void setEmail(String email) {
// this.email = email;
// }
//
// public String getPhoneNumber() {
// return phoneNumber;
// }
//
// public void setPhoneNumber(String phoneNumber) {
// this.phoneNumber = phoneNumber;
// }
//
// public String getReqYearMonth() {
// return reqYearMonth;
// }
//
// public void setReqYearMonth(String reqYearMonth) {
// this.reqYearMonth = reqYearMonth;
// }
// return 이 true일때만 정상, boolean 메서드에 대해선 is라는 메서드명이 붙어야함!!!!!!
// User 안에 작성하였기에 재사용이 불가능함!!!(dto.user) -> 어노테이션을 만드는 것이 바람직
// @AssertTrue(message = "yyyymmdd 형태가 아닙니다.")
// public boolean isreqYearMonthValidation(){
// // 파싱이 안되면 false, LocalDate는 기본적으로 dd 까지 들어감
// try{
// LocalDate localDate = LocalDate.parse(getReqYearMonth() +"01", DateTimeFormatter.ofPattern("yyyyMMdd"));
// }catch(Exception e){
// return false;
// }
// return true;
// }
public List<Car> getCars() {
return cars;
}
public void setCars(List<Car> cars) {
this.cars = cars;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", cars=" + cars +
'}';
}
}