※ 이 포스트는 스프링 실습 과정에서 작성하기 때문에 정보가 부정확할 수 있는 부분이 있습니다.
따라서 참고만 해주시고 틀린 부분이 있을 경우 알려주시면 감사하겠습니다.
이번 포스트는 제 깃허브의 https://github.com/kth1017/project_newPostLink 레포를 바탕으로 작성했습니다.
해당 레포의 ControllerTest를 작성하다 Test는 통과했지만 실제 톰캣 구동 후 해당 메소드를 실행했을때 415 오류가 발생하여 이번 포스트를 작성합니다. 기본적인 HTTP 지식이 있다는 것을 가정하므로 자세한 설명은 생략하겠습니다.
원인 탐색
실습을 하다가 테스트를 작성하여 통과하고 실제 톰캣 구동에서 415 오류를 만나게 되었습니다. 415 오류는 처음 만나는거라 당황했었는데 해결이 끝나니 크게 어려운 내용은 아니었었네요.
우선 415 오류를 스프링에서 만나시면 저처럼 아래 화면을 만나게 될텐데요.

오류를 조금만 읽어봐도 이 에러는 클라이언트에서 전송한 request의 content-type을 서버가 지원하지 않는다는 걸 알 수 있습니다. 앞서 post로 보낸 요청에 있어 개발자 도구를 열어 Header를 살펴보면

