meaningful_life
meaningful life
meaningful_life
전체 방문자
오늘
어제
  • 분류 전체보기 (28)
    • Programming (28)
      • Backend (25)
      • Machine Learning (1)
      • Infrastructure (2)

블로그 메뉴

  • 홈

공지사항

인기 글

태그

  • 머신러닝
  • flask
  • ubuntu
  • install
  • 자바
  • Spring
  • Kubernetes
  • docker
  • stringbuilder
  • linux
  • java
  • python
  • 쿠버네티스
  • 자바의신
  • Database
  • git
  • kubectl
  • 백준
  • error
  • ufw

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
meaningful_life

meaningful life

Spring Request 데이터를 List 형태로 받으면 @Valid 체크가 안되는 현상 해결 방법
Programming/Backend

Spring Request 데이터를 List 형태로 받으면 @Valid 체크가 안되는 현상 해결 방법

2021. 12. 4. 10:50
728x90

Spring does not check valid when list data is databinding

다음과 같은 예시가 있습니다.

스프링에서 Controller 로 데이터가 넘어올 때 [java.util.List] 형태의 데이터만 넘어와서

해당 List에 맞게 DTO를 만들어서 List로 데이터를 받는데,

List 안의 데이터에 대해서 분명히 조건 체크를 하게 했는데 Null로 데이터가 넘어온 데이터로 작업하게 되어 에러가 발생한다 ?????

위 문제로 인해 저도 여러 해결 방법을 찾아봤는데요

특히, "Custom Validator를 만들어서 체크하면 된다" 하는 글이 대부분이었습니다.

위 Custom Validator가 아닌 방법으로 좀 더 손쉽게 해결 가능하도록 해봅시다.

우선 이유부터 알아봅시다.

왜 ? 도대체 왜? Controller에서 @Valid를 붙여서 조건을 확인하도록 했는데 확인을 하지 않을까요?

다음과 같은 예시를 작성해서 확인해봅시다.

Controller

 

DTO

 

Test Code

자 조건을 확인해 봅시다.

  1. Controller에서는 RequestBody로 body에 있는 데이터를 List로 받고 valid 체크하도록 되어 있습니다.
  2. DTO에서는 num과 name를 받도록 되어 있고 Min과 NotNull 체크를 하네요
  3. TestCode에서는 우리가 받을 list의 데이터로 작성한 num과 name은 없는 전혀 이상한 productData만 넘겨주도록 되어 있습니다.

과연 잘 넘어올까요??

실행해보니 위와 같은 결과 나왔습니다.

  1. 우선 list가 null은 아니라네요?
  2. 그렇기 때문에 for 문이 실행됐습니다. 그런데 ?????
    name : null
    num : 0
    ??????????????????????????
  3. 가장 큰 문제는 메소드 파라미터 부터 Valid에 걸려 에러가 발생했음을 알려줘야 합니다.
    위 문자들이 한 글자도 적히면 안된다는 것입니다.

List로 데이터를 받을 경우 위와 같이 Valid 체크가 되지 않고 null 데이터들이 우리의 서버를 마구잡이로 돌아다니고 있는 것을 알 수 있습니다.

이러면 안되겠죠!?

 

그렇다면 해결 방법은 ??????

해결 방법으로 Custom Validator를 생성해서 확인하는 방법도 있는데요

하지만 좀 더 가시적? 쉽게 ? 만들어 볼까요 ?

방법은 바로, List를 직접 받지 않고

Request용 DTO Class에 선언되는 변수 목록에

List<>를 받도록 포함하면 된다는 것입니다.

다음과 같이 만들어 봅시다.

PathVariable은 그냥 붙여봤....

Controller

 

DTO

보시면 List가 포함되어 있는 것을 볼 수 있습니다.

DTO를 작성할 때 유의할 점이 있습니다.

다음과 같이 List 자체에 대해서 @valid를 붙이지 않았는데요

List에 있는 데이터에 대해서 @NotNull or @Min or @Max 와 같은 조건을 체크하도록 list 안에서 어노테이션을 붙여도

@Valid 유효한 조건이 체크가 되지 않을까요?

정답은 체크 되지 않는다 입니다. 그렇다면 어떻게?

List 객체 자체의 NotNull 체크 그리고 List 안에 있는 데이터에 대해 Valid 체크하기 위해

Controller + 여기서도 @Valid를 사용해줍니다.

위에서 말한 것처럼 웃기게도 여기서는 Valid Check가 됩니다.

 

