개발/Spring(Hodol)

Spring - 데이터 검증(컨트롤러, 데이터 타입, @ControllerAdvice...)

잇(IT) 2023. 8. 28. 14:46
728x90
- HTTP method

 

- GET : 메서드 GET는 지정된 리소스의 표현을 요청합니다. 를 사용하는 요청은 GET데이터 검색만 해야 합니다.

- HEAD : 이 메서드는 요청과 동일하지만 응답 본문이 없는 HEAD응답을 요청합니다 .GET

- POST : 이 POST메서드는 엔터티를 지정된 리소스에 제출하며, 종종 서버의 상태 변경이나 부작용을 유발합니다.

- PUT : 이 PUT메서드는 대상 리소스의 모든 현재 표현을 요청 페이로드로 바꿉니다.

- Delete : 이 DELETE메서드는 지정된 리소스를 삭제합니다.

- Connect : 이 CONNECT방법은 대상 리소스로 식별된 서버에 대한 터널을 설정합니다.

- Options : 이 OPTIONS방법은 대상 리소스에 대한 통신 옵션을 설명합니다.

- Trace : 이 TRACE메서드는 대상 리소스에 대한 경로를 따라 메시지 루프백 테스트를 수행합니다.

- Patch : 이 PATCH방법은 리소스에 부분 수정을 적용합니다.


- PostCreate.java

@Getter @Setter
@ToString
public class PostCreate {
    private String title;
    private String content;
}

 

- PostController.java

@RestController
@Slf4j
public class PostController {

    @PostMapping("/posts")
    public String post(PostCreate params) {
        log.info("params={}", params.toString());
        return "Hello World";
    }
}

- post를 통해 아무런 값이 넘어오지 않았기 때문에 PostCreate의 필드인 title과 cotent에는 null 값이 들어가 있는 상태인 것을 확인 할 수 있다.


- PostController.java

@RestController
@Slf4j
public class PostController {

    @PostMapping("/posts")
    public Map<String, String> post(PostCreate params) {
        log.info("params={}", params.toString());
        return Map.of();
    }
}

- return 값으로 Map으로 변경한다.

- Test 코드 추가

- PostControllerTest.java

@WebMvcTest
//MockMvc를 주입받기 위해서 사용한다.
class PostControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @DisplayName("/posts 요청시 Hello World를 출력한다.")
    void test() throws Exception {
        //expected
        mockMvc.perform(MockMvcRequestBuilders.post("/posts")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\":\"제목입니다.\", \"content\":\"내용입니다.\"}")
                )
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().string("{}"))
                .andDo(MockMvcResultHandlers.print());
    }
}

- MockMvc를 이용하여 요청 화면을 직접 생성하여 데이터를 보내지 않아도 테스트 용도로 만들어 볼 수 있다.

1. JSON 형태의 POST 방식으로 요청을 보낸다.

2. 그에 대한 응답에 대한 기대값을 작성한다.

 

- request

 

- response


- 데이터 검증

 

- build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    
    // 검증을 위해 gradle에 추가해야 하는 부분
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

- PostCreate.java

@Getter @Setter
@ToString
public class PostCreate {

    @NotBlank(message = "title을 입력해주세요")
    private String title;

    @NotBlank(message = "content를 입력해주세요")
    private String content;
}

- 직접 검증을 위한 코드를 작성해도 되지만, 검증에는 많은 요소들이 있기 때문에 Spring에서 편의를 제공해준다.

- 위의 코드처럼 @NotBlank와 같이 어노테이션을 지정해주면 스프링에서 검증을 해준다.

 

- PostController.java

@RestController
@Slf4j
public class PostController {

    @PostMapping("/posts")
    public Map<String, String> post(@RequestBody @Valid PostCreate params) {
        log.info("params={}", params.toString());
        return Map.of();
    }
}

 

- PostControllerTest.java

@Test
    @DisplayName("/posts 요청시 title값은 필수다.")
    void test2() throws Exception {
        //expected
        mockMvc.perform(MockMvcRequestBuilders.post("/posts")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\" : \"\", \"content\" : \"내용입니다.\"}")
                )
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print());
    }
}

- 검증에 실패했기 때문에 400오류가 발생하는 것을 확인 할 수 있다.


- BindingResult

 

- BindingResult를 사용하게 되면 검증에서 발생한 오든 에러들이 담기게 된다.

 

- PostController.java

