Home Validation
Post
Cancel

Validation

Spring Boot validation

  • Java에서는 null값에 대해서 접근 하려고 할 때 null pointer exception이 발생 함으로, 이러한 부분을 방지 하기 위해 미리 검증을 해야 하는데 이러한 과정을 Validation이라고 한다.

단순한 검증

1
2
3
4
5
6
7
8
9
public void run(String account, String pw, int age){
    if(account == null || pw == null){
        return ...
    }
    if(age == 0){
        return ..
    }
    proceed
}
  • 이러한 검증은 불필요하게 반복되는 코드가 생기게 된다.
  • 구현에 따라서 달라 질 수 있지만 Service Logic과의 분리가 필요하다.
  • 또한 흩어져 있는 경우 어디에서 검증을 하는지 알기 어려우며, 재사용의 한계가 있다.
  • 구현에 따라 달라질 수 있지만 검증 Logic이 변경 되는 경우 테스트 코드 등 참조하는 클래스에서 logic이 변경되어야 하는 부분이 발생 할 수 있다.

실습1(Annotation 사용)

  • dependency추가
1
implementation 'org.springframework.boot:spring-boot-starter-validation'
  • Email 검증
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.validation.dto;

import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import javax.validation.constraints.Email;

@Getter
@Setter
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
public class User {

    private String name;

    @Max(value=90)
    private int age;

    @Email
    private String email;
    private String phoneNumber;

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", email='" + email + '\'' +
                ", phoneNumber='" + phoneNumber + '\'' +
                '}';
    }
}
  • User dto의 Email에 @Email Annotation을 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.validation.controller;

import com.example.validation.dto.User;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequestMapping("/api")
public class ApiController {

    @PostMapping("/user")
    public User user(@Valid  @RequestBody User user){
        System.out.println(user);
        return user;
    }
}
  • controller에서는 기존 Post와 똑같이 작성하면 된다. 단, Validation할 항목이 있는 경우 @Valid Annotation을 작성해 줘야한다. spring이 해당 Annotation이 작성 되어 있는 경우 @Email과 같은 Validation용 Annotation을 찾아 검증을 수행한다.

실습2(정규식)

  • phoneNumber에 대한 검증
1
2
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$")
private String phoneNumber;
  • message parameter를 추가해 Error msg를 설정할 수 있다.
1
2
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "핸드폰 번호의 양식과 맞지 않습니다. xxx-xxx(x)-xxxx")
private String phoneNumber;

Validation 실패 후 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/api")
public class ApiController {

    @PostMapping("/user")
    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();
                sb.append("field : " + field.getField());
                sb.append("message : " + message);
            });
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString());
        }
        return ResponseEntity.ok(user);
    }
  • BindingResult를 사용해 Validation 결과를 알 수 있다.

![regex]](/assets/img/post/2021-06-28/regex.png)

실습3(Custom)

  • AssertTure, AssertFalse와 같은 method 지정을 통해 Custom Logic적용이 가능하다.
  • ConstraintValidator 를 적용해 재사용 가능한 Custom Logic적용이 가능하다.
  • Custom validation이 필요한 경우의 예로는 날짜 형식을 String yyyyMM과 같은 형식을 받을 때 Spring의 validation annotation으로는 검증할 수 없기 때문에 Custom이 필요하다.

메서드 지정 Validation

  • yyyyMM형식의 날짜를 받는다고 생각
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
public class User {

    @Size(min = 6, max = 6)
    private String reqYearMonth; // yyyyMM

    @AssertTrue(message = "yyyyMM 의 형식에 맞지 않습니다.")
    public boolean isReqYearMonthValidation(){
        try{
            LocalDate localDate = LocalDate.parse(getReqYearMonth() + "01", DateTimeFormatter.ofPattern("yyyyMMdd"));
        }catch(Exception e){
            return false;
        }
        return true;
    }

    public String getReqYearMonth() {
        return reqYearMonth;
    }

    public void setReqYearMonth(String reqYearMonth) {
        this.reqYearMonth = reqYearMonth;
    }
}
  • AssertTrue를 사용하기 위해선 boolean반환에 메서드 이름은 반드시 is변수명 이어야 한다.
  • yyyyMM형식을 검증 해야 하기 때문에 localDate로 dummy date인 01을 추가해 파싱을 시도하고 예외가 발생되면 False, 제대로 파싱되면 True를 반환하게 하면 된다.
  • 하지만 위와 같이 Validation을 진행하게 되면 User Dto에 직접 작성을 했기 때문에 재사용이 불가능하다. 재사용이 가능한 Validation을 하기 위해 직접 Annotation을 작성해 주면된다.

Annotation 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface Email {

	String message() default "{javax.validation.constraints.Email.message}";

	Class<?>[] groups() default { };

	Class<? extends Payload>[] payload() default { };

	/**
	 * @return an additional regular expression the annotated element must match. The default
	 * is any string ('.*')
	 */
	String regexp() default ".*";

	/**
	 * @return used in combination with {@link #regexp()} in order to specify a regular
	 * expression option
	 */
	Pattern.Flag[] flags() default { };

	/**
	 * Defines several {@code @Email} constraints on the same element.
	 *
	 * @see Email
	 */
	@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
	@Retention(RUNTIME)
	@Documented
	public @interface List {
		Email[] value();
	}
}
  • 위의 코드는 Email Annotation코드이다. 이를 참고하여 Annotation을 작성하면된다.
  • YearMonth
1
2
3
4
5
6
7
8
9
10
11
12
13
@Constraint(validatedBy = {YearMonthValidator.class})  // 검증을 수행할 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";      //외부에서는 yyyyMM이 들어오지만 내부에서 dummy값을 붙혀 yyyyMMdd형식으로 만들어 검증하기 때문
}
  • YearMonthValidator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//ConstraintValidator <Annotation, ValueType>
public class YearMonthValidator implements ConstraintValidator<YearMonth, String> {

    private String pattern;


    @Override
    public void initialize(YearMonth constraintAnnotation) {
		//초기화 때 YearMonth의 pattern을 가져옴
        this.pattern = constraintAnnotation.pattern();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
		//검증 수행은 이전 AssertTrue와 마찬가지로 진행
        try{
            LocalDate localDate = LocalDate.parse(value + "01", DateTimeFormatter.ofPattern(this.pattern));
        }catch(Exception e){
            return false;
        }
        return true;
    }
}

추가

  • User가 CarList를 가지고 있는 model을 생각해보자
  • 이때 User객체 RequestBody에 Valid Annotation 을 작성해 준다고 해서 User가 가지고 있는 Car 객체 대해서도 Validation을 수행하는 것은 아니다. 항상 Valid를 명시해 주어야 하며 이럴 경우 아래와 같이 코드를 작성해 주어야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class User{
    @NotBlank
    private String name;

    @Max(value = 90)
    private int age;

    @Valid
    private List<Car> cars;
...
}

public class Car{
    @NotBlank
    private String name;

    @NotBlank
    private String carNumber;
}
This post is licensed under CC BY 4.0 by the author.