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단계] 디노(신종화) 미션 제출합니다. #382

Merged
merged 12 commits into from
Sep 14, 2023
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]
types: [ opened, synchronize, reopened ]
jobs:
build:
name: Build and analyze
Expand Down Expand Up @@ -34,4 +34,4 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: ./gradlew clean build codeCoverageReport --info -x :study:build
run: ./gradlew clean build codeCoverageReport sonar --info -x :study:build
7 changes: 7 additions & 0 deletions app/src/test/java/com/techcourse/DispatcherServletTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.techcourse;

import static org.junit.jupiter.api.Assertions.*;

class DispatcherServletTest {

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package webmvc.org.springframework.web.servlet.mvc.tobe;

import context.org.springframework.stereotype.Controller;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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;
import web.org.springframework.web.bind.annotation.RequestMapping;
import web.org.springframework.web.bind.annotation.RequestMethod;

public class AnnotationHandlerMapping {

Expand All @@ -20,10 +26,30 @@ public AnnotationHandlerMapping(final Object... basePackage) {
}

public void initialize() {
final Reflections reflections = new Reflections(basePackage);
final Set<Class<?>> controllerClasses = reflections.getTypesAnnotatedWith(Controller.class);

controllerClasses.forEach(
aClass -> Arrays.stream(aClass.getDeclaredMethods()).forEach(
method -> {
final RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
if (requestMapping != null) {
putHandlerExecutions(method, requestMapping);
}
Comment on lines +32 to +38
Copy link

Choose a reason for hiding this comment

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

반영하지 않으셔도 됩니다 !!

저는 개인적으로 stream 내부에 stream이 들어가면 가독성이 더 떨어진다고 생각해요 ㅎㅎ

메서드 분리도 하나의 방법이 될 수 있을 것 같은데 디노가 끌리는대로 해주세요 😁

Copy link
Member Author

Choose a reason for hiding this comment

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

저도 가독성 측면에서 단점이 있다고 생각 해봤는데요.!
함수 분리로 인해서 오히려 로직의 흐름을 한 눈에 파악하기 힘들다는 생각과 함께 고민하다 그대로 두었었습니다.
그래도 한 번 정도는 더 분리를 해도 파악하기엔 어렵지 않다는 생각이 들어서 findRequestMapping이라는 함수를 분리해 보았습니다.!

}
)
);
log.info("Initialized AnnotationHandlerMapping!");
}

private void putHandlerExecutions(final Method method, final RequestMapping requestMapping) {
Arrays.stream(requestMapping.method())
.map(requestMethod -> new HandlerKey(requestMapping.value(), requestMethod))
.forEach(handlerKey -> handlerExecutions.put(handlerKey, new HandlerExecution(method)));
Copy link

Choose a reason for hiding this comment

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

중요한 부분은 아니지만 stream 최종연산에서 stream 외부에 있는 객체의 접근은 최대한 피하는게 좋다고 합니다 !

(스트림은 함수형 프로그래밍을 위해 등장했기 때문에 비동기 동작시 문제가 될 수 있습니다 🤓)

Copy link
Member Author

Choose a reason for hiding this comment

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

어렵네요.. 해당 부분은 다시 for문으로 바꾸어 주었습니다!

}

public Object getHandler(final HttpServletRequest request) {
return null;
HandlerKey handlerKey = new HandlerKey(request.getRequestURI(), RequestMethod.valueOf(request.getMethod()));
return handlerExecutions.get(handlerKey);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import webmvc.org.springframework.web.servlet.ModelAndView;

public class HandlerExecution {

private final Method method;

public HandlerExecution(final Method method) {
this.method = method;
}

public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception {
return null;
final Class<?> declaringClass = method.getDeclaringClass();
final Object object = declaringClass.getDeclaredConstructor().newInstance();
Comment on lines +17 to +18
Copy link

Choose a reason for hiding this comment

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

HandlerExecution 객체가 Object를 필드로 가지게 하는건 어떨까요?!

제가 리플렉션에 대해서 깊게 아는건 아니지만 ㅎㅎ
Method 객체를 invoke할 때 해당 클래스의 인스턴스가 필요하더라구요 !

디노가 작성해주신 것처럼 method에서 클래스 인스턴스를 생성할 수도 있지만, 핸들러를 생성하는 객체에서 함께 인스턴스 생성하는 책임을 가져보는건 어떨지 한번 제안드려봅니다 ㅎㅎ

Copy link
Member Author

Choose a reason for hiding this comment

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

그레이의 말대로 object를 만드는 책임을 HandlerExcution보다는 외부에서 만들어 가져오는 편이 더 자연스럽다고 생각이 드네요..! 변경했습니다ㅎㅎ

return (ModelAndView) method.invoke(object, request, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
@WebFilter("/*")
public class CharacterEncodingFilter implements Filter {

private static final String DEFAULT_ENCODING = "UTF-8";

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
request.getServletContext().log("doFilter() 호출");
request.setCharacterEncoding(DEFAULT_ENCODING);
response.setCharacterEncoding(DEFAULT_ENCODING);
chain.doFilter(request, response);
}
}
22 changes: 22 additions & 0 deletions study/src/test/java/reflection/Junit3TestRunner.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package reflection;

import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;

class Junit3TestRunner {
Expand All @@ -9,5 +10,26 @@ void run() throws Exception {
Class<Junit3Test> clazz = Junit3Test.class;

// TODO Junit3Test에서 test로 시작하는 메소드 실행

// 객체 생성 가능
final Junit3Test junit3Test = clazz.getDeclaredConstructor().newInstance();

// 1. 객체에서 바로 메서드 호출
junit3Test.test1();
junit3Test.test2();

// 2. getDeclaredMethod로 메서드 찾아서 호출
final Method test1 = clazz.getDeclaredMethod("test1");
final Method test2 = clazz.getDeclaredMethod("test2");
test1.invoke(clazz.cast(junit3Test));
test2.invoke(clazz.cast(junit3Test));

// 3. getDeclaredMethods로 메서드 배열 가져온 후 for문 돌면서 호출
final Method[] methods = clazz.getDeclaredMethods();
for (final Method method : methods) {
if (method.getName().startsWith("test")) {
method.invoke(clazz.cast(junit3Test));
}
}
}
}
14 changes: 14 additions & 0 deletions study/src/test/java/reflection/Junit4TestRunner.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package reflection;

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

import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;

class Junit4TestRunner {
Expand All @@ -9,5 +12,16 @@ void run() throws Exception {
Class<Junit4Test> clazz = Junit4Test.class;

// TODO Junit4Test에서 @MyTest 애노테이션이 있는 메소드 실행
final Junit4Test junit4Test = clazz.getDeclaredConstructor().newInstance();

final Method[] declaredMethods = clazz.getDeclaredMethods();

for (final Method method : declaredMethods) {
final MyTest methodAnnotation = method.getAnnotation(MyTest.class);
if (methodAnnotation != null) {
assertThat(methodAnnotation).isNotNull();
method.invoke(junit4Test);
}
}
}
}
71 changes: 43 additions & 28 deletions study/src/test/java/reflection/ReflectionTest.java
Original file line number Diff line number Diff line change
@@ -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.List;

import static org.assertj.core.api.Assertions.assertThat;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class ReflectionTest {

Expand All @@ -19,34 +20,38 @@ class ReflectionTest {
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 +61,7 @@ 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);
}
Expand All @@ -65,11 +70,20 @@ 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.getDeclaredConstructor(
String.class,
String.class,
String.class
);

final Constructor<?> secondConstructor = questionClass.getDeclaredConstructor(
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 +96,15 @@ 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();

assertThat(fields).hasSize(0);
assertThat(fields).isEmpty();
}

@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 +113,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 = (Student) studentClass.getDeclaredConstructor().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);
Expand Down
22 changes: 21 additions & 1 deletion study/src/test/java/reflection/ReflectionsTest.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package reflection;

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

import java.util.Set;
import org.junit.jupiter.api.Assertions;
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 {

Expand All @@ -13,6 +20,19 @@ class ReflectionsTest {
void showAnnotationClass() throws Exception {
Reflections reflections = new Reflections("reflection.examples");

// TODO 클래스 레벨에 @Controller, @Service, @Repository 애노테이션이 설정되어 모든 클래스 찾아 로그로 출력한다.
// TODO 클래스 레벨에 @Controller, @Service, @Repository 애노테이션이 설정되어 있는 모든 클래스 찾아 로그로 출력한다.
Set<Class<?>> controllerClass = reflections.getTypesAnnotatedWith(Controller.class);
Set<Class<?>> serviceClass = reflections.getTypesAnnotatedWith(Service.class);
Set<Class<?>> repositoryClass = reflections.getTypesAnnotatedWith(Repository.class);

Assertions.assertAll(
() -> assertThat(controllerClass).isNotNull(),
() -> assertThat(serviceClass).isNotNull(),
() -> assertThat(repositoryClass).isNotNull()
);

log.info(controllerClass.toString());
log.info(serviceClass.toString());
log.info(repositoryClass.toString());
}
}
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);
}
}