본문 바로가기
개발/JAVA

[JAVA] Spring Controller를 직접 만들어보자 (4) - 파라미터 바인딩

by 상용최 2021. 1. 28.
반응형

소스주소 : github.com/sangyongchoi/spring-controller-copy

 

대략적인 라우팅기능은 만들었다.

이제는 파라미터 바인딩기능을 만들거다.

 

  • Request -> Class로 변환하기위해 Jackson 라이브러리를 사용할 것이다.
  • 편리한 개발을위해 lombok을 사용할 것이다.
  • 테스트코드 작성을 위해 JUnit5를 사용할 것이다.

아래와 같이 의존성추가를 해준다.

 

테스트 케이스를 작성한다.

일단은 Json -> Class 기능만 만들것이므로 해당 기능이 정상적으로 동작하는지 확인한다.

테스트 코드를 위한 Class

@Builder
@NoArgsConstructor @AllArgsConstructor
@Getter @ToString
@Setter
public class TestDto {
    String name;
    int number;
    Integer wrapperNumber;
    List<TestInner> list = new ArrayList<>();
    Map<String, String> map = new HashMap<>();

    @Builder
    @NoArgsConstructor @AllArgsConstructor
    @Getter @ToString
    @Setter
    public static class TestInner{
         private String test;
         private String title;
    }
}

테스트 코드

테스트코드 Class이름이 HandlerMappingTest이다.

역할과 맞지않은 잘못된 이름이지만 필자는 동작하는지 먼저 확인하고 싶었다.

확인 후에 올바른 역할과 책임을 가지도록 리팩토링 할 예정이다.

class HandlerMappingTest {

    ObjectMapper objectMapper;
    {
        objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
    }

    @Test
    @DisplayName("파라미터 변환 테스트 실패 케이스 Json -> String")
    public void parameter_Convert_Test_fail_wrong_Right_Curly_Bracket_json2String() throws Exception{
        assertThrows(IllegalArgumentException.class, () -> {
            String json = "{\"123\"";
            boolean isStart = json.startsWith("{");
            boolean isEnd = json.endsWith("}");

            if (isStart && isEnd) {
                json = json.substring(1, json.length() - 1);
                boolean isStringStart = json.startsWith("\"");
                boolean isStringEnd = json.endsWith("\"");

                if (isStringStart && isStringEnd) {
                    json = json.substring(1, json.length() - 1);
                } else {
                    throw new IllegalArgumentException("");
                }
            } else {
                throw new IllegalArgumentException("");
            }
        });
    }

    @Test
    @DisplayName("파라미터 변환 테스트 실패 케이스 Json -> String")
    public void parameter_Convert_Test_fail_wrong_Left_Curly_Bracket_json2String() throws Exception{
        assertThrows(IllegalArgumentException.class, () -> {
            String json = "\"123\"}";
            boolean isStart = json.startsWith("{");
            boolean isEnd = json.endsWith("}");

            if (isStart && isEnd) {
                json = json.substring(1, json.length() - 1);
                boolean isStringStart = json.startsWith("\"");
                boolean isStringEnd = json.endsWith("\"");

                if (isStringStart && isStringEnd) {
                    json = json.substring(1, json.length() - 1);
                } else {
                    throw new IllegalArgumentException("");
                }
            } else {
                throw new IllegalArgumentException("");
            }
        });
    }

    @Test
    @DisplayName("파라미터 변환 테스트 실패 케이스 Json -> String")
    public void parameter_Convert_Test_fail_wrong_Right_Quotation_mark_json2String() throws Exception{
        assertThrows(IllegalArgumentException.class, () -> {
            String json = "{\"123}";
            boolean isStart = json.startsWith("{");
            boolean isEnd = json.endsWith("}");

            if (isStart && isEnd) {
                json = json.substring(1, json.length() - 1);
                boolean isStringStart = json.startsWith("\"");
                boolean isStringEnd = json.endsWith("\"");

                if (isStringStart && isStringEnd) {
                    json = json.substring(1, json.length() - 1);
                } else {
                    throw new IllegalArgumentException("");
                }
            } else {
                throw new IllegalArgumentException("");
            }
        });
    }

    @Test
    @DisplayName("파라미터 변환 테스트 실패 케이스 Json -> String")
    public void parameter_Convert_Test_fail_wrong_Left_Quotation_mark_json2String() throws Exception{
        assertThrows(IllegalArgumentException.class, () -> {
            String json = "{123\"}";
            boolean isStart = json.startsWith("{");
            boolean isEnd = json.endsWith("}");

            if (isStart && isEnd) {
                json = json.substring(1, json.length() - 1);
                boolean isStringStart = json.startsWith("\"");
                boolean isStringEnd = json.endsWith("\"");

                if (isStringStart && isStringEnd) {
                    json = json.substring(1, json.length() - 1);
                } else {
                    throw new IllegalArgumentException("");
                }
            } else {
                throw new IllegalArgumentException("");
            }
        });
    }

