From df6fa409b0e10dfbe0382c6d919424448fe3f6b4 Mon Sep 17 00:00:00 2001 From: SCY Date: Fri, 20 Sep 2024 20:51:35 +0900 Subject: [PATCH 01/21] =?UTF-8?q?test:=20=ED=95=99=EC=8A=B5=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20Reflection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/reflection/Junit3TestRunner.java | 15 ++++ .../java/reflection/Junit4TestRunner.java | 8 ++ .../test/java/reflection/ReflectionTest.java | 76 +++++++++++-------- .../test/java/reflection/ReflectionsTest.java | 11 +++ 4 files changed, 77 insertions(+), 33 deletions(-) diff --git a/study/src/test/java/reflection/Junit3TestRunner.java b/study/src/test/java/reflection/Junit3TestRunner.java index b4e465240c..151a0540ac 100644 --- a/study/src/test/java/reflection/Junit3TestRunner.java +++ b/study/src/test/java/reflection/Junit3TestRunner.java @@ -1,5 +1,8 @@ package reflection; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; import org.junit.jupiter.api.Test; class Junit3TestRunner { @@ -9,5 +12,17 @@ void run() throws Exception { Class clazz = Junit3Test.class; // TODO Junit3Test에서 test로 시작하는 메소드 실행 + Junit3Test junit3Test = clazz.getConstructor().newInstance(); + List testMethods = getDeclaredTestMethods(clazz); + for (Method method : testMethods) { + method.invoke(junit3Test); + } + } + + private List getDeclaredTestMethods(Class clazz) { + Method[] methods = clazz.getDeclaredMethods(); + return Arrays.stream(methods) + .filter(method -> method.getName().startsWith("test")) + .toList(); } } diff --git a/study/src/test/java/reflection/Junit4TestRunner.java b/study/src/test/java/reflection/Junit4TestRunner.java index 8a6916bc24..1c54f34111 100644 --- a/study/src/test/java/reflection/Junit4TestRunner.java +++ b/study/src/test/java/reflection/Junit4TestRunner.java @@ -1,5 +1,6 @@ package reflection; +import java.lang.reflect.Method; import org.junit.jupiter.api.Test; class Junit4TestRunner { @@ -9,5 +10,12 @@ void run() throws Exception { Class clazz = Junit4Test.class; // TODO Junit4Test에서 @MyTest 애노테이션이 있는 메소드 실행 + Junit4Test junit4Test = clazz.getConstructor().newInstance(); + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + if (method.isAnnotationPresent(MyTest.class)) { + method.invoke(junit4Test); + } + } } } diff --git a/study/src/test/java/reflection/ReflectionTest.java b/study/src/test/java/reflection/ReflectionTest.java index 370f0932b9..56ee26f075 100644 --- a/study/src/test/java/reflection/ReflectionTest.java +++ b/study/src/test/java/reflection/ReflectionTest.java @@ -1,15 +1,16 @@ package reflection; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import static org.assertj.core.api.Assertions.assertThat; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Date; import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; class ReflectionTest { @@ -19,36 +20,42 @@ class ReflectionTest { void givenObject_whenGetsClassName_thenCorrect() { final Class clazz = Question.class; - assertThat(clazz.getSimpleName()).isEqualTo(""); - assertThat(clazz.getName()).isEqualTo(""); - assertThat(clazz.getCanonicalName()).isEqualTo(""); + assertThat(clazz.getSimpleName()).isEqualTo("Question"); + assertThat(clazz.getName()).isEqualTo("reflection.Question"); + assertThat(clazz.getCanonicalName()).isEqualTo("reflection.Question"); } @Test void givenClassName_whenCreatesObject_thenCorrect() throws ClassNotFoundException { final Class clazz = Class.forName("reflection.Question"); - assertThat(clazz.getSimpleName()).isEqualTo(""); - assertThat(clazz.getName()).isEqualTo(""); - assertThat(clazz.getCanonicalName()).isEqualTo(""); + assertThat(clazz.getSimpleName()).isEqualTo("Question"); + assertThat(clazz.getName()).isEqualTo("reflection.Question"); + assertThat(clazz.getCanonicalName()).isEqualTo("reflection.Question"); } @Test void givenObject_whenGetsFieldNamesAtRuntime_thenCorrect() { final Object student = new Student(); - final Field[] fields = null; - final List actualFieldNames = null; - - assertThat(actualFieldNames).contains("name", "age"); + final Field[] fields = student.getClass().getDeclaredFields(); + final List actualFieldNames = Arrays.stream(fields) + .map(Field::getName) + .toList(); + + assertThat(actualFieldNames) + .hasSize(2) + .contains("name", "age"); } @Test void givenClass_whenGetsMethods_thenCorrect() { - final Class animalClass = Student.class; - final Method[] methods = null; - final List actualMethods = null; + final Class studentClass = Student.class; + final Method[] methods = studentClass.getDeclaredMethods(); + final List actualMethodNames = Arrays.stream(methods) + .map(Method::getName) + .toList(); - assertThat(actualMethods) + assertThat(actualMethodNames) .hasSize(3) .contains("getAge", "toString", "getName"); } @@ -56,20 +63,22 @@ void givenClass_whenGetsMethods_thenCorrect() { @Test void givenClass_whenGetsAllConstructors_thenCorrect() { final Class questionClass = Question.class; - final Constructor[] constructors = null; + final Constructor[] constructors = questionClass.getConstructors(); assertThat(constructors).hasSize(2); } @Test void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() throws Exception { - final Class questionClass = Question.class; + final Class questionClass = Question.class; - final Constructor firstConstructor = null; - final Constructor secondConstructor = null; + final Constructor firstConstructor = questionClass + .getConstructor(String.class, String.class, String.class); + final Constructor secondConstructor = questionClass + .getConstructor(long.class, String.class, String.class, String.class, Date.class, int.class); - final Question firstQuestion = null; - final Question secondQuestion = null; + final Question firstQuestion = firstConstructor.newInstance("gugu", "제목1", "내용1"); + final Question secondQuestion = secondConstructor.newInstance(1, "gugu", "제목2", "내용2", new Date(), 1); assertThat(firstQuestion.getWriter()).isEqualTo("gugu"); assertThat(firstQuestion.getTitle()).isEqualTo("제목1"); @@ -82,7 +91,7 @@ void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() throws Exception @Test void givenClass_whenGetsPublicFields_thenCorrect() { final Class questionClass = Question.class; - final Field[] fields = null; + final Field[] fields = questionClass.getFields(); // public fields assertThat(fields).hasSize(0); } @@ -90,7 +99,7 @@ void givenClass_whenGetsPublicFields_thenCorrect() { @Test void givenClass_whenGetsDeclaredFields_thenCorrect() { final Class questionClass = Question.class; - final Field[] fields = null; + final Field[] fields = questionClass.getDeclaredFields(); assertThat(fields).hasSize(6); assertThat(fields[0].getName()).isEqualTo("questionId"); @@ -99,7 +108,7 @@ void givenClass_whenGetsDeclaredFields_thenCorrect() { @Test void givenClass_whenGetsFieldsByName_thenCorrect() throws Exception { final Class questionClass = Question.class; - final Field field = null; + final Field field = questionClass.getDeclaredField("questionId"); assertThat(field.getName()).isEqualTo("questionId"); } @@ -107,23 +116,24 @@ void givenClass_whenGetsFieldsByName_thenCorrect() throws Exception { @Test void givenClassField_whenGetsType_thenCorrect() throws Exception { final Field field = Question.class.getDeclaredField("questionId"); - final Class fieldClass = null; + final Class fieldClass = field.getType(); // 필드의 타입 assertThat(fieldClass.getSimpleName()).isEqualTo("long"); } @Test void givenClassField_whenSetsAndGetsValue_thenCorrect() throws Exception { - final Class studentClass = Student.class; - final Student student = null; - final Field field = null; + final Class studentClass = Student.class; + final Student student = studentClass.getConstructor().newInstance(); + final Field field = studentClass.getDeclaredField("age"); // todo field에 접근 할 수 있도록 만든다. + field.setAccessible(true); assertThat(field.getInt(student)).isZero(); assertThat(student.getAge()).isZero(); - field.set(null, null); + field.set(student, 99); assertThat(field.getInt(student)).isEqualTo(99); assertThat(student.getAge()).isEqualTo(99); diff --git a/study/src/test/java/reflection/ReflectionsTest.java b/study/src/test/java/reflection/ReflectionsTest.java index 5040c2ffa2..1ad5b1aee8 100644 --- a/study/src/test/java/reflection/ReflectionsTest.java +++ b/study/src/test/java/reflection/ReflectionsTest.java @@ -1,9 +1,13 @@ package reflection; +import java.util.Set; import org.junit.jupiter.api.Test; import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reflection.annotation.Controller; +import reflection.annotation.Repository; +import reflection.annotation.Service; class ReflectionsTest { @@ -14,5 +18,12 @@ void showAnnotationClass() throws Exception { Reflections reflections = new Reflections("reflection.examples"); // TODO 클래스 레벨에 @Controller, @Service, @Repository 애노테이션이 설정되어 모든 클래스 찾아 로그로 출력한다. + Set> controllers = reflections.getTypesAnnotatedWith(Controller.class); + Set> services = reflections.getTypesAnnotatedWith(Service.class); + Set> repositories = reflections.getTypesAnnotatedWith(Repository.class); + + controllers.forEach(controller -> log.info(controller.getSimpleName())); + services.forEach(service -> log.info(service.getSimpleName())); + repositories.forEach(repository -> log.info(repository.getSimpleName())); } } From cb172db95c06b694618892b7588e47465195ef7b Mon Sep 17 00:00:00 2001 From: SCY Date: Fri, 20 Sep 2024 21:36:20 +0900 Subject: [PATCH 02/21] =?UTF-8?q?feat:=20=ED=95=B8=EB=93=A4=EB=9F=AC=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/bind/annotation/RequestMethod.java | 11 +++++- .../mvc/tobe/AnnotationHandlerMapping.java | 21 ++++++++--- .../servlet/mvc/tobe/HandlerExecution.java | 18 ++++++++-- .../bind/annotation/RequestMethodTest.java | 35 +++++++++++++++++++ mvc/src/test/java/samples/TestController.java | 8 ++--- 5 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 mvc/src/test/java/com/interface21/web/bind/annotation/RequestMethodTest.java diff --git a/mvc/src/main/java/com/interface21/web/bind/annotation/RequestMethod.java b/mvc/src/main/java/com/interface21/web/bind/annotation/RequestMethod.java index 3ffee4a52f..59aeea3ddd 100644 --- a/mvc/src/main/java/com/interface21/web/bind/annotation/RequestMethod.java +++ b/mvc/src/main/java/com/interface21/web/bind/annotation/RequestMethod.java @@ -1,5 +1,14 @@ package com.interface21.web.bind.annotation; +import java.util.Arrays; + public enum RequestMethod { - GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE; + + public static RequestMethod find(String raw) { + return Arrays.stream(values()) + .filter(value -> value.name().equals(raw)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException(String.format("유효하지 않은 요청 메서드입니다: %s", raw))); + } } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java index ccae5ea3ad..0fcfc375db 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -1,11 +1,11 @@ package com.interface21.webmvc.servlet.mvc.tobe; +import com.interface21.web.bind.annotation.RequestMethod; import jakarta.servlet.http.HttpServletRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.HashMap; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class AnnotationHandlerMapping { @@ -21,9 +21,22 @@ public AnnotationHandlerMapping(final Object... basePackage) { public void initialize() { log.info("Initialized AnnotationHandlerMapping!"); + handlerExecutions.put(new HandlerKey("/get-test", RequestMethod.GET), new HandlerExecution("/get-test.jsp")); + handlerExecutions.put(new HandlerKey("/post-test", RequestMethod.POST), new HandlerExecution("/post-test.jsp")); } public Object getHandler(final HttpServletRequest request) { - return null; + HandlerKey handlerKey = createHandlerKey(request); + HandlerExecution handlerExecution = handlerExecutions.get(handlerKey); + if (handlerExecution == null) { + throw new IllegalArgumentException("해당 요청에 대응하는 핸들러가 없습니다."); + } + return handlerExecution; + } + + private HandlerKey createHandlerKey(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + RequestMethod requestMethod = RequestMethod.find(request.getMethod()); + return new HandlerKey(requestURI, requestMethod); } } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java index 79a22355a9..8dcf58956c 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java @@ -1,12 +1,26 @@ package com.interface21.webmvc.servlet.mvc.tobe; +import com.interface21.webmvc.servlet.ModelAndView; +import com.interface21.webmvc.servlet.view.JspView; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import com.interface21.webmvc.servlet.ModelAndView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class HandlerExecution { + private static final Logger log = LoggerFactory.getLogger(HandlerExecution.class); + + private final String viewName; + + public HandlerExecution(String viewName) { + this.viewName = viewName; + } + public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { - return null; + log.info("handler execution handle"); + ModelAndView modelAndView = new ModelAndView(new JspView(viewName)); + modelAndView.addObject("id", request.getAttribute("id")); + return modelAndView; } } diff --git a/mvc/src/test/java/com/interface21/web/bind/annotation/RequestMethodTest.java b/mvc/src/test/java/com/interface21/web/bind/annotation/RequestMethodTest.java new file mode 100644 index 0000000000..4e241106e8 --- /dev/null +++ b/mvc/src/test/java/com/interface21/web/bind/annotation/RequestMethodTest.java @@ -0,0 +1,35 @@ +package com.interface21.web.bind.annotation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RequestMethodTest { + + @DisplayName("전달받은 문자열과 형태가 같은 요청 메서드를 반환한다.") + @Test + void should_findRequestMethod_when_givenValidString() { + // given + String raw = "GET"; + + // when + RequestMethod requestMethod = RequestMethod.find(raw); + + // then + assertThat(requestMethod).isEqualTo(RequestMethod.GET); + } + + @DisplayName("전달받은 문자열이 유효하지 않은 경우 예외가 발생한다.") + @Test + void should_throwException_when_givenInvalidString() { + // given + String raw = "INVALID"; + + // when & then + assertThatThrownBy(() -> RequestMethod.find(raw)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("유효하지 않은 요청 메서드입니다: " + raw); + } +} \ No newline at end of file diff --git a/mvc/src/test/java/samples/TestController.java b/mvc/src/test/java/samples/TestController.java index 6ad9c741dd..7ce0f4ea9b 100644 --- a/mvc/src/test/java/samples/TestController.java +++ b/mvc/src/test/java/samples/TestController.java @@ -16,17 +16,17 @@ public class TestController { private static final Logger log = LoggerFactory.getLogger(TestController.class); @RequestMapping(value = "/get-test", method = RequestMethod.GET) - public ModelAndView findUserId(final HttpServletRequest request, final HttpServletResponse response) { + public ModelAndView findUserId(HttpServletRequest request, HttpServletResponse response) { log.info("test controller get method"); - final var modelAndView = new ModelAndView(new JspView("")); + final ModelAndView modelAndView = new ModelAndView(new JspView("/get-test.jsp")); modelAndView.addObject("id", request.getAttribute("id")); return modelAndView; } @RequestMapping(value = "/post-test", method = RequestMethod.POST) - public ModelAndView save(final HttpServletRequest request, final HttpServletResponse response) { + public ModelAndView save(HttpServletRequest request, HttpServletResponse response) { log.info("test controller post method"); - final var modelAndView = new ModelAndView(new JspView("")); + final ModelAndView modelAndView = new ModelAndView(new JspView("/post-test.jsp")); modelAndView.addObject("id", request.getAttribute("id")); return modelAndView; } From 6e4c0e89a4d1b18de37a1bf939694ee3e986a051 Mon Sep 17 00:00:00 2001 From: SCY Date: Sat, 21 Sep 2024 00:13:40 +0900 Subject: [PATCH 03/21] =?UTF-8?q?feat:=20=EB=B7=B0=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?JspView=EB=A1=9C=20=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/techcourse/DispatcherServlet.java | 14 +++----------- .../interface21/webmvc/servlet/view/JspView.java | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/techcourse/DispatcherServlet.java b/app/src/main/java/com/techcourse/DispatcherServlet.java index 2943e9ae55..fa34a98164 100644 --- a/app/src/main/java/com/techcourse/DispatcherServlet.java +++ b/app/src/main/java/com/techcourse/DispatcherServlet.java @@ -4,6 +4,7 @@ import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.util.HashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.interface21.webmvc.servlet.view.JspView; @@ -32,20 +33,11 @@ protected void service(final HttpServletRequest request, final HttpServletRespon try { final var controller = manualHandlerMapping.getHandler(requestURI); final var viewName = controller.execute(request, response); - move(viewName, request, response); + JspView jspView = new JspView(viewName); + jspView.render(new HashMap<>(), request, response); } catch (Throwable e) { log.error("Exception : {}", e.getMessage(), e); throw new ServletException(e.getMessage()); } } - - private void move(final String viewName, final HttpServletRequest request, final HttpServletResponse response) throws Exception { - if (viewName.startsWith(JspView.REDIRECT_PREFIX)) { - response.sendRedirect(viewName.substring(JspView.REDIRECT_PREFIX.length())); - return; - } - - final var requestDispatcher = request.getRequestDispatcher(viewName); - requestDispatcher.forward(request, response); - } } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/view/JspView.java b/mvc/src/main/java/com/interface21/webmvc/servlet/view/JspView.java index 443fe4b4dd..3ca81cb6dd 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/view/JspView.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/view/JspView.java @@ -1,6 +1,7 @@ package com.interface21.webmvc.servlet.view; import com.interface21.webmvc.servlet.View; +import jakarta.servlet.RequestDispatcher; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; @@ -14,18 +15,27 @@ public class JspView implements View { public static final String REDIRECT_PREFIX = "redirect:"; + private final String viewName; + public JspView(final String viewName) { + this.viewName = viewName; } @Override public void render(final Map model, final HttpServletRequest request, final HttpServletResponse response) throws Exception { - // todo + log.info("jsp view render"); model.keySet().forEach(key -> { log.debug("attribute name : {}, value : {}", key, model.get(key)); request.setAttribute(key, model.get(key)); }); - // todo + if (viewName.startsWith(REDIRECT_PREFIX)) { + response.sendRedirect(viewName.substring(REDIRECT_PREFIX.length())); + return; + } + + RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewName); + requestDispatcher.forward(request, response); } } From f0cda4b0b092378964f258af8120b1a4c4d9812a Mon Sep 17 00:00:00 2001 From: SCY Date: Sat, 21 Sep 2024 01:49:09 +0900 Subject: [PATCH 04/21] =?UTF-8?q?refactor:=20=ED=95=B8=EB=93=A4=EB=9F=AC?= =?UTF-8?q?=20=EB=A7=A4=ED=95=91=20reflection=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvc/tobe/AnnotationHandlerMapping.java | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java index 0fcfc375db..639e653f45 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -1,9 +1,15 @@ package com.interface21.webmvc.servlet.mvc.tobe; +import com.interface21.context.stereotype.Controller; +import com.interface21.web.bind.annotation.RequestMapping; import com.interface21.web.bind.annotation.RequestMethod; import jakarta.servlet.http.HttpServletRequest; +import java.lang.reflect.Method; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,6 +17,8 @@ public class AnnotationHandlerMapping { private static final Logger log = LoggerFactory.getLogger(AnnotationHandlerMapping.class); + private static final String VIEW_EXTENSION = ".jsp"; + private final Object[] basePackage; private final Map handlerExecutions; @@ -21,8 +29,32 @@ public AnnotationHandlerMapping(final Object... basePackage) { public void initialize() { log.info("Initialized AnnotationHandlerMapping!"); - handlerExecutions.put(new HandlerKey("/get-test", RequestMethod.GET), new HandlerExecution("/get-test.jsp")); - handlerExecutions.put(new HandlerKey("/post-test", RequestMethod.POST), new HandlerExecution("/post-test.jsp")); + Reflections reflections = new Reflections(basePackage); + Set> controllers = reflections.getTypesAnnotatedWith(Controller.class); + controllers.stream() + .map(Class::getDeclaredMethods) + .forEach(this::mapHandlerToExecution); + } + + private void mapHandlerToExecution(Method[] handlers) { + Arrays.stream(handlers) + .filter(handler -> handler.isAnnotationPresent(RequestMapping.class)) + .map(handler -> handler.getAnnotation(RequestMapping.class)) + .forEach(this::addMapper); + } + + private void addMapper(RequestMapping requestMapping) { + String uri = requestMapping.value(); + RequestMethod[] requestMethods = requestMapping.method(); // TODO: method가 설정되어있지 않으면 모든 method 매퍼 지원! + Arrays.stream(requestMethods) + .forEach(requestMethod -> addHandlerExecution(requestMethod, uri)); + } + + private void addHandlerExecution(RequestMethod requestMethod, String uri) { + String viewName = uri + VIEW_EXTENSION; + HandlerKey handlerKey = new HandlerKey(uri, requestMethod); + HandlerExecution handlerExecution = new HandlerExecution(viewName); + handlerExecutions.put(handlerKey, handlerExecution); } public Object getHandler(final HttpServletRequest request) { From 2ecca4687a7c11bd750ffd5bca1773d61c504696 Mon Sep 17 00:00:00 2001 From: SCY Date: Sat, 21 Sep 2024 01:59:16 +0900 Subject: [PATCH 05/21] =?UTF-8?q?feat:=20RequestMapping=20method=20?= =?UTF-8?q?=EC=84=A0=EC=96=B8=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EB=AA=A8=EB=93=A0=20HTTP=20method=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvc/tobe/AnnotationHandlerMapping.java | 6 ++++- .../tobe/AnnotationHandlerMappingTest.java | 24 ++++++++++++++++++- mvc/src/test/java/samples/TestController.java | 8 +++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java index 639e653f45..950c78d4dd 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -17,6 +17,7 @@ public class AnnotationHandlerMapping { private static final Logger log = LoggerFactory.getLogger(AnnotationHandlerMapping.class); + private static final int EMPTY_REQUEST_METHODS = 0; private static final String VIEW_EXTENSION = ".jsp"; private final Object[] basePackage; @@ -45,7 +46,10 @@ private void mapHandlerToExecution(Method[] handlers) { private void addMapper(RequestMapping requestMapping) { String uri = requestMapping.value(); - RequestMethod[] requestMethods = requestMapping.method(); // TODO: method가 설정되어있지 않으면 모든 method 매퍼 지원! + RequestMethod[] requestMethods = requestMapping.method(); + if (requestMethods.length == EMPTY_REQUEST_METHODS) { + requestMethods = RequestMethod.values(); + } Arrays.stream(requestMethods) .forEach(requestMethod -> addHandlerExecution(requestMethod, uri)); } diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index a19ea6e102..f83eced6b1 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java @@ -3,7 +3,10 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -20,7 +23,7 @@ void setUp() { } @Test - void get() throws Exception { + void get() throws Exception { // todo: displayName final var request = mock(HttpServletRequest.class); final var response = mock(HttpServletResponse.class); @@ -48,4 +51,23 @@ void post() throws Exception { assertThat(modelAndView.getObject("id")).isEqualTo("gugu"); } + + // todo 다중 method + + @DisplayName("핸들러에 http method가 선언되지 않은 경우 모든 method가 매핑되도록 한다.") + @ParameterizedTest + @ValueSource(strings = {"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "TRACE"}) + void should_mapAllMethods_when_declaredMethodEmpty(String method) throws Exception { + final var request = mock(HttpServletRequest.class); + final var response = mock(HttpServletResponse.class); + + when(request.getAttribute("id")).thenReturn("gugu"); + when(request.getRequestURI()).thenReturn("/all-test"); + when(request.getMethod()).thenReturn(method); + + final var handlerExecution = (HandlerExecution) handlerMapping.getHandler(request); + final var modelAndView = handlerExecution.handle(request, response); + + assertThat(modelAndView.getObject("id")).isEqualTo("gugu"); + } } diff --git a/mvc/src/test/java/samples/TestController.java b/mvc/src/test/java/samples/TestController.java index 7ce0f4ea9b..bf125171f4 100644 --- a/mvc/src/test/java/samples/TestController.java +++ b/mvc/src/test/java/samples/TestController.java @@ -30,4 +30,12 @@ public ModelAndView save(HttpServletRequest request, HttpServletResponse respons modelAndView.addObject("id", request.getAttribute("id")); return modelAndView; } + + @RequestMapping(value = "/all-test") + public ModelAndView all(HttpServletRequest request, HttpServletResponse response) { + log.info("test controller all method"); + final ModelAndView modelAndView = new ModelAndView(new JspView("")); + modelAndView.addObject("id", request.getAttribute("id")); + return modelAndView; + } } From e3126649bd477dc8eb8260996b88cdad29b002b7 Mon Sep 17 00:00:00 2001 From: SCY Date: Sat, 21 Sep 2024 02:07:48 +0900 Subject: [PATCH 06/21] =?UTF-8?q?test:=20=EC=9C=A0=ED=9A=A8=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EC=9A=94=EC=B2=AD=EC=9D=98=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EC=98=88=EC=99=B8=20=EB=B0=9C=EC=83=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvc/tobe/AnnotationHandlerMapping.java | 3 ++- .../tobe/AnnotationHandlerMappingTest.java | 21 ++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java index 950c78d4dd..f9fe1cce81 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -65,7 +65,8 @@ public Object getHandler(final HttpServletRequest request) { HandlerKey handlerKey = createHandlerKey(request); HandlerExecution handlerExecution = handlerExecutions.get(handlerKey); if (handlerExecution == null) { - throw new IllegalArgumentException("해당 요청에 대응하는 핸들러가 없습니다."); + throw new IllegalArgumentException( + String.format("해당 요청에 대응하는 핸들러가 없습니다: %s %s", request.getMethod(), request.getRequestURI())); } return handlerExecution; } diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index f83eced6b1..50fc800fe7 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -22,8 +23,9 @@ void setUp() { handlerMapping.initialize(); } + @DisplayName("해당 uri와 method get에 매핑되는 핸들러를 찾아 실행한다.") @Test - void get() throws Exception { // todo: displayName + void should_handleHandler_when_getRequest() throws Exception { final var request = mock(HttpServletRequest.class); final var response = mock(HttpServletResponse.class); @@ -37,8 +39,9 @@ void get() throws Exception { // todo: displayName assertThat(modelAndView.getObject("id")).isEqualTo("gugu"); } + @DisplayName("해당 uri와 method post에 매핑되는 핸들러를 찾아 실행한다.") @Test - void post() throws Exception { + void should_handleHandler_when_postRequest() throws Exception { final var request = mock(HttpServletRequest.class); final var response = mock(HttpServletResponse.class); @@ -52,7 +55,19 @@ void post() throws Exception { assertThat(modelAndView.getObject("id")).isEqualTo("gugu"); } - // todo 다중 method + @DisplayName("해당 uri와 method에 매핑되는 핸들러가 없는 경우 예외가 발생한다.") + @Test + void should_throwException_when_givenInvalidRequest() throws Exception { + final var request = mock(HttpServletRequest.class); + + when(request.getAttribute("id")).thenReturn("gugu"); + when(request.getRequestURI()).thenReturn("/get-test"); + when(request.getMethod()).thenReturn("POST"); + + assertThatThrownBy(() -> handlerMapping.getHandler(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("해당 요청에 대응하는 핸들러가 없습니다: POST /get-test"); + } @DisplayName("핸들러에 http method가 선언되지 않은 경우 모든 method가 매핑되도록 한다.") @ParameterizedTest From 4e45a35a375fdc4cfe8f4f9bedcb0acd71ef6667 Mon Sep 17 00:00:00 2001 From: SCY Date: Sat, 21 Sep 2024 02:25:00 +0900 Subject: [PATCH 07/21] =?UTF-8?q?refactor:=20=ED=95=B8=EB=93=A4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20reflection=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvc/tobe/AnnotationHandlerMapping.java | 18 ++++++------------ .../servlet/mvc/tobe/HandlerExecution.java | 14 ++++++++------ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java index f9fe1cce81..9f50b85ccb 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -18,7 +18,6 @@ public class AnnotationHandlerMapping { private static final Logger log = LoggerFactory.getLogger(AnnotationHandlerMapping.class); private static final int EMPTY_REQUEST_METHODS = 0; - private static final String VIEW_EXTENSION = ".jsp"; private final Object[] basePackage; private final Map handlerExecutions; @@ -40,25 +39,20 @@ public void initialize() { private void mapHandlerToExecution(Method[] handlers) { Arrays.stream(handlers) .filter(handler -> handler.isAnnotationPresent(RequestMapping.class)) - .map(handler -> handler.getAnnotation(RequestMapping.class)) - .forEach(this::addMapper); + .forEach(handler -> { + HandlerExecution handlerExecution = new HandlerExecution(handler); + addMapper(handler.getAnnotation(RequestMapping.class), handlerExecution); + }); } - private void addMapper(RequestMapping requestMapping) { + private void addMapper(RequestMapping requestMapping, HandlerExecution handlerExecution) { String uri = requestMapping.value(); RequestMethod[] requestMethods = requestMapping.method(); if (requestMethods.length == EMPTY_REQUEST_METHODS) { requestMethods = RequestMethod.values(); } Arrays.stream(requestMethods) - .forEach(requestMethod -> addHandlerExecution(requestMethod, uri)); - } - - private void addHandlerExecution(RequestMethod requestMethod, String uri) { - String viewName = uri + VIEW_EXTENSION; - HandlerKey handlerKey = new HandlerKey(uri, requestMethod); - HandlerExecution handlerExecution = new HandlerExecution(viewName); - handlerExecutions.put(handlerKey, handlerExecution); + .forEach(requestMethod -> handlerExecutions.put(new HandlerKey(uri, requestMethod), handlerExecution)); } public Object getHandler(final HttpServletRequest request) { diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java index 8dcf58956c..7ad26c2505 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java @@ -4,6 +4,7 @@ import com.interface21.webmvc.servlet.view.JspView; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,16 +12,17 @@ public class HandlerExecution { private static final Logger log = LoggerFactory.getLogger(HandlerExecution.class); - private final String viewName; + private final Method method; - public HandlerExecution(String viewName) { - this.viewName = viewName; + public HandlerExecution(Method method) { + this.method = method; } public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { log.info("handler execution handle"); - ModelAndView modelAndView = new ModelAndView(new JspView(viewName)); - modelAndView.addObject("id", request.getAttribute("id")); - return modelAndView; + Object declaringInstance = method.getDeclaringClass() + .getConstructor() + .newInstance(); + return (ModelAndView) method.invoke(declaringInstance, request, response); } } From e76390e3d480ea85e1b31e9e699cb47431d6d264 Mon Sep 17 00:00:00 2001 From: SCY Date: Sat, 21 Sep 2024 02:33:49 +0900 Subject: [PATCH 08/21] =?UTF-8?q?refactor:=20DispatcherServlet=20service?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20ModelAndView=EB=A5=BC=20?= =?UTF-8?q?=EB=8B=A4=EB=A3=A8=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/techcourse/DispatcherServlet.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/techcourse/DispatcherServlet.java b/app/src/main/java/com/techcourse/DispatcherServlet.java index fa34a98164..3192c43b54 100644 --- a/app/src/main/java/com/techcourse/DispatcherServlet.java +++ b/app/src/main/java/com/techcourse/DispatcherServlet.java @@ -1,5 +1,7 @@ package com.techcourse; +import com.interface21.webmvc.servlet.ModelAndView; +import com.interface21.webmvc.servlet.View; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; @@ -33,8 +35,9 @@ protected void service(final HttpServletRequest request, final HttpServletRespon try { final var controller = manualHandlerMapping.getHandler(requestURI); final var viewName = controller.execute(request, response); - JspView jspView = new JspView(viewName); - jspView.render(new HashMap<>(), request, response); + ModelAndView modelAndView = new ModelAndView(new JspView(viewName)); + View view = modelAndView.getView(); + view.render(modelAndView.getModel(), request, response); } catch (Throwable e) { log.error("Exception : {}", e.getMessage(), e); throw new ServletException(e.getMessage()); From aa674d8a01eb998d16e30eef813183410440a19b Mon Sep 17 00:00:00 2001 From: SCY Date: Sat, 21 Sep 2024 02:34:38 +0900 Subject: [PATCH 09/21] =?UTF-8?q?style:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20import=EB=AC=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/techcourse/DispatcherServlet.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/techcourse/DispatcherServlet.java b/app/src/main/java/com/techcourse/DispatcherServlet.java index 3192c43b54..930562235b 100644 --- a/app/src/main/java/com/techcourse/DispatcherServlet.java +++ b/app/src/main/java/com/techcourse/DispatcherServlet.java @@ -2,14 +2,13 @@ import com.interface21.webmvc.servlet.ModelAndView; import com.interface21.webmvc.servlet.View; +import com.interface21.webmvc.servlet.view.JspView; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.util.HashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.interface21.webmvc.servlet.view.JspView; public class DispatcherServlet extends HttpServlet { @@ -28,7 +27,8 @@ public void init() { } @Override - protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException { + protected void service(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException { final String requestURI = request.getRequestURI(); log.debug("Method : {}, Request URI : {}", request.getMethod(), requestURI); From d4d274b6b0d35b138db600c0f7f7efd1a56b16dc Mon Sep 17 00:00:00 2001 From: SCY Date: Sun, 22 Sep 2024 17:49:27 +0900 Subject: [PATCH 10/21] =?UTF-8?q?refactor:=20HandlerKey=20=EC=BA=90?= =?UTF-8?q?=EC=8B=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvc/tobe/AnnotationHandlerMapping.java | 4 +- .../webmvc/servlet/mvc/tobe/HandlerKey.java | 47 +++++++++++----- .../servlet/mvc/tobe/HandlerKeyTest.java | 54 +++++++++++++++++++ 3 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java index 9f50b85ccb..7f6eb961d3 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -52,7 +52,7 @@ private void addMapper(RequestMapping requestMapping, HandlerExecution handlerEx requestMethods = RequestMethod.values(); } Arrays.stream(requestMethods) - .forEach(requestMethod -> handlerExecutions.put(new HandlerKey(uri, requestMethod), handlerExecution)); + .forEach(requestMethod -> handlerExecutions.put(HandlerKey.from(uri, requestMethod), handlerExecution)); } public Object getHandler(final HttpServletRequest request) { @@ -68,6 +68,6 @@ public Object getHandler(final HttpServletRequest request) { private HandlerKey createHandlerKey(HttpServletRequest request) { String requestURI = request.getRequestURI(); RequestMethod requestMethod = RequestMethod.find(request.getMethod()); - return new HandlerKey(requestURI, requestMethod); + return HandlerKey.from(requestURI, requestMethod); } } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java index 55f8e593a1..35d3c4ccfe 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java @@ -1,37 +1,56 @@ package com.interface21.webmvc.servlet.mvc.tobe; import com.interface21.web.bind.annotation.RequestMethod; - +import java.util.ArrayList; +import java.util.List; import java.util.Objects; public class HandlerKey { - private final String url; - private final RequestMethod requestMethod; + private static final List CACHE = new ArrayList<>(); + + public final String url; + public final RequestMethod requestMethod; - public HandlerKey(final String url, final RequestMethod requestMethod) { + public static HandlerKey from(String url, RequestMethod requestMethod) { + return CACHE.stream() + .filter(handlerKey -> handlerKey.url.equals(url) && handlerKey.requestMethod == requestMethod) + .findAny() + .orElseGet(() -> new HandlerKey(url, requestMethod)); + } + + private HandlerKey(final String url, final RequestMethod requestMethod) { this.url = url; this.requestMethod = requestMethod; + CACHE.add(this); } - @Override - public String toString() { - return "HandlerKey{" + - "url='" + url + '\'' + - ", requestMethod=" + requestMethod + - '}'; + protected static void cleanCache() { + CACHE.clear(); } @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof HandlerKey)) return false; - HandlerKey that = (HandlerKey) o; - return Objects.equals(url, that.url) && requestMethod == that.requestMethod; + if (this == o) { + return true; + } + if (!(o instanceof HandlerKey)) { + return false; + } + HandlerKey handlerKey = (HandlerKey) o; + return Objects.equals(url, handlerKey.url) && requestMethod == handlerKey.requestMethod; } @Override public int hashCode() { return Objects.hash(url, requestMethod); } + + @Override + public String toString() { + return "HandlerKey{" + + "url='" + url + '\'' + + ", requestMethod=" + requestMethod + + '}'; + } } diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java new file mode 100644 index 0000000000..c7fba1017b --- /dev/null +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java @@ -0,0 +1,54 @@ +package com.interface21.webmvc.servlet.mvc.tobe; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.interface21.web.bind.annotation.RequestMethod; +import java.lang.reflect.Field; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HandlerKeyTest { // TODO: 캐시 테스트 더 좋은 방식 없나? 질문하기 + + @BeforeEach + void setUp() { + HandlerKey.cleanCache(); + } + + @DisplayName("캐시에 존재하지 않는 HandlerKey 생성을 시도하는 경우 해당 인스턴스를 캐시에 추가 후 반환한다.") + @Test + void should_addInCache_when_createNewHandlerKey() { + // given & when + HandlerKey.from("/new", RequestMethod.GET); + + // then + assertThat(getCacheSize()).isEqualTo(1); + } + + @DisplayName("캐시에 존재하는 HandlerKey 생성을 시도하는 경우 캐시 내 인스턴스를 반환한다.") + @Test + void should_getInCache_when_createExistHandlerKey() { + // given + HandlerKey exist = HandlerKey.from("/url", RequestMethod.GET); + + // when + HandlerKey created = HandlerKey.from("/url", RequestMethod.GET); + + // then + assertThat(created).isEqualTo(exist); + assertThat(getCacheSize()).isEqualTo(1); + } + + private int getCacheSize() { + try { + Field cacheField = HandlerKey.class.getDeclaredField("CACHE"); + cacheField.setAccessible(true); + List cache = (List) cacheField.get(null); + return cache.size(); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + throw new RuntimeException("CACHE 사이즈에 접근할 수 없습니다."); + } + } +} From 0118167ca35ed9b53c3694b90e3ef6fe418f8d60 Mon Sep 17 00:00:00 2001 From: SCY Date: Sun, 22 Sep 2024 20:08:59 +0900 Subject: [PATCH 11/21] =?UTF-8?q?refactor:=20HandlerExecution=20=EB=82=B4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=A7=A4=EB=B2=88=20=EC=9D=B8=EC=8A=A4?= =?UTF-8?q?=ED=84=B4=EC=8A=A4=20=EC=83=9D=EC=84=B1=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvc/tobe/AnnotationHandlerMapping.java | 37 +++++++++++++------ .../servlet/mvc/tobe/HandlerExecution.java | 14 +++---- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java index 7f6eb961d3..32ea39058f 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -7,6 +7,7 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import org.reflections.Reflections; @@ -30,29 +31,41 @@ public AnnotationHandlerMapping(final Object... basePackage) { public void initialize() { log.info("Initialized AnnotationHandlerMapping!"); Reflections reflections = new Reflections(basePackage); - Set> controllers = reflections.getTypesAnnotatedWith(Controller.class); - controllers.stream() - .map(Class::getDeclaredMethods) - .forEach(this::mapHandlerToExecution); + Set> controllerTypes = reflections.getTypesAnnotatedWith(Controller.class); + controllerTypes.forEach(this::mapControllerHandlers); } - private void mapHandlerToExecution(Method[] handlers) { + private void mapControllerHandlers(Class controllerType) { + try { + Object controller = controllerType.getConstructor().newInstance(); + Method[] handlers = controllerType.getDeclaredMethods(); + mapHandlerToExecution(controller, handlers); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void mapHandlerToExecution(Object controller, Method[] handlers) { Arrays.stream(handlers) .filter(handler -> handler.isAnnotationPresent(RequestMapping.class)) - .forEach(handler -> { - HandlerExecution handlerExecution = new HandlerExecution(handler); - addMapper(handler.getAnnotation(RequestMapping.class), handlerExecution); - }); + .forEach(handler -> addMapper(handler, new HandlerExecution(controller, handler))); + } + + private void addMapper(Method handler, HandlerExecution handlerExecution) { + RequestMapping requestMapping = handler.getAnnotation(RequestMapping.class); + List handlerKeys = createHandlerKeys(requestMapping); + handlerKeys.forEach(handlerKey -> handlerExecutions.put(handlerKey, handlerExecution)); } - private void addMapper(RequestMapping requestMapping, HandlerExecution handlerExecution) { + private List createHandlerKeys(RequestMapping requestMapping) { String uri = requestMapping.value(); RequestMethod[] requestMethods = requestMapping.method(); if (requestMethods.length == EMPTY_REQUEST_METHODS) { requestMethods = RequestMethod.values(); } - Arrays.stream(requestMethods) - .forEach(requestMethod -> handlerExecutions.put(HandlerKey.from(uri, requestMethod), handlerExecution)); + return Arrays.stream(requestMethods) + .map(requestMethod -> HandlerKey.from(uri, requestMethod)) + .toList(); } public Object getHandler(final HttpServletRequest request) { diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java index 7ad26c2505..14ab0d60a7 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java @@ -1,7 +1,6 @@ package com.interface21.webmvc.servlet.mvc.tobe; import com.interface21.webmvc.servlet.ModelAndView; -import com.interface21.webmvc.servlet.view.JspView; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.lang.reflect.Method; @@ -12,17 +11,16 @@ public class HandlerExecution { private static final Logger log = LoggerFactory.getLogger(HandlerExecution.class); - private final Method method; + private final Object controller; + private final Method handler; - public HandlerExecution(Method method) { - this.method = method; + public HandlerExecution(Object controller, Method handler) { + this.controller = controller; + this.handler = handler; } public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { log.info("handler execution handle"); - Object declaringInstance = method.getDeclaringClass() - .getConstructor() - .newInstance(); - return (ModelAndView) method.invoke(declaringInstance, request, response); + return (ModelAndView) handler.invoke(controller, request, response); } } From ba17506f67cecc427d9c37051f8d095faabb55c0 Mon Sep 17 00:00:00 2001 From: SCY Date: Sun, 22 Sep 2024 20:39:58 +0900 Subject: [PATCH 12/21] =?UTF-8?q?test:=20EnumSource=20=ED=99=9C=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20?= =?UTF-8?q?=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tobe/AnnotationHandlerMappingTest.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index 50fc800fe7..7857baf853 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java @@ -1,17 +1,18 @@ package com.interface21.webmvc.servlet.mvc.tobe; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.interface21.web.bind.annotation.RequestMethod; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import org.junit.jupiter.params.provider.EnumSource; class AnnotationHandlerMappingTest { @@ -71,14 +72,14 @@ void should_throwException_when_givenInvalidRequest() throws Exception { @DisplayName("핸들러에 http method가 선언되지 않은 경우 모든 method가 매핑되도록 한다.") @ParameterizedTest - @ValueSource(strings = {"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "TRACE"}) - void should_mapAllMethods_when_declaredMethodEmpty(String method) throws Exception { + @EnumSource(value = RequestMethod.class) + void should_mapAllMethods_when_declaredMethodEmpty(RequestMethod method) throws Exception { final var request = mock(HttpServletRequest.class); final var response = mock(HttpServletResponse.class); when(request.getAttribute("id")).thenReturn("gugu"); when(request.getRequestURI()).thenReturn("/all-test"); - when(request.getMethod()).thenReturn(method); + when(request.getMethod()).thenReturn(method.name()); final var handlerExecution = (HandlerExecution) handlerMapping.getHandler(request); final var modelAndView = handlerExecution.handle(request, response); From de7370e047c3e0ff049f44a63ac4de3fc68f0077 Mon Sep 17 00:00:00 2001 From: SCY Date: Sun, 22 Sep 2024 21:03:16 +0900 Subject: [PATCH 13/21] =?UTF-8?q?test:=20getMethods=20vs=20getDeclaredMeth?= =?UTF-8?q?ods=20=ED=95=99=EC=8A=B5=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/test/java/reflection/Junit3Test.java | 2 +- .../java/reflection/Junit3TestRunner.java | 50 +++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/study/src/test/java/reflection/Junit3Test.java b/study/src/test/java/reflection/Junit3Test.java index 2fdf586e01..de7cad07df 100644 --- a/study/src/test/java/reflection/Junit3Test.java +++ b/study/src/test/java/reflection/Junit3Test.java @@ -9,7 +9,7 @@ public void test2() throws Exception { System.out.println("Running Test2"); } - public void three() throws Exception { + private void privateTest3() throws Exception { System.out.println("Running Test3"); } } diff --git a/study/src/test/java/reflection/Junit3TestRunner.java b/study/src/test/java/reflection/Junit3TestRunner.java index 151a0540ac..f9215d323a 100644 --- a/study/src/test/java/reflection/Junit3TestRunner.java +++ b/study/src/test/java/reflection/Junit3TestRunner.java @@ -1,28 +1,72 @@ package reflection; +import static org.assertj.core.api.Assertions.assertThat; + import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class Junit3TestRunner { + Class clazz = Junit3Test.class; + @Test void run() throws Exception { - Class clazz = Junit3Test.class; - // TODO Junit3Test에서 test로 시작하는 메소드 실행 Junit3Test junit3Test = clazz.getConstructor().newInstance(); List testMethods = getDeclaredTestMethods(clazz); + assertThat(testMethods).hasSize(2); for (Method method : testMethods) { method.invoke(junit3Test); } } private List getDeclaredTestMethods(Class clazz) { - Method[] methods = clazz.getDeclaredMethods(); + Method[] methods = clazz.getMethods(); return Arrays.stream(methods) .filter(method -> method.getName().startsWith("test")) .toList(); } + + @DisplayName("getMethods: 상속 메서드를 포함한 모든 public 메서드를 반환한다.") + @Test + void getMethods() { + // given & when + Method[] methods = clazz.getMethods(); + + // then + List actual = Arrays.stream(methods) + .map(Method::getName) + .toList(); + assertThat(actual).containsExactly( + "test1", + "test2", + "equals", + "toString", + "hashCode", + "getClass", + "notify", + "notifyAll", + "wait", + "wait", + "wait"); + } + + @DisplayName("getDeclaredMethods: 상속 메서드를 제외하고 직접 선언된 모든 메서드를 반환한다.") + @Test + void getDeclaredMethods() { + // given & when + Method[] methods = clazz.getDeclaredMethods(); + + // then + List actual = Arrays.stream(methods) + .map(Method::getName) + .toList(); + assertThat(actual).containsExactly( + "test1", + "test2", + "privateTest3"); + } } From 613a0c1d4d7d1724d2150774d63438a4ad9b0077 Mon Sep 17 00:00:00 2001 From: SCY Date: Sun, 22 Sep 2024 21:29:59 +0900 Subject: [PATCH 14/21] =?UTF-8?q?style:=20=EC=9E=AC=EC=A0=95=EC=9D=98=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=9C=EC=84=9C=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- study/src/test/java/reflection/Question.java | 32 ++++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/study/src/test/java/reflection/Question.java b/study/src/test/java/reflection/Question.java index 2b20c67659..452d7727b9 100644 --- a/study/src/test/java/reflection/Question.java +++ b/study/src/test/java/reflection/Question.java @@ -64,6 +64,25 @@ public void update(Question newQuestion) { this.contents = newQuestion.contents; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Question)) { + return false; + } + Question question = (Question) o; + return questionId == question.questionId && countOfComment == question.countOfComment && Objects.equals(writer, + question.writer) && Objects.equals(title, question.title) && Objects.equals(contents, question.contents) + && Objects.equals(createdDate, question.createdDate); + } + + @Override + public int hashCode() { + return Objects.hash(questionId, writer, title, contents, createdDate, countOfComment); + } + @Override public String toString() { return "Question{" + @@ -75,17 +94,4 @@ public String toString() { ", countOfComment=" + countOfComment + '}'; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Question)) return false; - Question question = (Question) o; - return questionId == question.questionId && countOfComment == question.countOfComment && Objects.equals(writer, question.writer) && Objects.equals(title, question.title) && Objects.equals(contents, question.contents) && Objects.equals(createdDate, question.createdDate); - } - - @Override - public int hashCode() { - return Objects.hash(questionId, writer, title, contents, createdDate, countOfComment); - } } From 5b297ceb5505a4bfe2ad0f25ea936f206b90f494 Mon Sep 17 00:00:00 2001 From: SCY Date: Sun, 22 Sep 2024 21:30:10 +0900 Subject: [PATCH 15/21] style: EOF --- .../com/interface21/web/bind/annotation/RequestMethodTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mvc/src/test/java/com/interface21/web/bind/annotation/RequestMethodTest.java b/mvc/src/test/java/com/interface21/web/bind/annotation/RequestMethodTest.java index 4e241106e8..f8e6d640eb 100644 --- a/mvc/src/test/java/com/interface21/web/bind/annotation/RequestMethodTest.java +++ b/mvc/src/test/java/com/interface21/web/bind/annotation/RequestMethodTest.java @@ -32,4 +32,4 @@ void should_throwException_when_givenInvalidString() { .isInstanceOf(IllegalArgumentException.class) .hasMessage("유효하지 않은 요청 메서드입니다: " + raw); } -} \ No newline at end of file +} From 54f3b368509df8b6dce02eef4800674e0ed07187 Mon Sep 17 00:00:00 2001 From: SCY Date: Sun, 22 Sep 2024 21:53:46 +0900 Subject: [PATCH 16/21] =?UTF-8?q?test:=20=ED=95=99=EC=8A=B5=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20Servlet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/servlet/com/example/ServletTest.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/study/src/test/java/servlet/com/example/ServletTest.java b/study/src/test/java/servlet/com/example/ServletTest.java index 75fbb10dd5..36a262fce4 100644 --- a/study/src/test/java/servlet/com/example/ServletTest.java +++ b/study/src/test/java/servlet/com/example/ServletTest.java @@ -1,10 +1,10 @@ package servlet.com.example; +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.jupiter.api.Test; import support.HttpUtils; -import static org.assertj.core.api.Assertions.assertThat; - class ServletTest { private final String WEBAPP_DIR_LOCATION = "src/main/webapp/"; @@ -27,8 +27,9 @@ void testSharedCounter() { assertThat(response.statusCode()).isEqualTo(200); // expected를 0이 아닌 올바른 값으로 바꿔보자. - // 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까? - assertThat(Integer.parseInt(response.body())).isEqualTo(0); + // Q. 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까? + // A. SharedCounterServlet의 sharedCounter는 공유되므로 + assertThat(Integer.parseInt(response.body())).isEqualTo(3); } @Test @@ -49,7 +50,9 @@ void testLocalCounter() { assertThat(response.statusCode()).isEqualTo(200); // expected를 0이 아닌 올바른 값으로 바꿔보자. - // 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까? - assertThat(Integer.parseInt(response.body())).isEqualTo(0); + // Q. 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까? + // A. LocalCounterServlet의 localCounter는 매 요청마다 새로 생성되므로 + assertThat(Integer.parseInt(response.body())).isEqualTo(1); + // init -> (doFilter -> service) * 3 -> destroy } } From fdd30788a078f19a38cd2a1861db843c8fa4d984 Mon Sep 17 00:00:00 2001 From: SCY Date: Sun, 22 Sep 2024 21:56:13 +0900 Subject: [PATCH 17/21] =?UTF-8?q?test:=20=ED=95=99=EC=8A=B5=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20Filter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/CharacterEncodingFilter.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java b/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java index cf4d886974..14a7e6525b 100644 --- a/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java +++ b/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java @@ -1,16 +1,27 @@ package servlet.com.example; -import jakarta.servlet.*; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import jakarta.servlet.annotation.WebFilter; - import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @WebFilter("/*") public class CharacterEncodingFilter implements Filter { + private static final Logger log = LoggerFactory.getLogger(CharacterEncodingFilter.class); + @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { request.getServletContext().log("doFilter() 호출"); + log.info("기존 characterEncoding = {}", response.getCharacterEncoding()); + response.setCharacterEncoding("UTF-8"); + log.info("변경 characterEncoding = {}", response.getCharacterEncoding()); chain.doFilter(request, response); } } From 8ace1d8dd7b16dc55b3e2c61eb957b79a10f6337 Mon Sep 17 00:00:00 2001 From: SCY Date: Sun, 22 Sep 2024 22:03:11 +0900 Subject: [PATCH 18/21] =?UTF-8?q?test:=20=ED=8F=AC=ED=95=A8=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- study/src/test/java/reflection/Junit3TestRunner.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/study/src/test/java/reflection/Junit3TestRunner.java b/study/src/test/java/reflection/Junit3TestRunner.java index f9215d323a..663a24fd97 100644 --- a/study/src/test/java/reflection/Junit3TestRunner.java +++ b/study/src/test/java/reflection/Junit3TestRunner.java @@ -40,7 +40,7 @@ void getMethods() { List actual = Arrays.stream(methods) .map(Method::getName) .toList(); - assertThat(actual).containsExactly( + assertThat(actual).containsOnly( "test1", "test2", "equals", @@ -64,7 +64,7 @@ void getDeclaredMethods() { List actual = Arrays.stream(methods) .map(Method::getName) .toList(); - assertThat(actual).containsExactly( + assertThat(actual).containsOnly( "test1", "test2", "privateTest3"); From 6afede4459d1a403c673bd0e9b535f7025415e82 Mon Sep 17 00:00:00 2001 From: SCY Date: Sun, 22 Sep 2024 22:03:21 +0900 Subject: [PATCH 19/21] =?UTF-8?q?style:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java index c7fba1017b..ffeb37fbb3 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java @@ -9,7 +9,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -class HandlerKeyTest { // TODO: 캐시 테스트 더 좋은 방식 없나? 질문하기 +class HandlerKeyTest { @BeforeEach void setUp() { From 279fc922cbb45ea99a644e30d086ef4e0ef6cbf3 Mon Sep 17 00:00:00 2001 From: SCY Date: Thu, 26 Sep 2024 00:31:49 +0900 Subject: [PATCH 20/21] =?UTF-8?q?test:=20=EB=8F=99=EB=93=B1=EC=84=B1=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20=EB=8F=99=EC=9D=BC=EC=84=B1=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B0=8F=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webmvc/servlet/mvc/tobe/HandlerKey.java | 4 ---- .../servlet/mvc/tobe/HandlerKeyTest.java | 22 ++++++++++++++++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java index 35d3c4ccfe..fb2f0dd48e 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java @@ -25,10 +25,6 @@ private HandlerKey(final String url, final RequestMethod requestMethod) { CACHE.add(this); } - protected static void cleanCache() { - CACHE.clear(); - } - @Override public boolean equals(Object o) { if (this == o) { diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java index ffeb37fbb3..a2c867d81d 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java @@ -13,13 +13,16 @@ class HandlerKeyTest { @BeforeEach void setUp() { - HandlerKey.cleanCache(); + cleanCache(); } @DisplayName("캐시에 존재하지 않는 HandlerKey 생성을 시도하는 경우 해당 인스턴스를 캐시에 추가 후 반환한다.") @Test void should_addInCache_when_createNewHandlerKey() { - // given & when + // given + assertThat(getCacheSize()).isEqualTo(0); + + // when HandlerKey.from("/new", RequestMethod.GET); // then @@ -31,12 +34,13 @@ void should_addInCache_when_createNewHandlerKey() { void should_getInCache_when_createExistHandlerKey() { // given HandlerKey exist = HandlerKey.from("/url", RequestMethod.GET); + assertThat(getCacheSize()).isEqualTo(1); // when HandlerKey created = HandlerKey.from("/url", RequestMethod.GET); // then - assertThat(created).isEqualTo(exist); + assertThat(created).isSameAs(exist); assertThat(getCacheSize()).isEqualTo(1); } @@ -51,4 +55,16 @@ private int getCacheSize() { throw new RuntimeException("CACHE 사이즈에 접근할 수 없습니다."); } } + + private void cleanCache() { + try { + Field cacheField = HandlerKey.class.getDeclaredField("CACHE"); + cacheField.setAccessible(true); + List cache = (List) cacheField.get(null); + cache.clear(); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + throw new RuntimeException("CACHE를 초기화할 수 없습니다."); + } + } } From 21fcb373ac38712228ba1b604f853578898926e0 Mon Sep 17 00:00:00 2001 From: SCY Date: Thu, 26 Sep 2024 01:50:30 +0900 Subject: [PATCH 21/21] =?UTF-8?q?refactor:=20HandlerKey=20=EC=BA=90?= =?UTF-8?q?=EC=8B=9C=20=EC=9E=90=EB=A3=8C=EA=B5=AC=EC=A1=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webmvc/servlet/mvc/tobe/HandlerKey.java | 17 +++++++++-------- .../webmvc/servlet/mvc/tobe/HandlerKeyTest.java | 6 +++--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java index fb2f0dd48e..6ff6b6c3f2 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java @@ -1,28 +1,29 @@ package com.interface21.webmvc.servlet.mvc.tobe; import com.interface21.web.bind.annotation.RequestMethod; -import java.util.ArrayList; -import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; public class HandlerKey { - private static final List CACHE = new ArrayList<>(); + private static final Map CACHE = new ConcurrentHashMap<>(); public final String url; public final RequestMethod requestMethod; public static HandlerKey from(String url, RequestMethod requestMethod) { - return CACHE.stream() - .filter(handlerKey -> handlerKey.url.equals(url) && handlerKey.requestMethod == requestMethod) - .findAny() - .orElseGet(() -> new HandlerKey(url, requestMethod)); + String key = url + requestMethod.name(); + if (CACHE.containsKey(key)) { + return CACHE.get(key); + } + return new HandlerKey(url, requestMethod); } private HandlerKey(final String url, final RequestMethod requestMethod) { this.url = url; this.requestMethod = requestMethod; - CACHE.add(this); + CACHE.put(url + requestMethod.name(), this); } @Override diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java index a2c867d81d..a5cde74acb 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java @@ -4,7 +4,7 @@ import com.interface21.web.bind.annotation.RequestMethod; import java.lang.reflect.Field; -import java.util.List; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -48,7 +48,7 @@ private int getCacheSize() { try { Field cacheField = HandlerKey.class.getDeclaredField("CACHE"); cacheField.setAccessible(true); - List cache = (List) cacheField.get(null); + Map cache = (Map) cacheField.get(null); return cache.size(); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); @@ -60,7 +60,7 @@ private void cleanCache() { try { Field cacheField = HandlerKey.class.getDeclaredField("CACHE"); cacheField.setAccessible(true); - List cache = (List) cacheField.get(null); + Map cache = (Map) cacheField.get(null); cache.clear(); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace();