Skip to content

Commit

Permalink
[MVC 구현하기 - 2단계] 페드로(류형욱) 미션 제출합니다. (#726)
Browse files Browse the repository at this point in the history
* refactor: RegisterController를 어노테이션 기반으로 변경하고 명시적 매핑 제거

* refactor(HandlerMapping): HandlerMapping 인터페이스 추가

* feat(HandlerAdapter): HandlerAdapter 인터페이스 추가

* feat(ControllerHandlerAdapter): 컨트롤러를 담당하는 HandlerAdapter 구현체 구현

* feat(HandlerExecutionHandlerAdapter): HandlerExecution을 담당하는 HandlerAdapter 구현체 구현

* feat: HandlerMapping 구현체에서 핸들러 초기화 시 로깅 추가

* feat(DispatcherServlet): 어노테이션 기반 컨트롤러와 인터페이스 기반 컨트롤러를 모두 지원하도록 구현

* refactor(JspView): 내부 로직 메서드 분리

* test(JspViewTest): redirect/forward 테스트 추가

* refactor(AnnotationHandlerMappingTest): 핸들러 부재 시 더 이상 예외를 던지지 않으므로 관련 테스트 삭제

* refactor(HandlerExecutionHandlerAdapter): 패키지 위치 변경

* test(HandlerExecutionTest): 메서드 접근 불가 시 예외 발생 테스트 추가

* refactor(ControllerScanner): Reflections에서 Controller를 찾고 인스턴스화를 담당하는 클래스 분리

* Test(ControllerScannerTest): 컨트롤러 클래스의 인스턴스화 실패 테스트 추가

* refactor(AnnotationHandlerMapping): 루프 내부의 forEach 구문을 메서드로 분리

* refactor(DispatcherServlet): 어노테이션 기반 컨트롤러의 베이스 패키지 경로 상수화

* test(DispatcherServletTest): DispatcherServlet 예외 테스트 추가

* refactor(DispatcherServlet): HandlerMapping과 HandlerAdapter를 일급 컬렉션으로 관리하도록 변경

* refactor: Registry 클래스들을 프레임워크 패키지로 이동

* style(ControllerScannerTest): 불필요한 개행 제거

* refactor(FakeController): 미 사용 경고 제거 어노테이션 추가

* fix(JspViewTest): `@DisplayName` 오타 수정

* test(ControllerScannerTest): 컨트롤러 클래스 인스턴스화 성공 테스트 추가

* test(HandlerMappingRegistry): HandlerMappingRegistry 테스트 추가

* test(DispatcherServletTest): 어노테이션/인터페이스 기반 컨트롤러 처리 테스트 추가
  • Loading branch information
hw0603 authored Sep 29, 2024
1 parent 83a618b commit f450148
Show file tree
Hide file tree
Showing 22 changed files with 578 additions and 76 deletions.
49 changes: 37 additions & 12 deletions app/src/main/java/com/techcourse/DispatcherServlet.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.techcourse;

import com.interface21.webmvc.servlet.view.JspView;
import com.interface21.webmvc.servlet.ModelAndView;
import com.interface21.webmvc.servlet.mvc.HandlerAdapter;
import com.interface21.webmvc.servlet.mvc.HandlerAdapterRegistry;
import com.interface21.webmvc.servlet.mvc.HandlerMapping;
import com.interface21.webmvc.servlet.mvc.HandlerMappingRegistry;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.Serial;
import java.util.HashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -16,30 +19,52 @@ public class DispatcherServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class);

private ManualHandlerMapping manualHandlerMapping;
private final HandlerMappingRegistry handlerMappingRegistry;
private final HandlerAdapterRegistry handlerAdapterRegistry;

public DispatcherServlet() {
handlerMappingRegistry = new HandlerMappingRegistry();
handlerAdapterRegistry = new HandlerAdapterRegistry();
}

@Override
public void init() {
manualHandlerMapping = new ManualHandlerMapping();
manualHandlerMapping.initialize();
handlerMappingRegistry.initialize();
handlerAdapterRegistry.initialize();
}

@Override
protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException {
final String requestURI = request.getRequestURI();
log.debug("Method : {}, Request URI : {}", request.getMethod(), requestURI);
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException {
String method = request.getMethod();
String requestURI = request.getRequestURI();
log.debug("Method : {}, Request URI : {}", method, requestURI);

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);
Object handler = findHandlerOf(request);
HandlerAdapter adapter = findHandlerAdapterOf(handler);
ModelAndView modelAndView = adapter.handle(request, response, handler);
modelAndView.render(request, response);
} catch (Exception e) {
log.error("Exception : {}", e.getMessage(), e);
throw new ServletException(e.getMessage());
}
}