    @Test
    @DisplayName("파라미터 변환 테스트 성공 케이스 Json -> String")
    public void parameter_Convert_Test_success_json2String() throws Exception{
        String json = "{\"123\"}";
        boolean isStart = json.startsWith("{");
        boolean isEnd = json.endsWith("}");

        if(isStart && isEnd){
            json = json.substring(1, json.length() - 1);
            boolean isStringStart = json.startsWith("\"");
            boolean isStringEnd = json.endsWith("\"");

            if (isStringStart && isStringEnd) {
                json = json.substring(1, json.length() - 1);
            }
        }

        assertEquals("123", json);
    }

    @Test
    @DisplayName("Json -> Class Convert Test")
    public void parameter_convert_test_success_json2Class() throws Exception{
        String json = "{\n" +
                "    \"name\" : \"test\",\n" +
                "    \"number\":\"123\",\n" +
                "    \"wrapperNumber\":\"1234\",\n" +
                "    \"list\" : [\n" +
                "        {\n" +
                "            \"test\":\"test\",\n" +
                "            \"title\":\"title\"\n" +
                "        },\n" +
                "        {\n" +
                "            \"test\":\"test1\",\n" +
                "            \"title\":\"title2\"\n" +
                "        }\n" +
                "    ],\n" +
                "    \"map\" : {\n" +
                "        \"test\":\"123\",\n" +
                "        \"test1\":\"456\"\n" +
                "    }\n" +
                "}";
        TestDto testDto = objectMapper.readValue(json, TestDto.class);

        assertEquals("test", testDto.getName());
        assertEquals(123, testDto.getNumber());
        assertEquals(1234, testDto.getWrapperNumber());
        assertEquals(2, testDto.getList().size());
        assertNotNull(testDto.getMap().get("test"));
        assertNotNull(testDto.getMap().get("test1"));
    }

    @Test
    @DisplayName("Json -> Class Convert Test Case 2")
    public void parameter_convert_test_success_json2Class_without_name_column() throws Exception{
        String json = "{\n" +
                "    \"number\":\"123\",\n" +
                "    \"wrapperNumber\":\"1234\",\n" +
                "    \"list\" : [\n" +
                "        {\n" +
                "            \"test\":\"test\",\n" +
                "            \"title\":\"title\"\n" +
                "        },\n" +
                "        {\n" +
                "            \"test\":\"test1\",\n" +
                "            \"title\":\"title2\"\n" +
                "        }\n" +
                "    ],\n" +
                "    \"map\" : {\n" +
                "        \"test\":\"123\",\n" +
                "        \"test1\":\"456\"\n" +
                "    }\n" +
                "}";
        TestDto testDto = objectMapper.readValue(json, TestDto.class);

        assertEquals(123, testDto.getNumber());
        assertEquals(1234, testDto.getWrapperNumber());
        assertEquals(2, testDto.getList().size());
        assertNotNull(testDto.getMap().get("test"));
        assertNotNull(testDto.getMap().get("test1"));
    }

    @Test
    @DisplayName("Json -> Class Convert Fail Test")
    public void parameter_convert_test_fail_json2Class() throws Exception{
        assertThrows(UnrecognizedPropertyException.class, () -> {
            String json = "{\n" +
                    "    \"number11\":\"123\",\n" +
                    "    \"wrapperNumber\":\"1234\",\n" +
                    "    \"list\" : [\n" +
                    "        {\n" +
                    "            \"test\":\"test\",\n" +
                    "            \"title\":\"title\"\n" +
                    "        },\n" +
                    "        {\n" +
                    "            \"test\":\"test1\",\n" +
                    "            \"title\":\"title2\"\n" +
                    "        }\n" +
                    "    ],\n" +
                    "    \"map\" : {\n" +
                    "        \"test\":\"123\",\n" +
                    "        \"test1\":\"456\"\n" +
                    "    }\n" +
                    "}";
            TestDto testDto = objectMapper.readValue(json, TestDto.class);
        });
    }
}

테스트 코드는 문제없이 통과한다.

그렇다면 이제 실제 요청이 들어왔을때도 파라미터 바인딩이 이루어지도록 로직을 변경한다.

 

CommonUtil.class

public class CommonUtil {
    public static final ObjectMapper objectMapper;

    static {
        objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
    }
}

 

HandlerMapping.class

현재는 Handler매핑과 파라미터 추출이라는 2가지역할을 한다.

이는 올바르지 않은 설계이다.

우선 파라미터 바인딩이 되는지 확인하고 역할, 책임을 분리하도록 하겠다.

public class HandlerMapping {

    Map<String, MethodInvoker> controller = new HashMap<>();
    Map<String, MethodInvoker> restController = new HashMap<>();

