Spring

[Spring] 유효성 검사

dalooong 2023. 7. 9. 14:01

유효성 검사란?

사용자가 입력한 데이터가 허용하는 형태인지 검사하는 과정

예) 이름을 입력하는 공간에 숫자를 입력하거나 등 원하는 데이터가 맞는지

spring-boot-starter-validation

jakarta Bean Vaolidation

  • 유효성 검증을 위한 기술 명세
  • 어떤 항목이 어떤 규칙을 지켜야 하는지 표시하는 기준

Hibernate Vailidation

  • jakarta beanb validation을 토대로 실제로 검증해주는 프레임 워크

둘의 관계는 JPA와 Hibernate ORM과 유사하다. ⇒ Spring data JPA jpa repository

 

Jakarta Bean Validation specification

validation 참고 사이트

💻 실습

 

📌 목표 : usercontrolloer 가 userdto의 요구사항을 유효했는지 확인하는게 목표

  • spring project 생성

  • dto → userdto 생성
@Data
public class UserDto {
    private Long id;

    @NotBlank //비어있지 않다.
    private String username;
    private String email;
    private String phone;
}

  • controller→ usercontroller 생성
@Slf4j
@RestController
public class UserController {
    @PostMapping("/users")
    public ResponseEntity<Map<String, String>> addUser(
            //UserDto가 우리가 정의한 요구사항
            //지키고 있는지 유효성 검사
            @RequestBody UserDto dto
    ){
        log.info(dto.toString());
        Map<String, String> responseBody = new HashMap<>();
        responseBody.put("message", "success!");
        return ResponseEntity.ok(responseBody);
    }
}

실행

  • postman 실행 후 검사
  •  

 유효성 검사

@NotBlank : 빈칸이면 안된다.

@Email : 이메일 형식으로 입력해야한다.

@NotNull : 널 값이면 안된다.

 

추가

  • userdto
@Data
public class UserDto {
    //요구사항들
    private Long id;

    @NotBlank //비어있지 않다.
    private String username;
    @Email //형식이 이메일이어야한다.
    private String email;
    @NotNull
    private String phone;
}
  • controller
@Slf4j
@RestController
public class UserController {
    @PostMapping("/users")
    public ResponseEntity<Map<String, String>> addUser(
            //UserDto가 우리가 정의한 요구사항
            //지키고 있는지 유효성 검사
            @**Valid** @RequestBody UserDto dto
    ){
        log.info(dto.toString());
        Map<String, String> responseBody = new HashMap<>();
        responseBody.put("message", "success!");
        return ResponseEntity.ok(responseBody);
    }
}
  • 추가 한 뒤에 빌드하면 userdto에서 있던 요구사항들이 빠지면 400 badrequest가 발생한다.

400에러
@NotBlank 추가 하게 되면 공백을 입력해도 공백일수 없다는 에러가 뜨게 된다.
@Email 형식이 아닐때 에러가 나온다.
@NotNull null값일 경우 나오는 에러

  • @Min() 사용 : () 괄호 안에 숫자가 최소 몇글자인지
@Min(14) //나이최소 14
    private Integer age;

  • @Future : 현재 시점보다 미래시간입력해야한다.
@Future
    private LocalDate validUntil;

현재시점 이후 미래시간이여야한다.
미래는 성공

  • @NotNull, @NotEmpty, @NotBlank
		@NotNull //notNullString이 널이 아닌지만 검증
    private String notNullString;

    @NotEmpty //notEmptyString의 길이가 0이 아닌지만 검증
    private String notEmptyString;

    @NotBlank //notBlankString이 공백 문자로만 이루어지지 않았는지 검증
    private String notBlankString;
}

notNullString은 널이면 안된다.
notEmptyString은 비어있으면 안된다.
notBlankString은 공백이면 안된다.

검증 실패시 응답 @ExceptionHandler 사용

예외 처리를 수동으로 해주어 검증실패시 응답하는 것이다.

유효성 검사 실패는 사용자 오류임으로 응답코드를 400으로 설정해준다.

@ExceptionHandler(MethodArgumentNotValidException.class) // 검증 실패시 응답
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String,String> handelValidationException(
            MethodArgumentNotValidException exception
    ){
        Map<String, String> errors = new HashMap<>();
        for (FieldError error :
                exception.getBindingResult().getFieldErrors()) {
            errors.put(error.getField(), error.getDefaultMessage());
        }
        return errors;
    }
  • userdto
@Data
public class UserDto {
    //요구사항들
    private Long id;

