6 분 소요


Spring Validation이란?

validation이란 프로그래밍에 있어서 가장 필요한 부분이다.

Java/Kotlin 에서는 null값에 대해 접근하려고 할 때, null point exception이 발생한다. 이런 부분을 방지하기 위해 미리 검증하는 과정을 Validation이라고 함

validation

  • 검증할 값이 많을경우 코드가 복잡
  • validation은 재사용성이 높아야하고, Service Logic과의 분리가 필요
  • Logic이 변경되어야 할 때에 Validation이 같이 들어가면 매우 난잡해진다

validaton을 할 수 있는 Annotation

Size 문자길이측정 Future 미래날짜
NotNull null 불가 FutureOrPresent 오늘이거나 미래
NotEmpty null, ‘’ 불가 Pattern 정규식
NotBlank null,’’, ‘ ‘ 불가 Max 최대값
Past 과거 날짜 Min 최소값
PastOrPresent 오늘이나 과거날짜 Valid object validation

우선 validation을 적용 할 곳을 고민해보자. 나는 현재 시작하는 프로젝트에 간단하게 적용 해 보려고한다. 회원가입하는 곳에서 사용자 정보를 가지고왔을 때 체크 해주는 부분을 작성해보고자한다

데이터를 받아 올 DTO

  • getter와 setter, toString은 너무 길어 생략했다

    package com.wool.modulink.dto.user;
    
    public class User {
    
        private String name;
        private String password;
        private String email;
        private String phone;
        private int age;
        // getter, setter, toString
    }
    

컨트롤러

package com.wool.modulink.controller.user;

import com.wool.modulink.dto.user.User;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class AuthController {

    @PostMapping("/user")
    public User user(@RequestBody User user) {
        System.out.println(user);
        return user;
    }
}

localhost:8080/auth/user 로 POST 전송하기

{
    "name":"test",
    "password":"1234",
    "email":"paullee@mail.com",
    "phone":"01012341234",
    "age":1000
}
  • 위의 데이터는 언뜻봐서는 괜찮은 데이터같다.
  • email, phone 과 같은 경우는 쓰는 사람들마다 형식이 다를 수 도 있다
  • age같은 경우는.. 아직까지 1000년정도 살아있는 사람은… 보지못했기때문에…! 나이도 입력제한을 걸어주어야겠다
  • 포스팅의 목적에 맞게 Service Logic에서가 아닌, Spring Validation을 사용 해보자

validation 적용하기

기존의 방식

기존에는 아래와같이 if문을 사용해서 컨트롤러 내부나 서비스로직 내부에서 검사를했다

package com.wool.modulink.controller.user;

import com.wool.modulink.dto.user.User;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class AuthController {

    @PostMapping("/user")
    public ResponseEntity user(@RequestBody User user) {
        System.out.println(user);

        if(user.getAge() > 200){
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(user);
        }
        if(user.getEmail().contains("@")){
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(user);
        }

        return ResponseEntity.ok(user);
    }
}

이제 위의 부분을 좀 더 Spring이 제공하는 방법들로 이쁘고 간편하게 바꿔보려고한다

기본적으로 제공되는 Validation들이 적용되기 위해서는 Valid 하고자하는 곳의 데이터에 어노테이션이 적용되어야한다.

기본적인 컨트롤러에 우선 Valid를 붙이고 다음으로 넘어가자

Email Validation

이메일은 DTO에서 이메일을 담는 변수에 @Email 어노테이션을 사용 해 주면 된다.

그리고 해당하는 어노테이션이 동작하기 위해서는, 컨트롤러의 RequestBody 앞쪽에 @Valid 어노테이션으로 “검증을 할 것이다” 라고 스프링에게 알려주어야 한다

UserDto

import javax.validation.constraints.Email;

public class User {

    private String name;
    private String password;
    @Email
    private String email;
    private String phone;
    private int age;
}

UserController

package com.wool.modulink.controller.user;

import com.wool.modulink.dto.user.User;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequestMapping("/auth")
public class AuthController {

    @PostMapping("/user")
    public ResponseEntity user(@Valid @RequestBody User user) {
        System.out.println(user);
        }
}

Phone Validation

이부분은 @Pattern 어노테이션의 regexp 옵션을 사용해서 정규식을 사용하는 모든 곳에 적용 할 수 있다

UserDto

package com.wool.modulink.dto.user;

import javax.validation.constraints.Email;
import javax.validation.constraints.Pattern;

public class User {

    private String name;

    private String password;

    @Email
    private String email;

    @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$",message = "핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx(x)-xxxx")
    private String phone;

    private int age;
}

Max/Min 값 지정하기

숫자가 들어오는 경우 나이는 1살 이상, 100살 이하로 제한을 둘 수 있다.

각각의 상황에 맞게 max/min 값을 지정 해 주어야 한다

UserDto

public class User {

    private String name;

    private String password;

    @Email
    private String email;

    @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$",message = "핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx(x)-xxxx")
    private String phone;

    @Min(value = 0,message = "적절한 연령을 입력해주세요.")
    @Max(value = 150,message = "적절한 연령을 입력해주세요.")
    private int age;
}

validation error 모아받기