@RestController
@Slf4j
public class PostController {

    @PostMapping("/posts")
    public Map<String, String> post(@RequestBody @Valid PostCreate params, BindingResult result) {
        log.info("params={}", params.toString());

        if (result.hasErrors()) {
            List<FieldError> fieldErrors = result.getFieldErrors();
            FieldError firstFieldError = fieldErrors.get(0);
            String fieldName = firstFieldError.getField(); // title에 검증 오류 값이 있기 때문에 title이 넘어온다.
            String errorMessage = firstFieldError.getDefaultMessage();// 에러메시지가 넘어간다.

            Map<String, String> error = new HashMap<>();
            error.put(fieldName, errorMessage);
            return error; // 반환값도 맞춰줘야 한다.
        }
        return Map.of();
    }
}

- BindingResult를 파라미터로 넣어준 다음 bindingResult에 저장된 에러들을 error라는 객체에 넣어 반환한다.

 

- PostControllerTest.java

@Test
    @DisplayName("/posts 요청시 title값은 필수다.")
    void test2() throws Exception {
        //expected
        mockMvc.perform(MockMvcRequestBuilders.post("/posts")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\" : \"\", \"content\" : \"내용입니다.\"}")
                )
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(jsonPath("$.title").value("title을 입력해주세요"))
                .andDo(MockMvcResultHandlers.print());
    }
}

 

- PostCreate.java

@Getter @Setter
@ToString
public class PostCreate {

    @NotBlank(message = "title을 입력해주세요")
    private String title;

    @NotBlank(message = "content를 입력해주세요")
    private String content;
}

- title 필드에서 에러가 발생했기 때문에 body에 error 객체에 저장된 key, value 값이 반환되는 것을 확인 할 수 있다.

 

.andExpect(jsonPath("$.title").value("title을 입력해주세요"))

- 추가로 jsonPath를 통해 json에 저장된 key에 대한 value 값을 검증해 볼 수 있다.


- ControllerAdvice

- bindingResult를 통해 매번 에러에 대한 조건을 작성하기는 비효율적이기 때문에 @ControllerAdvice를 사용한다.

- @ControllerAdvice를 사용하게 되면 전역 컨트롤러에서 발생하는 예외를 처리하고 관리하는 데 사용된다.

- BindingResult가 있으면 에러가 발생하게 되면 bindingResult에 담겨 넘어가게 되기 때문에 ControllerAdvice를 사용하게 되면, BindingResult는 지워준다.

 

- ExceptionController.java

@Slf4j
@ControllerAdvice
public class ExceptionController {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody // controller는 rest이기 때문에 body로 넘어가는데
    // controllerAdvice는 viewResolver로 넘어가기 때문에 body 어노테이션을 달아서
    // body로 넘겨준다.
    public Map<String, String> exceptionHandler(MethodArgumentNotValidException e) {
        FieldError fieldError = e.getFieldError();
        String field = fieldError.getField();
        String message = fieldError.getDefaultMessage();
        Map<String, String> response = new HashMap<>();
        response.put(field, message);
        return response;
    }
}

- 예외의 최고 조상인 Exception으로 받아도 되지만 Exception으로 받을 경우 특정 에러의 세부사항을 알 수 없기 때문에 현재 발생하는 MethodArgumentNotValidException 클래스를 사용하여 에러의 field와 message를 받아서 body에 json 형태로 넘길 수 있다.

 

- PostControllerTest.java

@Test
    @DisplayName("/posts 요청시 title값은 필수다.")
    void test2() throws Exception {
        //expected
        mockMvc.perform(MockMvcRequestBuilders.post("/posts")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\" : \"\", \"content\" : \"내용입니다.\"}")
                )
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(jsonPath("$.title").value("title을 입력해주세요"))
                .andDo(MockMvcResultHandlers.print());
    }
}

- ExceptionController에서 보낸 json형태의 데이터가 넘어온 것을 확인 할 수 있다.

 

* MockMvc에서 요청을 생성 -> body에 json형태로 넘어온 값에 title 값이 비어있는 상태 -> valid에 의해 에러 발생 -> 에러 발생 시 ControllerAdvice로 해당 에러 이동 -> body에 json 형태로 에러에 대한 정보 전달 -> HttpResponse에 담아서 전달


- ExceptionController에서 Map 형태로 넘기는 것이 아닌 응답 전용 클래스를 생성해서 전달 할 수 있다. 에러 클래스, 에러 json을 만드는 규칙은 전부 다르다.

 