    @NotBlank //비어있지 않다.
    @Size(
            min = 8,
            message = "최소 8글자이어야 합니다.")
    private String username;

    @Email //형식이 이메일이어야한다.
    private String email;

    @NotNull
    private String phone;

    @Min(14)
    @Min(
            value = 14,
            message = "14세 미만은 부모님의 동의가 필요합니다.")
    //나이최소 14
    private Integer age;

    @Future(message = "미래의 시간까지 유효해야합니다.")
    private LocalDate validUntil;

    @NotNull //notNullString이 널이 아닌지만 검증
    private String notNullString;

    @NotEmpty //notEmptyString의 길이가 0이 아닌지만 검증
    private String notEmptyString;

    @NotBlank //notBlankString이 공백 문자로만 이루어지지 않았는지 검증
    private String notBlankString;
}
  • 결과

 

사용자 지정 유효성 검사

이미 제공되는 어노테이션만으로 충분하지만, 상황에 따라 그것으로 부족한 경우도 존재한다.

이런 경우에는 직접 사용자가 어노테이션을 만들고, 그 어노테이션이 적용된 필드를 검사하는 방법을 사용하여 사용자가 직접 유효성 검증 방법을 만들 수도 있다.

먼저 어노테이션을 생성한다.

 

조건

  1. email 형식이어야한다.
  2. 이메일이 네이버와 지메일이여야한다.
  • constraints 패키지 생성
  • annotations 패키지 생성 → EmailWhiteList 어노테이션 생성
 @Target(ElementType.FIELD) //어노테이션을 어디에 적용할 것인지 (선택)
@Retention(RetentionPolicy.RUNTIME)
public @interface EmailWhiteList {
}
  • EmailWhiteListValidator = 데이터 유효성 검사기 클래스
public class EmailWhiteListValidator
        //사용자 지정 유효성 검사를 위해 구현해야 하는 인터페이스
        implements ConstraintValidator<EmailWhiteList, String> {
    private final Set<String> whiteList;
    
    public EmailWhiteListValidator(){
        this.whiteList = new HashSet<>();
        this.whiteList.add("gmail.com");
        this.whiteList.add("naver.com");
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context){
        //유효한 값일 때 true 반환
        //유효하지 않은 값을 때 false 반환
        String[] split = value.split("@");
        String domain = split[split.length -1];
        //set whitelist에 domain이 추가되어 있는지 
        return whiteList.contains(domain);
    }
}
  • userdto
@Email //형식이 이메일이어야한다.
    // 이메일이 지정된 도메인 (gmail.com 등) 이도록
    // 검증하는 어노테이션을 만들어봅시다.
    @EmailWhiteList
    private String email;

결과

이메일이 네이버와 지메일이 아닐 경우 실패
네이버와 지메일인 경우 성공

 

전화번호의 조건 넣기 (010-시작 & (010) 시작)

  • Phone010 어노테이션
//@Phone010가 붙은 필드는
//유효성 검사시
// (010) 또는 010-으로 시작해야한다.
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = Phone010Validator.class)

public @interface Phone010 {
    //Annotaion Element
    String message() default "010으로 시작하지 않음";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
  • Phone010Validator 클래스
public class Phone010Validator
        implements ConstraintValidator<Phone010, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        //010-
        boolean withDash = value.startsWith("010-");
        //(010)
        boolean withPar = value.startsWith("(010)");
        return withDash || withPar;
    }
}
  • userdto
@NotNull //null이 아니다.
    @Phone010 //010으로 시작하는 번호 형식인지
    private String phone;
  • 결과

010-으로 시작하지 않을시 오류
010- 성공
(010)성공

blacklist

  • userdto추가
// UserDto 어노테이션 추가
    @NotBlank @Size(min = 8, message = "최소 8글자 이상이여야 합니다.")
    @Blacklist(blacklist = {"blacklist"})
    private String username;
  • blacklist 인터페이스 생성
// Blacklist(@interface)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = BlacklistValidator.class)
public @interface Blacklist {
    String message() default "username in blacklist";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    String[] blacklist() default {};
}
  • blacklistValidator 생성
// BlacklistValidator
public class BlacklistValidator implements ConstraintValidator<Blacklist, String> {
    private Set<String> blacklist;
    @Override
    public void initialize(Blacklist constraintAnnotation) {
        blacklist = new HashSet<>();
        for (String target: constraintAnnotation.blacklist()) {
            blacklist.add(target);
        }
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // this.blacklist 안에 value가 있으면 실패
        return !this.blacklist.contains(value);
    }
}
  • 결과