private Object findHandlerOf(HttpServletRequest request) {
return handlerMappingRegistry.getHandler(request)
.orElseThrow(() -> new IllegalArgumentException("요청에 해당하는 핸들러를 찾을 수 없습니다."));
}

private HandlerAdapter findHandlerAdapterOf(Object handler) {
return handlerAdapterRegistry.getHandlerAdapter(handler)
.orElseThrow(() -> new IllegalArgumentException("요청에 해당하는 핸들러 어댑터를 찾을 수 없습니다."));
}

public void addHandlerMapping(HandlerMapping handlerMapping) {
handlerMappingRegistry.addHandlerMapping(handlerMapping);
}

public void addHandlerAdapter(HandlerAdapter handlerAdapter) {
handlerAdapterRegistry.addHandlerAdapter(handlerAdapter);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.techcourse;

import com.interface21.web.WebApplicationInitializer;
import jakarta.servlet.ServletContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.interface21.web.WebApplicationInitializer;

/**
* Base class for {@link WebApplicationInitializer}
Expand All @@ -18,6 +18,7 @@ public class DispatcherServletInitializer implements WebApplicationInitializer {
@Override
public void onStartup(final ServletContext servletContext) {
final var dispatcherServlet = new DispatcherServlet();
dispatcherServlet.addHandlerMapping(new ManualHandlerMapping());

final var registration = servletContext.addServlet(DEFAULT_SERVLET_NAME, dispatcherServlet);
if (registration == null) {
Expand Down
15 changes: 9 additions & 6 deletions app/src/main/java/com/techcourse/ManualHandlerMapping.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.techcourse;

import com.interface21.webmvc.servlet.mvc.HandlerMapping;
import com.techcourse.controller.*;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.interface21.webmvc.servlet.mvc.asis.Controller;
Expand All @@ -9,27 +11,28 @@
import java.util.HashMap;
import java.util.Map;

public class ManualHandlerMapping {
public class ManualHandlerMapping implements HandlerMapping {

private static final Logger log = LoggerFactory.getLogger(ManualHandlerMapping.class);

private static final Map<String, Controller> controllers = new HashMap<>();

@Override
public void initialize() {
controllers.put("/", new ForwardController("/index.jsp"));
controllers.put("/login", new LoginController());
controllers.put("/login/view", new LoginViewController());
controllers.put("/logout", new LogoutController());
controllers.put("/register/view", new RegisterViewController());
controllers.put("/register", new RegisterController());

log.info("Initialized Handler Mapping!");
log.info("Initialized ManualHandlerMapping!");
controllers.keySet()
.forEach(path -> log.info("Path : {}, Controller : {}", path, controllers.get(path).getClass()));
}

public Controller getHandler(final String requestURI) {
log.debug("Request Mapping Uri : {}", requestURI);
@Override
public Controller getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.debug("(Manual) Request Mapping Uri : {}", requestURI);
return controllers.get(requestURI);
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
package com.techcourse.controller;

import com.interface21.context.stereotype.Controller;
import com.interface21.web.bind.annotation.RequestMapping;
import com.interface21.web.bind.annotation.RequestMethod;
import com.interface21.webmvc.servlet.ModelAndView;
import com.interface21.webmvc.servlet.view.JspView;
import com.techcourse.domain.User;
import com.techcourse.repository.InMemoryUserRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.interface21.webmvc.servlet.mvc.asis.Controller;

public class RegisterController implements Controller {
@Controller
public class RegisterController {

@Override
public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception {
@RequestMapping(value = "/register", method = RequestMethod.GET)
public ModelAndView view(HttpServletRequest req, HttpServletResponse res) {
JspView jspView = new JspView("/register.jsp");
return new ModelAndView(jspView);
}

@RequestMapping(value = "/register", method = RequestMethod.POST)
public ModelAndView save(HttpServletRequest req, HttpServletResponse res) {
final var user = new User(2,
req.getParameter("account"),
req.getParameter("password"),
req.getParameter("email"));
InMemoryUserRepository.save(user);

return "redirect:/index.jsp";
JspView jspView = new JspView("redirect:/index.jsp");
return new ModelAndView(jspView);
}
}

This file was deleted.

115 changes: 115 additions & 0 deletions app/src/test/java/com/techcourse/DispatcherServletTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.techcourse;

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.webmvc.servlet.mvc.HandlerMapping;
import com.techcourse.controller.LoginViewController;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

class DispatcherServletTest {

private DispatcherServlet dispatcherServlet;
private HttpServletRequest request;
private HttpServletResponse response;

@BeforeEach
void setUp() {
dispatcherServlet = new DispatcherServlet();
dispatcherServlet.init();
request = mock(HttpServletRequest.class);
response = mock(HttpServletResponse.class);
}

@DisplayName("어노테이션 기반 컨트롤러를 찾아서 처리한다.")
@Test
void processAnnotationBasedController() throws ServletException {
// given
when(request.getMethod()).thenReturn("GET");
when(request.getRequestURI()).thenReturn("/register");

ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
RequestDispatcher requestDispatcher = mock(RequestDispatcher.class);
when(request.getRequestDispatcher(argumentCaptor.capture()))
.thenReturn(requestDispatcher);

// when
dispatcherServlet.service(request, response);

// then
assertThat(argumentCaptor.getValue())
.isEqualTo("/register.jsp");
}

@DisplayName("Controller 인터페이스 기반 컨트롤러를 찾아서 처리한다.")
@Test
void processInterfaceBasedController() throws ServletException {
// given
when(request.getMethod()).thenReturn("GET");
when(request.getRequestURI()).thenReturn("/login/view");
when(request.getSession()).thenReturn(mock(HttpSession.class));
registerFakeHandlerMapping(new LoginViewController());

ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
RequestDispatcher requestDispatcher = mock(RequestDispatcher.class);
when(request.getRequestDispatcher(argumentCaptor.capture()))
.thenReturn(requestDispatcher);

// when
dispatcherServlet.service(request, response);

// then
assertThat(argumentCaptor.getValue())
.isEqualTo("/login.jsp");
}

@DisplayName("요청에 해당하는 핸들러를 찾을 수 없으면 예외가 발생한다.")
@Test
void throwsWhenHandlerNotFound() {
// given
when(request.getMethod()).thenReturn("GET");
when(request.getRequestURI()).thenReturn("/not-found");

// when & then
assertThatThrownBy(() -> dispatcherServlet.service(request, response))
.isInstanceOf(ServletException.class)
.hasMessageContaining("요청에 해당하는 핸들러를 찾을 수 없습니다.");
}

@DisplayName("요청에 해당하는 핸들러 어댑터를 찾을 수 없으면 예외가 발생한다.")
@Test
void throwsWhenHandlerAdapterNotFound() {
// given
registerFakeHandlerMapping("test");
when(request.getMethod()).thenReturn("GET");
when(request.getRequestURI()).thenReturn("/test");

// when & then
assertThatThrownBy(() -> dispatcherServlet.service(request, response))
.isInstanceOf(ServletException.class)
.hasMessageContaining("요청에 해당하는 핸들러 어댑터를 찾을 수 없습니다.");
}

private void registerFakeHandlerMapping(Object retVal) {
dispatcherServlet.addHandlerMapping(new HandlerMapping() {
@Override
public void initialize() {
}

@Override
public Object getHandler(HttpServletRequest request) {
return retVal;
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.interface21.webmvc.servlet;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -14,6 +16,10 @@ public ModelAndView(final View view) {
this.model = new HashMap<>();
}

public void render(HttpServletRequest request, HttpServletResponse response) throws Exception {
view.render(model, request, response);
}

public ModelAndView addObject(final String attributeName, final Object attributeValue) {
model.put(attributeName, attributeValue);
return this;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.interface21.webmvc.servlet.mvc;

import com.interface21.webmvc.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public interface HandlerAdapter {

ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

boolean canHandle(Object handler);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.interface21.webmvc.servlet.mvc;

import com.interface21.webmvc.servlet.mvc.tobe.ControllerHandlerAdapter;
import com.interface21.webmvc.servlet.mvc.tobe.HandlerExecutionHandlerAdapter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class HandlerAdapterRegistry {

private final List<HandlerAdapter> handlerAdapters;

public HandlerAdapterRegistry() {
this.handlerAdapters = new ArrayList<>();
}

public void initialize() {
handlerAdapters.add(new ControllerHandlerAdapter());
handlerAdapters.add(new HandlerExecutionHandlerAdapter());
}

public void addHandlerAdapter(HandlerAdapter adapter) {
handlerAdapters.add(adapter);
}

public Optional<HandlerAdapter> getHandlerAdapter(Object handler) {
return handlerAdapters.stream()
.filter(adapter -> adapter.canHandle(handler))
.findFirst();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.interface21.webmvc.servlet.mvc;

import jakarta.servlet.http.HttpServletRequest;

public interface HandlerMapping {

void initialize();

Object getHandler(HttpServletRequest request);
}
Loading

0 comments on commit f450148

Please sign in to comment.