    public void init(){
        subDirList(MainApplication.class.getResource(".").getPath());
    }

    private void subDirList(String source) {
        File dir = new File(source);
        File[] fileList = dir.listFiles();

        if (fileList != null) {
            try {
                for (File file : fileList) {
                    if (file.isDirectory()) {
                        subDirList(file.getCanonicalPath());
                    } else if (file.isFile()) {
                        String path = file.getPath();

                        if (path.endsWith(".class")) {
                            int classes = path.lastIndexOf("classes");
                            String substring = path.substring(classes + 8);
                            String className = substring.split(".class")[0].replace("\\", ".");
                            Class<?> aClass = Class.forName(className);
                            if (aClass.isAnnotationPresent(Controller.class)) {
                                Method[] methods = aClass.getMethods();
                                Arrays.stream(methods)
                                        .forEach(m -> addPageHandler(aClass, m));
                            } else if (aClass.isAnnotationPresent(RestController.class)) {
                                Method[] methods = aClass.getMethods();
                                Arrays.stream(methods)
                                        .forEach(m -> addRestHandler(aClass, m));
                            }
                        }
                    }
                }
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    private void addPageHandler(Class<?> aClass, Method m) {
        if (m.isAnnotationPresent(PostMapping.class)) {
            addPostHandler(aClass, m, MethodType.PAGE);
        } else if (m.isAnnotationPresent(GetMapping.class)) {
            addGetMapping(aClass, m, MethodType.PAGE);
        }
    }

    private void addRestHandler(Class<?> aClass, Method m) {
        if (m.isAnnotationPresent(PostMapping.class)) {
            addPostHandler(aClass, m, MethodType.POST);
        } else if (m.isAnnotationPresent(GetMapping.class)) {
            addGetMapping(aClass, m, MethodType.GET);
        }
    }

    private void addPostHandler(Class<?> aClass, Method m, MethodType methodType){
        PostMapping declaredAnnotation = m.getDeclaredAnnotation(PostMapping.class);
        String value = declaredAnnotation.value();
        MethodInvoker methodInvoker = new MethodInvoker(value, methodType, aClass, m);

        if(MethodType.PAGE.equals(methodType)){
            controller.put(value, methodInvoker);
        }else{
            restController.put(value, methodInvoker);
        }
    }

    private void addGetMapping(Class<?> aClass, Method m, MethodType methodType){
        GetMapping declaredAnnotation = m.getDeclaredAnnotation(GetMapping.class);
        String value = declaredAnnotation.value();
        MethodInvoker methodInvoker = new MethodInvoker(value, methodType, aClass, m);

        if(MethodType.PAGE.equals(methodType)){
            controller.put(value, methodInvoker);
        }else{
            restController.put(value, methodInvoker);
        }
    }

    public Object invoke(HttpServletRequest request) {
        try{
            String requestURI = request.getRequestURI();

            if (isApiRequest(requestURI)) {
                MethodInvoker methodInvoker = restController.get(requestURI);
                return methodInvoker.invoke(getParameter(request, methodInvoker.getMethod()));
            } else if (isPageRequest(requestURI)) {
                MethodInvoker methodInvoker = controller.get(requestURI);
                return methodInvoker.invoke(getParameter(request, methodInvoker.getMethod()));
            }
        } catch (InvocationTargetException | IllegalAccessException | InstantiationException | IOException e) {
            e.printStackTrace();
        }

        return null;
    }

    public boolean isPageRequest(String requestURI) {
        return controller.containsKey(requestURI);
    }

    public boolean isApiRequest(String requestURI) {
        return restController.containsKey(requestURI);
    }

    private Object[] getParameter(HttpServletRequest request, Method method) throws IOException {
        Parameter[] parameters = method.getParameters();
        if (parameters.length == 0) {
            return new Object[0];
        }

        String body = getBody(request);
        Object[] result = new Object[1];
        Parameter parameter = parameters[0];
        result[0] = CommonUtil.objectMapper.readValue(body, parameter.getType());

        return result;
    }

    private String getBody(HttpServletRequest request) throws IOException {

        StringBuilder stringBuilder = new StringBuilder();

        try(InputStream inputStream = request.getInputStream()) {
            if (inputStream != null) {
                try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
                    char[] charBuffer = new char[128];
                    int bytesRead = -1;
                    while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                        stringBuilder.append(charBuffer, 0, bytesRead);
                    }
                }
            }
        }
        return stringBuilder.toString();
    }
}

RestTestController.class

@RestController
public class RestTestController {

    @PostMapping("/rest/posttest")
    public String restPostMapping(TestDto test){
        System.out.println(test);
        return "post";
    }

    @GetMapping("/rest/gettest")
    public String restGetMapping(){
        return "get";
    }

}

 

결과 확인

 

다음시간에는 역할, 책임별로 리팩토링을 진행 해보겠습니다.

반응형

댓글