- ErrorResponse.java

@Getter
@RequiredArgsConstructor
public class ErrorResponse {

    private final String code;
    private final String message;
}

- 응답 전용 클래스를 생성하게 되면 에러가 발생했을 때 원하는 필드를 작성하여 json 형태로 값을 전달 할 수 있다.

 

- ExceptionController.java

@Slf4j
@ControllerAdvice
public class ExceptionController {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody // controller는 rest이기 때문에 body로 넘어가는데
    // controllerAdvice는 viewResolver로 넘어가기 때문에 body 어노테이션을 달아서
    // body로 넘겨준다.
    public ErrorResponse exceptionHandler(MethodArgumentNotValidException e) {
        return new ErrorResponse("400", "잘못된 요청입니다.");
    }
}

 

 - PostControllerTest.java

@Test
    @DisplayName("/posts 요청시 title값은 필수다.")
    void test2() throws Exception {
        //expected
        mockMvc.perform(MockMvcRequestBuilders.post("/posts")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\" : \"\", \"content\" : \"내용입니다.\"}")
                )
                .andExpect(MockMvcResultMatchers.status().isBadRequest())
                .andExpect(jsonPath("$.code").value("400"))
                .andExpect(jsonPath("$.message").value("잘못된 요청입니다."))
                .andDo(MockMvcResultHandlers.print());
    }
}

- ExceptionController.java에 작성된 메서드에 의해 반환된 ErrorResponse가 @ResponseBody에 의해 json 형태로 변환되어 응답에 전달된다.

 

- 위 경우에는 에러가 발생한 경우 해당 사실을 전달 할 수 있지만 세부사항에 대한 정보를 보내기는 어려운 상황이다.


- ErrorResponse.java

@Getter
@RequiredArgsConstructor
public class ErrorResponse {

    private final String code;
    private final String message;

    private final Map<String, String> validation = new HashMap<>();

    public void addValidation(String field, String errorMessage) {
        this.validation.put(field, errorMessage);
    }
}

- Map 대신 생성한 클래스에 원하는 필드를 생성하여 넘김과 동시에 어떤 구체적인 에러가 발생했는지 전달하기 위해 bindingResult로 넘어온 값을 넣을 Map을 하나 생성한다.

- addValidation 메서드는 에러가 발생한 field와, errorMessage를 필드로 받아서 validation Map 객체에 값을 넣는다.

 

- ExceptionController.java

@Slf4j
@ControllerAdvice
public class ExceptionController {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody // controller는 rest이기 때문에 body로 넘어가는데
    // controllerAdvice는 viewResolver로 넘어가기 때문에 body 어노테이션을 달아서
    // body로 넘겨준다.
    public ErrorResponse exceptionHandler(MethodArgumentNotValidException e) {
        ErrorResponse response = new ErrorResponse("400", "잘못된 요청입니다.");

        for (FieldError fieldError : e.getFieldErrors()) {
            response.addValidation(fieldError.getField(), fieldError.getDefaultMessage());
        }
        return response;
    }
}

- bindingResult에 여러개의 검증 에러가 발생하게 되면 getFieldErrors List에 담기게 되고, 해당 List를 for문으로 돌리면서 해당 에러의 field와 message를 받아와 validation 객체에 순차적으로 넣는다.

 

- PostControllerTest.java

@Test
    @DisplayName("/posts 요청시 title값은 필수다.")
    void test2() throws Exception {
        //expected
        mockMvc.perform(MockMvcRequestBuilders.post("/posts")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"title\" : \"\", \"content\" : \"내용입니다.\"}")
                )
                .andExpect(MockMvcResultMatchers.status().isBadRequest())
                .andExpect(jsonPath("$.code").value("400"))
                .andExpect(jsonPath("$.message").value("잘못된 요청입니다."))
                .andExpect(jsonPath("$.validation.title").value("title을 입력해주세요"))
                .andDo(MockMvcResultHandlers.print());
    }
}

- 마찬가지로 Test 코드를 실행하게 되면 title에 빈 값이 들어가 검증 에러가 발생하게 되고, ErrorResponse를 통해 개발자가 임의로 생성한 에러 필드들과 bindingResult를 통해 넘어온 에러들을 함께 body에 json 형태로 넣어서 전달할 수 있다.

 

728x90