Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MVC 구현하기 - 1단계] 무민(박무현) 미션 제출합니다. #350

Merged
merged 7 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
# @MVC 구현하기

---

## 1. @MVC 프레임워크 구현하기
- [x] AnnotationHandlerMapping 구현
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package webmvc.org.springframework.web.servlet.mvc.tobe;

import context.org.springframework.stereotype.Controller;
import jakarta.servlet.http.HttpServletRequest;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import web.org.springframework.web.bind.annotation.RequestMapping;
import web.org.springframework.web.bind.annotation.RequestMethod;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class AnnotationHandlerMapping {

Expand All @@ -21,9 +30,43 @@ public AnnotationHandlerMapping(final Object... basePackage) {

public void initialize() {
log.info("Initialized AnnotationHandlerMapping!");
Reflections reflections = new Reflections(basePackage);
Set<Class<?>> controllers = reflections.getTypesAnnotatedWith(Controller.class);
List<Method> requestMappingMethods = mapRequestMappingMethods(controllers);

putHandlerExecutions(requestMappingMethods);
}

private List<Method> mapRequestMappingMethods(Set<Class<?>> controllers) {
return controllers.stream()
.map(Class::getDeclaredMethods).flatMap(Stream::of)
.filter(method -> method.isAnnotationPresent(RequestMapping.class))
.collect(Collectors.toList());
}

private void putHandlerExecutions(List<Method> requestMappingMethods) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

void 반환으로 위임 방식으로 메서드를 작성해주셨네요 ㅎㅎ
메서드들이 반환 값을 갖도록 만들어 상위 호출 메서드에서 합쳐주는 방식도 있었을 것 같은데 요 방식은 어떻게 생각하시나요 ?ㅎㅎ

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 지금 작성한 것보다 이해하기 쉽다면 그 방식도 고려해볼 것 같습니다.

Copy link

@BGuga BGuga Sep 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예시를 첨가하자면!

private Map<HandlerKey, HandlerExecution> extractHandlerFromMethod(Method method, Object handler);

저는 클래스 내부의 메서드에 이런식으로 반환 값을 갖는 것을 선호합니다 ㅎㅎ

  1. 이렇게 작성하면 메서드 디버깅할 때 해당 메서드를 직접 호출해보면서 테스트하기 편리했습니다!

void 값일 경우에는 handlerExecutions 한번 본 다음에 메서드 호출하고 또 다시 조회해서 변화를 찾아내야 겠죵?

  1. 객체 분리가 조금 더 수월하다.

객체가 너무 커져서 분리를할 때 void 로 선언되어 있다면 해당 클래스에 handlerExecutions 을 넘기는 형태로 구현할 수는 없으니 리팩터링이 발생합니다.
반환 타입이 있을 경우Class 를 가져오는 것은 ClassScanner, Map<HandlerKey, HandlerExecutor> 를 만들어주는 것은 HandlerExtractor 와 같이 책임 별 분리가 좀 더 수월해지더라고요 ㅎㅎ

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오.. 좋은거 같아요 👍🏻👍🏻 혹시 이렇게 리턴했을 때의 단점도 있을까여?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

흠..
내부 메서드를 하나의 작은 클래스의 메서드의 호출이라 상상하고 메서드를 작성하는 과정인데...
큰 단점을 아직까지 느끼진 못했던 것 같네요🤔

requestMappingMethods.forEach(method -> {
Object controller = createControllerInstance(method);
RequestMapping annotation = method.getAnnotation(RequestMapping.class);
String path = annotation.value();
RequestMethod[] requestMethods = annotation.method();
RequestMethod requestMethod = RequestMethod.valueOf(requestMethods[0].name());
handlerExecutions.put(new HandlerKey(path, requestMethod), new HandlerExecution(controller, method));
});
}

private Object createControllerInstance(Method method) {
try {
return method.getDeclaringClass().newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new IllegalArgumentException(e);
}
Comment on lines +60 to +63
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

method 에서 Class 만드는 흐름이 상당히 직관적이네요 👍
클래스들을 먼저 추출하고 해당 클래스에 대한 등록 작업도 가능할 것 같은데 혹시 고려해보셨는지 궁금합니다 ㅎㅎ

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

쉽고 가독성 좋은 것이 베스트라 생각해서 우선 생각나는대로 짜봤습니다. 해당 방법이 더 좋다고 느껴지면 해당 방법도 고려해볼 만한 것 같은데 예시가 있을까요

Copy link

@BGuga BGuga Sep 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클래스를 먼저 추출한 후 클래스의 RequestMapping 메서드를 추출하는 예시 코드는 아래와 같은 흐름일 것 같네요!

Set<Class<?>> controllers = reflections.getTypesAnnotatedWith(Controller.class);
Map<Class<?>, Object> instances = makeInstances(controllers); // Class 별 인스턴스 만들기
for (Class<?> targetClass : controllers.keySet()) {
    enrollHandler(targetClass, controllers.get(targetClass))
}

이런 흐름으로 간다면
하나의 클래스에 RequestMapping 메서드가 5개 있을 경우 똑같은 Class를 한번만 Instance 화 할 수 있네요 ㅎㅎ

}

public Object getHandler(final HttpServletRequest request) {
return null;
RequestMethod requestMethod = RequestMethod.valueOf(request.getMethod());
HandlerKey handlerKey = new HandlerKey(request.getRequestURI(), requestMethod);

return handlerExecutions.get(handlerKey);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@
import jakarta.servlet.http.HttpServletResponse;
import webmvc.org.springframework.web.servlet.ModelAndView;

import java.lang.reflect.Method;

public class HandlerExecution {

private final Object controller;
private final Method method;

public HandlerExecution(Object controller, Method method) {
this.controller = controller;
this.method = method;
}

public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception {
return null;
return (ModelAndView) method.invoke(controller, request, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class CharacterEncodingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
request.getServletContext().log("doFilter() 호출");
response.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
}
}
11 changes: 11 additions & 0 deletions study/src/test/java/reflection/Junit3TestRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,23 @@

import org.junit.jupiter.api.Test;

import java.util.Arrays;

class Junit3TestRunner {

@Test
void run() throws Exception {
Class<Junit3Test> clazz = Junit3Test.class;

// TODO Junit3Test에서 test로 시작하는 메소드 실행
Arrays.stream(clazz.getMethods())
.filter(c -> c.getName().startsWith("test"))
.forEach(c -> {
try {
c.invoke(clazz.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
11 changes: 11 additions & 0 deletions study/src/test/java/reflection/Junit4TestRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,23 @@

import org.junit.jupiter.api.Test;

import java.util.Arrays;

class Junit4TestRunner {

@Test
void run() throws Exception {
Class<Junit4Test> clazz = Junit4Test.class;

// TODO Junit4Test에서 @MyTest 애노테이션이 있는 메소드 실행
Arrays.stream(clazz.getMethods())
.filter(c -> c.isAnnotationPresent(MyTest.class))
.forEach(c -> {
try {
c.invoke(clazz.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
51 changes: 28 additions & 23 deletions study/src/test/java/reflection/ReflectionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import static org.assertj.core.api.Assertions.assertThat;

Expand All @@ -18,35 +21,34 @@ class ReflectionTest {
@Test
void givenObject_whenGetsClassName_thenCorrect() {
final Class<Question> 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<String> actualFieldNames = null;
final Field[] fields = student.getClass().getDeclaredFields();
final List<String> actualFieldNames = Arrays.stream(fields).map(Field::getName).collect(Collectors.toList());

assertThat(actualFieldNames).contains("name", "age");
}

@Test
void givenClass_whenGetsMethods_thenCorrect() {
final Class<?> animalClass = Student.class;
final Method[] methods = null;
final List<String> actualMethods = null;
final Method[] methods = animalClass.getDeclaredMethods();
final List<String> actualMethods = Arrays.stream(methods).map(Method::getName).collect(Collectors.toList());

assertThat(actualMethods)
.hasSize(3)
Expand All @@ -56,7 +58,7 @@ void givenClass_whenGetsMethods_thenCorrect() {
@Test
void givenClass_whenGetsAllConstructors_thenCorrect() {
final Class<?> questionClass = Question.class;
final Constructor<?>[] constructors = null;
final Constructor<?>[] constructors = questionClass.getDeclaredConstructors();

assertThat(constructors).hasSize(2);
}
Expand All @@ -65,11 +67,11 @@ void givenClass_whenGetsAllConstructors_thenCorrect() {
void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() throws Exception {
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(String.class, String.class, String.class);

final Question firstQuestion = null;
final Question secondQuestion = null;
final Question firstQuestion = (Question) firstConstructor.newInstance("gugu", "제목1", "내용1");
final Question secondQuestion = (Question) secondConstructor.newInstance("gugu", "제목2", "내용2");

assertThat(firstQuestion.getWriter()).isEqualTo("gugu");
assertThat(firstQuestion.getTitle()).isEqualTo("제목1");
Expand All @@ -82,15 +84,17 @@ void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() throws Exception
@Test
void givenClass_whenGetsPublicFields_thenCorrect() {
final Class<?> questionClass = Question.class;
final Field[] fields = null;
final Field[] fields = Arrays.stream(questionClass.getDeclaredFields())
.filter(AccessibleObject::isAccessible)
.toArray(Field[]::new);

assertThat(fields).hasSize(0);
}

@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");
Expand All @@ -99,31 +103,32 @@ 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");
}

@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 Student student = new Student();
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);
Expand Down
9 changes: 9 additions & 0 deletions study/src/test/java/reflection/ReflectionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
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 {

Expand All @@ -14,5 +17,11 @@ void showAnnotationClass() throws Exception {
Reflections reflections = new Reflections("reflection.examples");

// TODO 클래스 레벨에 @Controller, @Service, @Repository 애노테이션이 설정되어 모든 클래스 찾아 로그로 출력한다.
reflections.getTypesAnnotatedWith(Controller.class)
.forEach(c -> log.info("{}", c.getSimpleName()));
reflections.getTypesAnnotatedWith(Service.class)
.forEach(c -> log.info("{}", c.getSimpleName()));
reflections.getTypesAnnotatedWith(Repository.class)
.forEach(c -> log.info("{}", c.getSimpleName()));
}
}
4 changes: 2 additions & 2 deletions study/src/test/java/servlet/com/example/ServletTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ void testSharedCounter() {

// expected를 0이 아닌 올바른 값으로 바꿔보자.
// 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까?
assertThat(Integer.parseInt(response.body())).isEqualTo(0);
assertThat(Integer.parseInt(response.body())).isEqualTo(3);
}

@Test
Expand All @@ -50,6 +50,6 @@ void testLocalCounter() {

// expected를 0이 아닌 올바른 값으로 바꿔보자.
// 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까?
assertThat(Integer.parseInt(response.body())).isEqualTo(0);
assertThat(Integer.parseInt(response.body())).isEqualTo(1);
}
}
Loading