Spring Validation을 이용해서 요청 검증처리
web/Spring

Spring Validation을 이용해서 요청 검증처리

반응형

대게 개발을 진행할 때 front에서 validate를 체크하고 민감한 정보에 대해서는 한번더 체크를 진행하고 작업을 했었다. 

하지만 클라이언트에서만 validation을 체크하게 되는 경우 브라우저에서 악의적인 행동에 대해서 대응하기 어려워질수 있기 때문에 백엔드에서도 Validation을 처리해야한다. 

여기서 사용되는 @valid 어노테이션들을 알아보자.

1. DTO validation 선언
우선적으로 DTO에 각 속성에 필요한 @valid 옵션들을 추가한다.

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
package com.wedul.springboottest.rest.dto;
 
import lombok.*;
 
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
 
/**
 * 사용자 클래스.
 * User: wedul
 * Date: 2018-08-05
 * Time: 오후 9:47
 */
@Data
@AllArgsConstructor
@EqualsAndHashCode
@NoArgsConstructor
public class UserDto {
    @NotBlank(message = "이름을 입력해주세요.")
    private String name;
 
    private int age;
 
    @NotBlank(message = "이메일을 입력해주세요.")
    @Email(message = "이메일을 양식을 지켜주세요.")
    private String email;
 
    public void updateInfo(UserDto user) {
        this.age = user.getAge();
        this.email = user.getEmail();
    }
}
cs


@Valid 데이터 종류

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@AssertFalse : false 값만 통과 가능
@AssertTrue : true 값만 통과 가능
@DecimalMax(value=) : 지정된 값 이하의 실수만 통과 가능
@DecimalMin(value=) : 지정된 값 이상의 실수만 통과 가능
@Digits(integer=,fraction=) : 대상 수가 지정된 정수와 소수 자리수보다 적을 경우 통과 가능
@Future : 대상 날짜가 현재보다 미래일 경우만 통과 가능
@Past : 대상 날짜가 현재보다 과거일 경우만 통과 가능
@Max(value) : 지정된 값보다 아래일 경우만 통과 가능
@Min(value) : 지정된 값보다 이상일 경우만 통과 가능
@NotNull : null 값이 아닐 경우만 통과 가능
@Null : null일 겨우만 통과 가능
@Pattern(regex=, flag=) : 해당 정규식을 만족할 경우만 통과 가능
@Size(min=, max=) : 문자열 또는 배열이 지정된 값 사이일 경우 통과 가능
@Valid : 대상 객체의 확인 조건을 만족할 경우 통과 가능
 
출처: http://goldenraccoon.tistory.com/entry/Valid-annotation-종류 [황금너구리 블로그]
cs


2. @Request에서 전달받는 파라미터 설정
간단하게 @Valid 설정을 통해 전달받을 객체에 대한 유효성 체크를 설정할 수 있다.

1
2
3
4
5
6
7
8
9
10
 /**
   * Edit user info response entity.
   *
   * @param user the user
   * @return the response entity
 */
 @PutMapping(value = "/user/info")
 public ResponseEntity<?> editUserInfo(@RequestBody @Valid UserDto user) {
     return ResponseEntity.ok(userService.updateUser(user));
 }
cs

3. 테스트
위의 설정한 validation 설정을 위반하여 request를 보내보자

요청

응답

validation 위반 시 400에러와 함께 오류 field와 기존에 설정한 defaultMessage를 확인할 수 있다.


4. 추가적인 action에 대한 validation 설정
단순하게 regex와 null, email 등등에 대한 validation 체크 이외에 중복 체크등을 진행하기 위해서는 별도의 작업이 필요하다.

4-1)  기존의 Validation과 동일한 방식으로 표현하기 위해서 별도의 RuntimeException 클래스를 만든다.

이 예외 클래스는 Response Stauts는 400으로 전달될 수 있게 @ResponseStatus(HttpStatus.BAD_REQUEST)로 설정하고 예외가 발생한 field와 defaultMessage를 출력해주기 위해 별도의 속성을 만들어준다.

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.wedul.springboottest.rest.exception;
 
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
 
import java.util.List;
 
/**
 * Created by Leo.
 * User: wedul
 * Date: 2018-08-09
 * Time: 오후 9:06
 */
// 기존에 validation의 에러와 동일하게 400 에러가 발생하도록 추가
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class ValidationException extends RuntimeException {
 
    private Error[] errors;
 
    public ValidationException(String defaultMessage, String field){
        this.errors = new Error[]{new Error(defaultMessage, field)};
    }
 
    public ValidationException(Error[] errors) {
        this.errors = errors;
    }
 
    public Error[] getErrors() {
        return errors;
    }
 
    public static class Error {
 
        private String defaultMessage;
        private String field;
 
        public Error(String defaultMessage, String field) {
            this.defaultMessage = defaultMessage;
            this.field = field;
        }
 
        public String getDefaultMessage() {
            return defaultMessage;
        }
 
        public String getField() {
            return field;
        }
    }
 
 
}
cs


이 생성된 ValidationException 클래스를 ErrorAttributes 인터페이스를 재 정의하여 Validation 오류가 발생하였을 때 별도의 작업을 할 수 있도록 처리해준다. (이 Bean 객체는 Application에 정의해준다.)


※ ErrorAttributes는 기본적으로 스프링 부트에서 에러 처리하는 BasicErrorController인데 이 인터페이스는 DefaultErrorAttributes 클래스를 참조하여 커스터마이징이 가능하다.
참고
https://brunch.co.kr/@sbcoba/9


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
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;
 
 
 
@SpringBootApplication
public class SpringboottestApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(SpringboottestApplication.class, args);
    }
 
    @Bean
    public ErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes() {
 
        @Override
        public Map<String, Object> getErrorAttributes(WebRequest webRequest,
                                                          boolean includeStackTrace) {
            Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
            Throwable error = getError(webRequest);
 
            // validatijon Exception에 경우 별도의 처리를 진행한 에러 데이터 추가
            if (error instanceof ValidationException) {
                errorAttributes.put("errors", ((ValidationException)error).getErrors());
            }
            return errorAttributes;
        }
    };
}
cs


그리고 서비스 코드 영역에서 중복, 금지 등등을 확인하고  ValidationException을 throw하면 front에 동일한 형태에 에러코드를 전송해 줄 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 서비스 코드
 
@Override
public ResultDto insertUser(UserDto user) {
    if (null == users) {
       return ResultDto.fail("UserList Empty");
    }
 
    if (null == user) {
        return ResultDto.fail("Request User Is Null");
    } else {
        if (users.contains(user)) {
            // 중복된 부분에 대해 ValidationException
            throw new ValidationException("name""이름이 중복됩니다.");
        }
        users.add(user);
        return ResultDto.success();
    }
}
cs

테스트 해보면 다음과 같이 중복코드에 대해서 체크가 정상적으로 되는 것을 알 수 있다.

물론 이렇게 RuntimeException을 만들지 않고 팩토리 객체를 만들어서 동일하게 실패 메시지를 만들어서 줘도 상관없다.

하지만 예측이 어려운 Runtime Exception을 잘 정의해놓으면 편하게 처리할 수 있다. 




반응형