Spring Boot Validation

이종찬·2023년 2월 12일
0

📖 Validation?

null 혹은 데이터에 맞지 않는 자료형은 예외가 발생됩니다. 이러한 부분을 방지 하기 위해 미리 검증을 하는 과정입니다. 스프링 부트는 사용자 입력의 유효성을 검사하는 방법을 어노테이션 기반으로 제공합니다.

유효성 검사 제약 조건은 모델 클래스의 어노테이션을 사용하여 정의합니다. 이용자가 데이터를 제출할 때 프레임워크에 의해 자동으로 적용됩니다.

컨트롤러에서는 @Valid 어노테이션을 사용하여 유효성 검사를 트리거할 수 있습니다. BindingResult로 유효성 검사 결과가 전달되며 오류 발생 여부 및 오류가 난 개체를 확인할 수 있습니다.

Validation내용
@Size문자 길이 측정, Int Type (x)
@NotNullnull (x)
@NotEmptynull,"" (x)
@NotBlanknull, "", " ", (x)
@Past과거 날짜
@PastOrPresent오늘 또는 과거 날짜
@Future미래 날짜
@FutureOrPresent오늘 또는 미래 날짜
@Pattern정규식 허용
@Max최대값
@Min최소값
@Valid해당 Object validation 실행
@AssertTrue, @AssertFasle별도의 로직 실행

🤔 사용해야 하는 이유는?

1. 검증해야 할 값이 많은 경우 코드의 길이가 길어집니다.

2. 구현에 따라 다를수 있지만 서비스 로직과 분리가 필요합니다.

3. 각기 다른 위치에 있는 경우 검증하는 것이 어려우며, 재사용에 한계가 있습니다.

4. 로직이 변경 되는 경우 참조하는 클래스가 변경되어야 하는 영향을 끼칠수도 있습니다.

Validation을 사용하면 사용자의 입력을 쉽게 유효성 검사할 수 있습니다. 애플리케이션은 유효한 데이터만 처리할 수 있고 이는 품질과 안정성을 높일 수 있습니다.


👨‍💻 구현

우선 코드 작성을 하기 전에 gradle에서 implementation 'org.springframework.boot:spring-boot-starter-validation' 추가해줍니다.

User

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class User {
    private String name;
    private int age;
    @Email
    private String email;
    @Pattern(regexp = "\\d{3}-\\d{4}-\\d{4}", message = "핸드폰 번호가 아닙니다.")
    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 + '\'' +
                '}';
    }
}

@Email,@Pattern을 사용하여 해당 변수에 대한 유효성 검사를 어노테이션으로 할 수 있습니다. 정규식은@Pattern 같은 경우에만 사용하지만 message는 작성 가능합니다.

Controller

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

    @PostMapping("/user")
    public Object user(@Valid @RequestBody User user, BindingResult result) {
        if (result.hasErrors()) {
            StringBuilder sb = new StringBuilder();
            result.getAllErrors().forEach(error -> {
                FieldError field = (FieldError) error;
                String msg = error.getDefaultMessage();
                System.err.println("field : " + field.getField() + ", msg : " + msg);

                sb.append("field : ").append(field.getField())
                        .append(", message : ").append(msg);
            });

            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString());
        }
        return user;
    }
}

앞서 모델 클레스에서 어노테이션 추가함으로 유효성 검사 기능을 추가하였습니다. Controller에서 @Valid를 사용하므로 모델 클래스에서 유효성 검사 어노테이션이 들어갔다는 것을 알려줍니다.

BindingResult는 입력받은 값에 대한 결과를 알려줍니다. 위의 코드에서는 bingingResult 값이 이상이 있으면 StringBuilder에 추가하여 리턴하는 로직입니다.

입력

{
  "name" : "Chan",
  "age" : 27,
  "email" : "ieejo716@naver.com",
  "phone_number" : "010-11112222"
}

실행결과

field : phoneNumber, msg : 핸드폰 번호가 아닙니다.


CustomValidation

어노테이션에 있지만 부가적으로 더 검사를 할 수 있는 방법이 있습니다. @AssertTrue, @AssertFalse를 활용을 하여 추가적으로 조건을 추가할 수 있습니다.

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

해당 변수는 6자리로 yyyyMM형식의 데이터를 받을 예정입니다. 올바른 값도 받을 수 있지만 String이기 때문에 12345a가 들어와도 에러가 나지 않습니다. 부가적인 조건이 필요하며 구현은 다음과 같습니다.

    @AssertTrue(message = "yyyyMM형식이 아닙니다.")
    public boolean isCheckReqYearMonth() {
        String text = this.reqYearMonth + "01";
        try {
            LocalDate localDate = LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyyMMdd"));
        } catch (Exception e) {
            return false;
        }
        return true;
    }

@AssertTrue는 true값을 반환하지 않으면 에러로 간주합니다. LocalDate 타입을 선언하여 입력값을 파싱합니다. 해당 과정에서 예외가된다면 제대로된 데이터가 아님을 간주하고 false를 반환합니다. 아까와 같은 12345a가 들어가게 되면 false를 반환하여 올바른 값을 검사할 수 있습니다.

하지만 부가적인 조건이 필요할 때 마다 로직을 구현해야 합니다. 재사용도 안되고 어노테이션을 사용하여 유효성 검사를 하는 이유가 떨어지게 됩니다.

이러한 단점을 보완하려면 어노테이션을 만들어서 사용하면 됩니다.

YearMonth

@Constraint(validatedBy = {YearMonthValidator.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
public @interface YearMonth {
    String message() default "yyyyMM 형식이 아닙니다.";
    String pattern() default "yyyyMMdd";
}

@constraint에 해당 어노테이션에 대한 기능 구현할 클래스를 넣어줍니다.
@Target에 대상을 지정합니다. -> 어노테이션 클래스 들어가면 보통 이렇게 되어있습니다.
@Retention을 사용하여 런타임까지 남아있는다고 선언해줍니다.

YearMonthValidator

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) {
        //yyyyMM
        String text = value + "01";
        try {
            LocalDate.parse(text, DateTimeFormatter.ofPattern(this.pattern));
        } catch (Exception e) {
            return false;
        }

        return true;
    }
}

이전에 구현한 로직을 따로 분리하였습니다. ConstrantValidator를 구현해줍니다. 제네릭 안에는 아까 만든 어노테이션과 유효성 검사를 할 자료형을 넣어줍니다.

initialize메서드에 초기화를 해주고, isValid에 검사할 로직을 넣어줍니다.

이렇게 하면 이전과 로직은 동일하지만 어노테이션으로 만듬으로써 재사용이 가능합니다.

✅ 요약

  • 어노테이션으로 타입의 유효성 검사가 가능합니다.
  • 모델 클래스에 유효성 검사를 한다면 외부에서 호출할 때 @Valid를 붙어야합니다.
  • 부가적으로 더 조건을 걸고 싶은 경우 @AssertTrue,@AssertFalse를 통해 커스텀 로직 적용이 가능합니다.
  • 커스텀 로직이 재사용이 필요하다면 어노테이션을 만들어 커스텀로직 적용이 가능합니다.
profile
왜? 라는 질문이 사라질 때까지

0개의 댓글