Test Code

list에 들어가는 데이터 세팅

 

위와 같이 준비해서 테스트를 돌려봅시다.

다음과 같이 데이터가 잘 통과돼서 데이터를 반환해준 것을 볼 수 있습니다.

그렇다면 지금까지 이렇게 한 이유인 null 체크가 되는지 확인해보겠습니다.

다음과 같이 list에 들어가는 productData에 null을 넣어 테스트 코드를 변경하고 실행해보겠습니다.

사진으로 다 담기는 힘들어서 결과를 문자로 그대로 복,붙 해보겠습니다.

아래 전체 문자를 보기 전에 조금 나눠서 먼저 설명하겠습니다.

  • Request 데이터는 null 포함해서 Body에 잘 들어가있네요!
    • MockHttpServletRequest: HTTP Method = PUT Request URI = /example/databind/list/100 Parameters = {} Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"71"] Body = {"name":"홍길동","databindList":[{},{"productData":"닭가슴살"}]} Session Attrs = {}
  • 첫 번째 줄을 보시면 알겠지만 HandlerException이 발생한 것을 볼 수 있습니다
    • org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [1]
      Validation failed for argument가 있네요! Valid 체크가 잘 된 것을 볼 수 있습니다.
    • 2021-12-04 10:23:37.693 WARN 23420 --- [ main] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [1] in public java.lang.String com.example.demo.databind_list.DatabindListController.databindListExample2(java.lang.Integer,com.example.demo.databind_list.DatabindListDTO$DatabindRequest): [Field error in object 'databindRequest' on field 'databindList[0].productData': rejected value [null]; codes [NotNull.databindRequest.databindList[0].productData,NotNull.databindRequest.databindList.productData,NotNull.databindList[0].productData,NotNull.databindList.productData,NotNull.productData,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [databindRequest.databindList[0].productData,databindList[0].productData]; arguments []; default message [databindList[0].productData]]; default message [must not be null]] ]
  • 마지막에 결국에는 Response 반환 값을 줄 때 400 Error를 함께 주네요
    • MockHttpServletResponse: Status = 400 Error message = null Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"] Content type = null Body = Forwarded URL = null Redirected URL = null Cookies = []
2021-12-04 10:23:37.693  WARN 23420 --- [           main] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [1] in public java.lang.String com.example.demo.databind_list.DatabindListController.databindListExample2(java.lang.Integer,com.example.demo.databind_list.DatabindListDTO$DatabindRequest): [Field error in object 'databindRequest' on field 'databindList[0].productData': rejected value [null]; codes [NotNull.databindRequest.databindList[0].productData,NotNull.databindRequest.databindList.productData,NotNull.databindList[0].productData,NotNull.databindList.productData,NotNull.productData,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [databindRequest.databindList[0].productData,databindList[0].productData]; arguments []; default message [databindList[0].productData]]; default message [must not be null]] ]

MockHttpServletRequest:HTTP Method = PUT
      Request URI = /example/databind/list/100
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"71"]
             Body = {"name":"홍길동","databindList":[{},{"productData":"닭가슴살"}]}
    Session Attrs = {}

Handler:
             Type = com.example.demo.databind_list.DatabindListController
           Method = com.example.demo.databind_list.DatabindListController#databindListExample2(Integer, DatabindRequest)

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 = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

MockHttpServletRequest:
      HTTP Method = PUT
      Request URI = /example/databind/list/100
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"71"]
             Body = {"name":"홍길동","databindList":[{},{"productData":"닭가슴살"}]}
    Session Attrs = {}

Handler:
             Type = com.example.demo.databind_list.DatabindListController
           Method = com.example.demo.databind_list.DatabindListController#databindListExample2(Integer, DatabindRequest)

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 = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

java.lang.AssertionError: Status expected:<200> but was:<400>
Expected :200
Actual   :400
<Click to see difference>

    at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:59)
    at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:122)
    at org.springframework.test.web.servlet.result.StatusResultMatchers.lambda$matcher$9(StatusResultMatchers.java:627)
    at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:212)
    at com.example.demo.databind_list.DatabindListControllerTest.databindListExample2(DatabindListControllerTest.java:71)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
    at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:210)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

 

위와 같이 List의 데이터에 대해 Valid 체크가 잘 되는 것을 확인 할 수 있습니다.

설명이 길었을 뿐! 생각보다는 쉽죠?

모두 문제가 잘 해결되시길 바랍니다 ^_^

 

전체 코드

Controller

package com.example.demo.databind_list;

