본문 바로가기

Spring Boot

[Spring Boot] Validation 유효성 검사 클라이언트 오류 보내기

728x90

1. API 형식 정의

먼저 서버와 클라이언트 간에 주고받는 데이터 형식을 통일하기 위해 API 형식을 정의합니다. Api 클래스는 다음과 같이 구성됩니다:

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Api<T> {

    private String statusCode;

    private String message;

    private T data;

    private Error error;
    
    public static class Error {
        private List<String> errorList;
    }
}

2. Controller API 형식으로 변경

다음으로, StudentValidationController 컨트롤러에서 API 형식을 적용합니다:

import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.example.model.Api;
import org.example.model.Student;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@Slf4j
@RequestMapping("/student/info")
public class StudentValidationController {

    @PostMapping("")
    public Api<Student> studentInfo(
            @Valid
            @RequestBody
            Api<Student> student
    ) {
        log.info("info : {}", student);

        return student;
    }
}

 

 

 

3. API 형식으로 클라이언트가 서버에 보내기

클라이언트가 다음과 같은 형식으로 서버에 데이터를 보냅니다:

{
    "status_code": "",
    "message": "",
    "data": {
        "id": "",
        "password": " ",
        "email": "hong.com",
        "name": "홍길동",
        "score": 1000
    },
    "error": {
        "error_list": []
    }
}

4. 결과 화면

서버에서 반환되는 결과는 다음과 같습니다:

 

 

{
    "status_code": "",
    "message": "",
    "data": {
        "id": "",
        "password": " ",
        "email": "hong.com",
        "name": "홍길동",
        "score": 1000
    },
    "error": {
        "error_list": []
    }
}

유효성 검사가 동작하지 않는 이유

Student 모델에서 유효성 검사 어노테이션을 사용했음에도 불구하고 유효성 검사가 동작하지 않는 이유는 Api 클래스의 data 필드에 @Valid 어노테이션이 없기 때문입니다.

 

  • Api 클래스의 data 필드는 유효성 검사가 필요하지만, @Valid 어노테이션이 적용되지 않았습니다.
  • 유효성 검사는 @Valid 어노테이션이 있는 필드에 대해 적용됩니다. 따라서, data 필드에 @Valid 어노테이션을 추가해야 합니다.

 

5. @Valid 어노테이션 추가

Api 클래스의 data 필드에 @Valid 어노테이션을 추가하여 유효성 검사를 활성화합니다:

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Api<T> {

    private String statusCode;

    private String message;
    
    @Valid  // data 필드를 검사한다
    private T data;

    private Error error;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
    public static class Error {
        private List<String> errorList;
    }
}

 

400번 Bad Request가 나온 것을 확인 할 수 있습니다

 

BindingResult 설명

  • BindingResult란?
    BindingResult는 스프링 프레임워크에서 제공하는 인터페이스로, 유효성 검사 후 발생한 오류 정보를 저장하는 역할을 합니다. 컨트롤러 메서드에서 유효성 검사가 수행된 후, 유효성 검사 오류가 있으면 BindingResult 객체에 오류 정보가 저장됩니다.
  • BindingResult의 역할:
    • 유효성 검사 결과를 저장하고, 유효성 검사 실패 시 오류 정보를 제공.
    • 오류 메시지를 처리하고, 클라이언트에게 적절한 오류 메시지를 전송하는 데 사용.

 

6. 오류 메시지 전송

유효성 검사 오류가 발생했을 때 이를 처리하기 위해 BindingResult를 사용합니다:

import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.example.model.Api;
import org.example.model.Student;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@Slf4j
@RequestMapping("/student/info")
public class StudentValidationController {

    @PostMapping("")
    public Api<Student> studentInfo(
            @Valid
            @RequestBody
            Api<Student> student,

            BindingResult bindingResult  // 빈 주입
    ) {
        log.info("info : {}", student);

        return student;
    }
}

 

 

7. 오류 검사

오류가 발생했을 때 오류 메시지를 수집하는 로직을 추가합니다:

import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.example.model.Api;
import org.example.model.Student;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.stream.Collectors;


@RestController
@Slf4j
@RequestMapping("/student/info")
public class StudentValidationController {

    @PostMapping("")
    public Api<Student> studentInfo(
            @Valid
            @RequestBody
            Api<Student> student,

            BindingResult bindingResult
    ) {
        // error 를 가지고 있는지 확인한다 반환값은 true, false
        if (bindingResult.hasErrors()) {
        	// bindingResult에 존재하는 에러들을 전부 반환하여 stream map을 사용
            var errorList = bindingResult.getFieldErrors().stream()
                    .map(it -> {
                    	// errorList에 담을 값의 format
                        var format = "%s : { %s } 은 %s";
                        // getField : 오류 필드 명
                        // getRejectedValue : 삽입된 오류 값
                        // getDefaultMessage : 기본 오류 메세지
                        var errorFormat = String.format(format, it.getField(), it.getRejectedValue(), it.getDefaultMessage());
                        
                        return errorFormat;
                        // 타입을 List로 반환
                    }).collect(Collectors.toList());
        }
        
        
        log.info("info : {}", student);

        return student;
    }
}

 