내가 보낸 요청의 헤더에 Content-Type이 application/x-www-form-urlencoded으로 되어 있는 것을 보실 수 있는데 아시다시피 보통 Client에서 보내는 Content-Type은 쿼리 스트링 형식인 application/x-www-form-urlencoded과 json 형식인 application/json 두 가지가 제일 흔합니다.
저 같은 경우는 타임리프로 간단히 view를 작성하므로 단순 쿼리 스트링으로 form을 제출하므로 이런 것이고 ajax 등 javascript를 사용해서 json 형식으로 요청을 보내는 것도 가능합니다.
문제 해결
결국엔 415 에러가 발생하는 이유는 view를 통해 요청을 보내는 형식은 현재 쿼리스트링이지만 서버의 컨트롤러가 해당 요청을 자바 객체화 하는 과정에서 해당 타입을 지원하지 않기 때문입니다.
따라서 다음과 같은 방법으로 해결이 가능할 것 같습니다.
1. 요청을 받는 컨트롤러의 post 메소드 코드를 수정한다.
2. 애초에 form을 컨트롤러에 json 형식으로 넘긴다.
이번 포스트는 간단하게 1번 방법으로 해결이 됩니다. 알아보니 최근 실무에서도 front, back의 분업이 잦다보니 해당 이슈에 대해 정보가 많더라구요.
우선 가장 먼저 찾은 정보는 GET은 @ModelAttribute, POST는 @RequestBody를 써라 라는 내용의 글인데 사실 이 글도 좋은 글이겠지만 더 찾아보며 이를 사용할 때 유의할 사항이 있음을 알았습니다.
@ModelAttribute와 @RequestBody에 대해 잘 모르신다면 https://fadet-coding.tistory.com/66 를 보고 오시면 좋을 것 같습니다.
최근 대부분은 REST API를 사용하기에 요청이나 응답 모두 JSON 형식으로 되있는 것이 당연하듯 하지만 개발이 간단하거나 레거시 코드를 그대로 쓰는 경우 서버에 요청이 쿼리스트링으로 오는 경우도 있습니다. 원래라면 협업을 하는 구성원들이 사용하는 API 명세에서 무조건 요청을 json으로 못 박으면 가장 확실하지만 의외로 이미 쿼리스트링으로 작업을 거의 다 진행했다던지 하는 문제가 생기곤 합니다.
일반적으로 json 데이터를 컨트롤러가 받을때 사용하는 @RequestBody 어노테이션의 경우 메시지 컨버터가 작동해 jackson 라이브러리가 json 형식의 데이터를 자바 객체로 변환해주기 때문에 json 형식의 요청의 경우 대부분 문제 없이 넘어갑니다.
메시지 컨버터가 작동하는 원리는 더 복잡하지만 조금 간단히 정리하면 컨트롤러의 파라미터 앞에 @ModelAttribute나 @RequestBody가 붙어있다면 스프링에선 AnnotationMethodHandlerAdapter라는 어댑터가 어노테이션에 맞는 메시지 컨버터를 조회하여 각 어노테이션에 맞는 메시지 컨버터를 등록합니다. @RequestBody 어노테이션이 붙었다면 MappingJackson2HttpMessageConverter가 등록되어 JSON 데이터를 Jackson 라이브러리가 자바 객체로 변환하여 줍니다.
하지만 쿼리스트링 형식의 데이터는 컨트롤러의 파라미터로 일반적인 VO를 인식하지 않으며 아래 코드처럼 파라미터 타입을 Map으로 정의해주어야 메시지 컨버터가 작동합니다.
//비효율적
@PostMapping("/")
public String index(@RequestBody MultiValueMap<String, String> form){
이 경우 마찬가지로 스프링에선 AnnotationMethodHandlerAdapter가 어노테이션에 맞는 메시지 컨버터를 조회하고 그 결과 FormHttpMessageConveter가 등록됩니다. 이 메시지 컨버터는 이후 쿼리스트링 데이터를 자바 객체로 변환하여 줍니다.
하지만 위 방법처럼 쿼리스트링 요청을 처리하면 쓸데 없는 클래스를 하나 더 정의해야하고 깔끔하지도 않습니다. 또한 무엇보다 FormHttpMessageConveter가 효율적으로 사용되지 않습니다. 그런데 @RequestBody 대신 어노테이션으로 @ModelAttribute를 사용하면 메시지 컨버터로 앞서 등록된 FormHttpMessageConveter를 똑같이 사용합니다. 그러므로 @ModelAttribute는 쿼리 스트링을 받으면 자연스럽게 자바 객체로 변환해주는 것이죠.
따라서 요청을 보내는 형식이 json과 쿼리스트링 두 가지라면 @RequestBody와 @ModelAttribute 두 어노테이션을 같이 사용하면 해결되는거죠.
여기서 어떻게 두 어노테이션을 같이 쓸 수 있는지 의문을 가지시는 분도 있을 수 있습니다. 이것은 postMapping에 consumes를 추가해주면 해결됩니다. 아래 코드처럼 메소드별로 consumes 이후에 MediaType을 지정해주어 요청 타입을 강제해주면 됩니다.
@PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
public String postCodeJson(@RequestBody InputForm form){
Code code = new Code(form.getAllCode(), form.getTitleHtmlKeyword(), form.getIndexHtmlKeyword());
codeService.saveCode(code);
return "redirect:/valid";
}
@PostMapping(value = "/", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public String postCodeParam(@ModelAttribute InputForm form){
Code code = new Code(form.getAllCode(), form.getTitleHtmlKeyword(), form.getIndexHtmlKeyword());
codeService.saveCode(code);
return "redirect:/valid";
}
추가로 2번 방법인 애초에 view에서 요청을 보내는 형식을 JSON으로 바꾸는 것 역시 415 에러를 해결하는 것이 가능합니다만 이를 하기 위해선 javascript를 사용해 쿼리 스트링을 json 형식으로 바꿔주어야하는 매우 번거로운 작업을 해줘야하기 때문에 그냥 간단하게 postman으로 테스트만 하고 넘어가겠습니다.
우선 컨트롤러에 post 메서드를 나누지 않고 원래대로 놔둡니다. 이러면 해당 메소드는 json 형식이 아니면 415 에러가 발생하겠죠?
@PostMapping("/")
public String postCodeJson(@RequestBody InputForm form){
Code code = new Code(form.getAllCode(), form.getTitleHtmlKeyword(), form.getIndexHtmlKeyword());
codeService.saveCode(code);
return "redirect:/valid";
}
이제 postman에서 json 형식으로 요청 하나를 보내겠습니다.

에러 코드로 500이 발생했습니다. 하지만 스택트레이스를 읽어보면 앞선 메소드인 postCodeJson에서 문제가 발생하지 않았고 다음의 valid 메소드에서 NPE가 발생합니다. 따라서 요청 데이터를 JSON 형식으로 보내면 문제가 없음을 알 수 있으며 에러가 발생한건 당연히 postman은 서버와 연결되어 있지 않으므로 valid에서 사용하는 codeService를 알리가 없고 NPE가 발생할 수 밖에 없습니다.
부가 설명
사실 제 경우는 조금 특별한 케이스입니다. 어떤 분이 보실땐 이런 의문이 드실 수 있습니다. " 아니 넌 요청을 쿼리 스트링으로만 보내니까 컨트롤러에 @ModelAttribute만 사용해도 충분하지 않아? "라구요. 맞습니다. 하지만 그게 다는 아닙니다.
애초에 제가 이 포스트를 작성한 이유는 테스트 코드로 해당 이슈가 걸러지지 않았기에 발생했기 때문입니다. 실제로 앞선 말은 맞는 말이지만 포스트를 작성하기 위해 정보를 찾아보기 전까진 저런 고민조차 하지 않았습니다. 따라서 저는 테스트 코드를 다시 살펴보았습니다. 분명히 테스트 코드를 잘 통과해서 프로덕트 코드를 작성했는데 왜 이랬을까요?
그것은 제가 TDD에 미숙했기 때문입니다. 분명 저는 테스트 코드로 다음과 같은 코드를 작성했습니다.
/* 생략 */
mvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(form)))
.andExpect(status().is3xxRedirection());
위 코드에서 contentType을 JSON으로 설정한 것이 보이시나요? 하지만 제가 해당 테스트 코드를 통과하기 위해 작성한 view는 form을 쿼리스트링으로 전달하기에 415 에러가 발생한 것입니다.
따라서 저 코드 역시 MediaType.APPLICATIN_FORM_URLENCORDED_VALUE로 바꿔주면 정상 작동합니다. 한마디로 제가 TDD 과정 중 테스트 코드를 통과만 하도록 프로덕트 코드를 짰어야했는데 이전 repo를 참조하다보니 이를 어긴 것입니다.
따라서 앞으로 TDD를 함에 있어서 조금 더 주의해야함을 다시 한 번 깨닫게 되네요 ㅎㅎ...
'Spring' 카테고리의 다른 글
Spring_정리6_Spring과 Spring Boot(feat. Module) (0) | 2023.03.19 |
---|---|
Spring_짧1_ControllerTest와 @ModelAttribute, @RequestBody (0) | 2022.07.18 |
5_짧_th:field와 th:value (0) | 2022.06.25 |
Spring_정리5_Spring의 구조 훑어보기_FrontController (0) | 2022.05.09 |
Spring_정리4_Spring의 구조 훑어보기_MVC (0) | 2022.05.08 |