import org.springframework.web.bind.annotation.*;
import com.example.demo.databind_list.DatabindListDTO.DatabindResponse;
import com.example.demo.databind_list.DatabindListDTO.DatabindRequest;
import com.example.demo.databind_list.DatabindListDTO.DatabindList;

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

@RestController
@RequestMapping("/example/databind")
public class DatabindListController {
    @PutMapping("/list")
    public void databindListExample1(@Valid @RequestBody List<DatabindList> list){
        System.out.println("data가 있을까요? " + list.isEmpty());
        for (DatabindList data : list){
            System.out.println("name : " + data.getName());
            System.out.println("num : " + data.getNum());
            System.out.println("------- 오! 들어왔네?");
        }
        System.out.println("과연 데이터가 있었을까요?");
    }

    @PutMapping("/list/{api}")
    public String databindListExample2(
            @PathVariable("api") Integer api, @Valid @RequestBody DatabindRequest request){
        DatabindResponse response = new DatabindResponse();

        response.setApi(api);
        response.setData1(request.getDatabindList().get(0).getProductData());
        response.setData2(request.getDatabindList().get(1).getProductData());
        response.setName(request.getName());
        return response.toString();
    }
}

DTO

package com.example.demo.databind_list;

import lombok.Data;

import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;

public class DatabindListDTO {
    @Data
    public static class DatabindList{
        @Min(1)
        private int num;
        @NotNull
        private String name;
    }

    @Data
    public static class DatabindRequest{
        @NotEmpty
        private String name;
        @NotNull @Valid
        private List<DatabindExampleList> databindList;

        @Data
        public static class DatabindExampleList{
            @NotNull
            private String productData;
        }
    }

    @Data
    public static class DatabindResponse{
        private String name;
        private Integer api;
        private String data1;
        private String data2;
    }
}

Test Code

package com.example.demo.databind_list;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.jupiter.api.BeforeEach;
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.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import com.example.demo.databind_list.DatabindListDTO.DatabindResponse;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
class DatabindListControllerTest {

    @Autowired
    private MockMvc mockMvc;

    private JSONArray arr;

    @BeforeEach
    public void setting() throws JSONException {
        arr = new JSONArray();
        arr.put(new JSONObject().put("productData",null));
        arr.put(new JSONObject().put("productData","닭가슴살"));
    }

    @Test
    @DisplayName("리스트로 데이터 받을 경우 validation 체크 여부 확인")
    public void databindListExample1() throws Exception {
        mockMvc.perform(
                put("/example/databind/list")
                .content(arr.toString())
                .contentType(MediaType.APPLICATION_JSON_VALUE)
        )
                .andDo(print());
    }

    @Test
    @DisplayName("데이터 바인드시 리스트 데이터 validation 체크")
    public void databindListExample2() throws Exception {
        JSONObject result = new JSONObject();
        result.put("name", "홍길동");
        result.put("databindList", arr);

        DatabindResponse response = new DatabindResponse();
        response.setData1("고구마");
        response.setData2("닭가슴살");
        response.setName("홍길동");
        response.setApi(100);

        mockMvc.perform(
                put("/example/databind/list/100")
                .content(result.toString())
                .contentType(MediaType.APPLICATION_JSON)
        )
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().string(response.toString()))
        ;
    }

}
728x90

'Programming > Backend' 카테고리의 다른 글

1.Flask 웹 서버 구축 시작  (0) 2022.04.20
MariaDB + SQLAlchemy + Flask에서 database 연결 끊김 현상  (0) 2022.03.21
협업을 위한 snake_case -> camelCase 변환 방법 @JsonNaming, @JsonProperty  (0) 2021.11.22
객체 지향, 오브젝트와 의존 관계의 이해  (0) 2021.10.09
API 명세서 뜯어보기 - StringTokenizer Class  (0) 2021.10.05
    'Programming/Backend' 카테고리의 다른 글
    • 1.Flask 웹 서버 구축 시작
    • MariaDB + SQLAlchemy + Flask에서 database 연결 끊김 현상
    • 협업을 위한 snake_case -> camelCase 변환 방법 @JsonNaming, @JsonProperty
    • 객체 지향, 오브젝트와 의존 관계의 이해
    meaningful_life
    meaningful_life
    하루하루가 의미 있고 가치 있는 삶이 될 수 있길. 그리고 나의 추억과 각종 지식의 기록과 이를 공유함으로써 다른 사람에게도 도움이 되길...

    티스토리툴바