관리 메뉴

bright jazz music

blog03: 데이터 검증3 (ExceptionController) 본문

Projects/blog

blog03: 데이터 검증3 (ExceptionController)

bright jazz music 2022. 12. 27. 13:21

요약:

 

테스트 코드 ==> PostController ==> 바인딩 에러 발생 ==> ExceptionController ==> 에러를 꺼내서 ErrorResponse 객체에 바인딩하여 json형식으로 클라이언트에게 반환.

 

예상된 값(여기서는 코드 또는 메시지가 반환되지 않으면 에러가 발생하는 문제가 있음)

 

---

test2() 실행하여 json형식으로 PostController에 request

//PostControllerTest.java

package com.endofma.blog.controller;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; //이게 맞음
//import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath; 원래 이걸로 했음.


@WebMvcTest
class PostControllerTest {
    @Autowired
    private MockMvc mockMvc; ////Could not autowire. No beans of 'MockMvc' type found.

    @Test
    @DisplayName("/posts 요청시 hello world를 출력한다")
    void test() throws Exception {
        //expected

        //기본적으로 Content-Type을 application/json으로 보냄. 예전에는 application/x-www-form-urlencoded를 썼다.
        mockMvc.perform(post("/posts")
                        .contentType(MediaType.APPLICATION_JSON) //기본값이라 주석처리
                        .content("{\"title\": \"제목입니다.\", \"content\": \"내용입니다.\"}") //json 형태로 값 넣어주기
                )
                .andExpect(status().isOk()) //http response 가 200인지
                .andExpect(content().string("{}")) // 내용이 hello world인지
                .andDo(print()); //요청에 대한 전반적인 요약을 출력해준다.

    }

//PostControllerTest.java

    @Test
    @DisplayName("/posts 요청시 title 값은 필수다")
    void test2() throws Exception {
        //expected


        mockMvc.perform(post("/posts")
                        .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"title\": null, \"content\": \"내용입니다.\"}") //title 값을 빈 스트링으로 설정하였다.
//                        .content("{\"title\": \"안녕\", \"content\": \"내용입니다.\"}") //title 값을 빈 스트링으로 설정하였다.
                )
                .andExpect(status().isBadRequest()) //.OK()
                .andExpect(jsonPath("$.code").value("400")) //json 검증
                .andExpect(jsonPath("$.message").value("잘못된 요청입니다!"))
                .andDo(print());

    }

}

 

PostController 내부에 진입하지 못하고 바인딩 에러 발생

//PostController.java

package com.endofma.blog.controller;

import com.endofma.blog.request.PostCreate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


@Slf4j
@RestController
public class PostController {

    @PostMapping("/posts")
    public Map<String, String> post(@RequestBody @Valid PostCreate params) {
    //파라미터에서 BindingResult 제거.

        //이미 바인딩에서 에러 발생. 여기까지 오지 못함.
        log.info("params={}", params.toString());
        return Map.of();
    }

}
  1.  @ControllerAdvice가 붙은 클래스에서 에러 가로챔.
  2. MethodArgumentNotValidException 객체를 사용해서 에러를 받고, 변수명은 e로 선언.
  3. getFieldErrors()를 사용하여  e에서 필드 에러만 뽑아냄.
  4. FieldError 객체를 사용해서 뽑아낸 에러를 바인딩.
  5. getField()와 getDefaultMessage()로 fieldError에서 필드와 기본 에러 메시지를 뽑아냄.
  6. 그걸 우리가 만든 ErrorResponse 객체에 바인딩하여 리턴
//ExceptionController.java

package com.endofma.blog.controller;

import com.endofma.blog.response.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@ControllerAdvice
public class ExceptionController {

    @ResponseStatus(HttpStatus.BAD_REQUEST) //400
    @ResponseBody //응답을 json으로 보내기 위해 추가
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ErrorResponse invalidRequestHandler(MethodArgumentNotValidException e) {

        ErrorResponse response = new ErrorResponse("400", "잘못된 요청입니다!");

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

        }

        return response;
    }
}
//ErrorResponse.java
package com.endofma.blog.response;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

import java.util.HashMap;
import java.util.Map;

/*
* {
*   "code": "400",
*   "message": "잘못된 요청입니다."
*   "validation": {
*       "title": "값을 입력해주세요"
*   }
* }
*/
@Getter    //게터가 없으면 값을 가져올 수 없어서 ResponseBody에 넣어주질 못함.(빈 바디)
@RequiredArgsConstructor
public class ErrorResponse {
    private final String code;
    private final String message;
    private final Map<String, String> vaidation = new HashMap<>(); //final로 선언하면 생성자 파라미터가 3개가 됨. 맵 대신 클래스로 받아보자...

    public void addValidation(String fieldName, String errorMessage){
        ValidationTuple validationTuple = new ValidationTuple(fieldName, errorMessage);
        this.vaidation.put(fieldName, errorMessage);
    }

    // Map을 안 쓰기 위해서 내부 클래스를 만들었으니 이걸로 위의 Map을 대체해 보자..현재는 안 되어 있음
    @RequiredArgsConstructor
    private class ValidationTuple{
        private final String fieldName;
        private final String errorMessage;
}

}

 

결과

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.6)

2022-12-27 12:53:48.209  INFO 36332 --- [    Test worker] c.e.blog.controller.PostControllerTest   : Starting PostControllerTest using Java 11.0.12 on DESKTOP-8H1PTVG with PID 36332 (started by markany-hjcha in D:\personal\blog)
2022-12-27 12:53:48.211  INFO 36332 --- [    Test worker] c.e.blog.controller.PostControllerTest   : No active profile set, falling back to 1 default profile: "default"
2022-12-27 12:53:49.281  INFO 36332 --- [    Test worker] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2022-12-27 12:53:49.281  INFO 36332 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : Initializing Servlet ''
2022-12-27 12:53:49.282  INFO 36332 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : Completed initialization in 1 ms
2022-12-27 12:53:49.310  INFO 36332 --- [    Test worker] c.e.blog.controller.PostControllerTest   : Started PostControllerTest in 1.483 seconds (JVM running for 3.742)

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /posts
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"46"]
             Body = {"title": null, "content": "내용입니다."}
    Session Attrs = {}

Handler:
             Type = com.endofma.blog.controller.PostController
           Method = com.endofma.blog.controller.PostController#post(PostCreate)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = org.springframework.web.bind.MethodArgumentNotValidException

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 400
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"code":"400","message":"잘못된 요청입니다!","vaidation":{"title":"타이틀을 입력하세요!"}}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
BUILD SUCCESSFUL in 6s
4 actionable tasks: 3 executed, 1 up-to-date
PM 12:53:50: Execution finished ':test --tests "com.endofma.blog.controller.PostControllerTest.test2"'.

 

-----

 

 

e!!!=org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.util.Map<java.lang.String, java.lang.String> com.endofma.blog.controller.PostController.post(com.endofma.blog.request.PostCreate): [Field error in object 'postCreate' on field 'title': rejected value [null]; codes [NotBlank.postCreate.title,NotBlank.title,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [postCreate.title,title]; arguments []; default message [title]]; default message [타이틀을 입력하세요!]]

fieldError!!!=Field error in object 'postCreate' on field 'title': rejected value [null]; codes [NotBlank.postCreate.title,NotBlank.title,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [postCreate.title,title]; arguments []; default message [title]]; default message [타이틀을 입력하세요!]
Comments