8. 응답하는 변수에 값을 넣어서 반환하기

오류 메시지를 API 형식으로 반환합니다:

if (bindingResult.hasErrors()) {
        var errorList = bindingResult.getFieldErrors().stream()
                .map(it -> {
                    var format = "%s : { %s } 은 %s";
                    var errorFormat = String.format(format, it.getField(), it.getRejectedValue(), it.getDefaultMessage());

                    return errorFormat;
                }).collect(Collectors.toList());


        // api error에 담을 body 생성
        var body = Api.Error.builder()
                .errorList(errorList)
                .build();

        // 반환하기
        var response = Api.builder()
                .statusCode(String.valueOf(HttpStatus.BAD_REQUEST.value()))
                .message(HttpStatus.BAD_REQUEST.getReasonPhrase())
                .error(body)
                .build();

        return response;
    }

 

 

메서드 반환 값 오류 발생

public Api<Student> studentInfo() {...}

 

반환 값이 Api <Student>이기 때문에 반환 값을 바꾸어야합니다

public Api<? extends Object> studentInfo() {...}

 

 

  • 모든 클래스는 Object를 상속받습니다.
  • ?는 와일드카드로, 어떤 특정 타입이 올지 모른다는 의미입니다.
  • ? extends Object는 Object를 상속받는 모든 클래스 타입이 올 수 있다는 의미입니다.

자세한 설명

이 선언은 메서드의 반환 타입으로 Api 클래스의 인스턴스를 사용하지만, 그 내부 데이터 타입이 무엇이 될지는 컴파일 시점에서 알 수 없음을 나타냅니다.

  • 모든 클래스는 Object를 상속받습니다. 자바에서 최상위 클래스는 Object이므로, 모든 클래스는 Object를 상속받습니다.
  • ? 와일드카드: ?는 특정 타입을 명시하지 않고, 어떤 타입이든 가능하다는 것을 나타냅니다.
  • extends Object 제약조건: ? extends Object는 Object를 상속하는 어떤 클래스든 올 수 있다는 의미입니다. 따라서, 메서드는 Api 클래스의 인스턴스를 반환하지만, 그 데이터 타입이 무엇인지는 미리 정의되어 있지 않습니다.

이를 통해 메서드가 반환할 수 있는 타입의 유연성을 높일 수 있습니다. 다음과 같은 여러 가지 타입을 반환할 수 있습니다:

 

 

9. 최종 결과

다음은 최종적으로 클라이언트에게 반환되는 오류 메시지의 예입니다:

{
    "status_code": "400",
    "message": "Bad Request",
    "data": null,
    "error": {
        "error_list": [
            "data.password : {   } 은 크기가 4에서 12 사이여야 합니다",
            "data.email : { hong.com } 은 올바른 형식의 이메일 주소여야 합니다",
            "data.id : {  } 은 크기가 3에서 10 사이여야 합니다",
            "data.score : { 1000 } 은 100 이하여야 합니다",
            "data.password : {   } 은 공백일 수 없습니다",
            "data.id : {  } 은 공백일 수 없습니다"
        ]
    }
}

 

 

전체 코드

import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.example.model.Api;
import org.example.model.Student;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.stream.Collectors;


@RestController
@Slf4j
@RequestMapping("/student/info")
public class StudentValidationController {

    @PostMapping("")
    public Api<? extends Object> studentInfo(
            @Valid
            @RequestBody
            Api<Student> student,

            BindingResult bindingResult
    ) {
        if (bindingResult.hasErrors()) {
            var errorList = bindingResult.getFieldErrors().stream()
                    .map(it -> {
                        var format = "%s : { %s } 은 %s";
                        var errorFormat = String.format(format, it.getField(), it.getRejectedValue(), it.getDefaultMessage());

                        return errorFormat;
                    }).collect(Collectors.toList());


            var body = Api.Error.builder()
                    .errorList(errorList)
                    .build();

            var response = Api.builder()
                    .statusCode(String.valueOf(HttpStatus.BAD_REQUEST.value()))
                    .message(HttpStatus.BAD_REQUEST.getReasonPhrase())
                    .error(body)
                    .build();

            return response;
        }


        log.info("info : {}", student);

        return student;
    }
}

 

전체 코드를 보면 서비스 로직이 무엇인지 잘 보이지 않습니다.

이는 서비스 로직 안에서 오류 처리를 하기 때문입니다.

다음 글에서는 서비스 로직에서 오류 처리를 하지 않고, 이를 따로 다른 클래스에서 처리하는 방법을 설명하겠습니다.

 

  1. 오류가 있는지 확인: bindingResult.hasErrors()를 사용하여 요청에 오류가 있는지 확인합니다.
  2. 오류 메시지 생성: bindingResult에서 오류 목록을 추출하여 각 필드에 대한 오류 메시지를 생성합니다.
  3. API 오류 응답 생성: 오류 메시지를 Api.Error 객체에 담고, 이를 포함한 Api 응답을 생성하여 반환합니다.
  4. 정상 처리: 오류가 없는 경우, 입력받은 student 객체를 그대로 반환합니다.

 

728x90