validation을 한번에 모아받는 친구가 존재한다. BindingResult 라는 친구인데, validation에서 실패한 모든값을 들고있다. 반복문을 통해 error 메시지를 뽑아 줄 수 있다.

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.getField() + ": " + message);

            sb.append("field: " + field.getField());
            sb.append("message: " + message);
        });
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString());
    }
}

테스트 해보자

{
    "name":"test",
    "password":"1234",
    "email":"paulleeemail.com",
    "phone":"01012341234",
    "age":1000
}

위의 데이터를 전송했을 때, 콘솔창에 아래와 같이 나온다

phone: 핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx(x)-xxxx
age: 적절한 연령을 입력해주세요.
email: 올바른 형식의 이메일 주소여야 합니다

API return값도 잘 꾸며주도록 나중에 작업 하면 좋을 것 같다.

What is Spring Validation?

Validation is one of the most essential parts of programming.

In Java/Kotlin, when you try to access a null value, a null pointer exception occurs. The process of verifying in advance to prevent such issues is called Validation.

validation

  • Code becomes complex when there are many values to validate
  • Validation should have high reusability and needs to be separated from Service Logic
  • When Logic needs to change, having Validation mixed in makes it very messy

Annotations for Validation

Size String length Future Future date
NotNull Not null FutureOrPresent Today or future
NotEmpty Not null, ‘’ Pattern Regex
NotBlank Not null,’’, ‘ ‘ Max Maximum value
Past Past date Min Minimum value
PastOrPresent Today or past Valid Object validation

Let’s think about where to apply validation. I’m going to simply apply it to a project I’m starting. I want to write the part that checks when user information is received during registration.

DTO to Receive Data

  • getter, setter, and toString are omitted as they’re too long

    package com.wool.modulink.dto.user;
    
    public class User {
    
        private String name;
        private String password;
        private String email;
        private String phone;
        private int age;
        // getter, setter, toString
    }
    

Controller

package com.wool.modulink.controller.user;

import com.wool.modulink.dto.user.User;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class AuthController {

    @PostMapping("/user")
    public User user(@RequestBody User user) {
        System.out.println(user);
        return user;
    }
}

POST to localhost:8080/auth/user

{
    "name":"test",
    "password":"1234",
    "email":"paullee@mail.com",
    "phone":"01012341234",
    "age":1000
}
  • The data above looks fine at first glance.
  • For email and phone, formats may differ depending on users
  • For age… I haven’t seen anyone who has lived about 1000 years…! We should put input restrictions on age too
  • In line with the purpose of this post, let’s use Spring Validation instead of Service Logic

Applying validation

Traditional Method

Previously, we used if statements to check inside the controller or service logic like this:

package com.wool.modulink.controller.user;

import com.wool.modulink.dto.user.User;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class AuthController {

    @PostMapping("/user")
    public ResponseEntity user(@RequestBody User user) {
        System.out.println(user);

        if(user.getAge() > 200){
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(user);
        }
        if(user.getEmail().contains("@")){
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(user);
        }

        return ResponseEntity.ok(user);
    }
}

Now I’m going to change the above to something prettier and simpler using methods provided by Spring.

For the default Validations to be applied, annotations must be applied to the data where you want to validate.

Let’s first add Valid to the basic controller and move on.

Email Validation

For email, use the @Email annotation on the variable that holds the email in the DTO.

And for the annotation to work, you need to tell Spring “I will validate” with the @Valid annotation in front of the controller’s RequestBody.

UserDto

import javax.validation.constraints.Email;

public class User {

    private String name;
    private String password;
    @Email
    private String email;
    private String phone;
    private int age;
}

UserController

package com.wool.modulink.controller.user;

import com.wool.modulink.dto.user.User;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequestMapping("/auth")
public class AuthController {

    @PostMapping("/user")
    public ResponseEntity user(@Valid @RequestBody User user) {
        System.out.println(user);
        }
}

Phone Validation

This part can be applied anywhere using regex with the regexp option of the @Pattern annotation

UserDto

package com.wool.modulink.dto.user;

import javax.validation.constraints.Email;
import javax.validation.constraints.Pattern;

public class User {

    private String name;

    private String password;

    @Email
    private String email;

    @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$",message = "Does not match phone number format. 01x-xxx(x)-xxxx")
    private String phone;

    private int age;
}

Setting Max/Min Values

When numbers come in, you can limit age to 1 or more and 100 or less.

You need to set max/min values according to each situation.

UserDto

public class User {

    private String name;

    private String password;

    @Email
    private String email;

    @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$",message = "Does not match phone number format. 01x-xxx(x)-xxxx")
    private String phone;

    @Min(value = 0,message = "Please enter an appropriate age.")
    @Max(value = 150,message = "Please enter an appropriate age.")
    private int age;
}

Collecting Validation Errors

There’s something that collects all validations at once. It’s called BindingResult, which holds all values that failed validation. You can extract error messages through a loop.

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.getField() + ": " + message);

            sb.append("field: " + field.getField());
            sb.append("message: " + message);
        });
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString());
    }
}

Let’s Test

{
    "name":"test",
    "password":"1234",
    "email":"paulleeemail.com",
    "phone":"01012341234",
    "age":1000
}

When sending the above data, the console shows:

phone: Does not match phone number format. 01x-xxx(x)-xxxx
age: Please enter an appropriate age.
email: Must be a properly formatted email address

It would be good to work on formatting the API return value nicely later.

댓글남기기