반응형
소스주소 : 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";
}
}
결과 확인
다음시간에는 역할, 책임별로 리팩토링을 진행 해보겠습니다.
반응형
'개발 > JAVA' 카테고리의 다른 글
[JUnit] 테스트에 필요한 Request 객체 만들기 (0) | 2021.02.04 |
---|---|
[JAVA] Spring Controller를 직접 만들어보자 (5) - 리팩토링 (0) | 2021.01.30 |
[JAVA] Spring Controller를 직접 만들어보자 (3) - 핸들러 매핑 (0) | 2021.01.16 |
[JAVA] Spring Controller를 직접 만들어보자 (2) - 핸들러 등록 (0) | 2021.01.15 |
[JAVA] Spring Controller를 직접 만들어보자 (1) - 프로젝트 생성 (0) | 2021.01.12 |
댓글