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 구현하기 - 2단계] 페드로(류형욱) 미션 제출합니다. #726

Merged
merged 25 commits into from
Sep 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5370cdf
refactor: RegisterController를 어노테이션 기반으로 변경하고 명시적 매핑 제거
hw0603 Sep 23, 2024
e8cec5f
refactor(HandlerMapping): HandlerMapping 인터페이스 추가
hw0603 Sep 23, 2024
7ab73a9
feat(HandlerAdapter): HandlerAdapter 인터페이스 추가
hw0603 Sep 23, 2024
22504fc
feat(ControllerHandlerAdapter): 컨트롤러를 담당하는 HandlerAdapter 구현체 구현
hw0603 Sep 23, 2024
4dfbbe1
feat(HandlerExecutionHandlerAdapter): HandlerExecution을 담당하는 HandlerA…
hw0603 Sep 23, 2024
45dfa16
feat: HandlerMapping 구현체에서 핸들러 초기화 시 로깅 추가
hw0603 Sep 23, 2024
01edf41
feat(DispatcherServlet): 어노테이션 기반 컨트롤러와 인터페이스 기반 컨트롤러를 모두 지원하도록 구현
hw0603 Sep 23, 2024
9454879
refactor(JspView): 내부 로직 메서드 분리
hw0603 Sep 23, 2024
d6eb8a8
test(JspViewTest): redirect/forward 테스트 추가
hw0603 Sep 23, 2024
c1e936a
refactor(AnnotationHandlerMappingTest): 핸들러 부재 시 더 이상 예외를 던지지 않으므로 관련…
hw0603 Sep 23, 2024
2a5c2ee
refactor(HandlerExecutionHandlerAdapter): 패키지 위치 변경
hw0603 Sep 23, 2024
f501d94
test(HandlerExecutionTest): 메서드 접근 불가 시 예외 발생 테스트 추가
hw0603 Sep 23, 2024
c5b430f
refactor(ControllerScanner): Reflections에서 Controller를 찾고 인스턴스화를 담당하는…
hw0603 Sep 23, 2024
a5d79bb
Test(ControllerScannerTest): 컨트롤러 클래스의 인스턴스화 실패 테스트 추가
hw0603 Sep 23, 2024
0309308
refactor(AnnotationHandlerMapping): 루프 내부의 forEach 구문을 메서드로 분리
hw0603 Sep 23, 2024
849d79f
refactor(DispatcherServlet): 어노테이션 기반 컨트롤러의 베이스 패키지 경로 상수화
hw0603 Sep 23, 2024
753154a
test(DispatcherServletTest): DispatcherServlet 예외 테스트 추가
hw0603 Sep 23, 2024
8e6e237
refactor(DispatcherServlet): HandlerMapping과 HandlerAdapter를 일급 컬렉션으로…
hw0603 Sep 23, 2024
3b52a1b
refactor: Registry 클래스들을 프레임워크 패키지로 이동
hw0603 Sep 23, 2024
10d5980
style(ControllerScannerTest): 불필요한 개행 제거
hw0603 Sep 23, 2024
1d66e3f
refactor(FakeController): 미 사용 경고 제거 어노테이션 추가
hw0603 Sep 23, 2024
5fd0595
fix(JspViewTest): `@DisplayName` 오타 수정
hw0603 Sep 27, 2024
894da3c
test(ControllerScannerTest): 컨트롤러 클래스 인스턴스화 성공 테스트 추가
hw0603 Sep 28, 2024
13e946d
test(HandlerMappingRegistry): HandlerMappingRegistry 테스트 추가
hw0603 Sep 28, 2024
8f72eaf
test(DispatcherServletTest): 어노테이션/인터페이스 기반 컨트롤러 처리 테스트 추가
hw0603 Sep 28, 2024
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
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();
}
Comment on lines +22 to 34
Copy link

Choose a reason for hiding this comment

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

HandlerMappingRegistry과 HandlerAdapterRegistry 모두 외부(DispatcherServlet)에서 초기화를 하는 이유가 있을까요?
각 객체 생성 시에 내부에서 진행되도록 변경해도 될 것 같기도 해요!
페드로의 생각은 어떤가요 👀

Copy link
Member Author

Choose a reason for hiding this comment

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

현재 DispatcherServltetDispatcherServletInitializer 내의 onStartup() 호출 직후 바로
빈 인스턴스가 만들어지고, 사용자 지정 HandlerMapping이 등록된 이후 내부적으로 DispatcherServlet.init()이 호출되어 지연 초기화되는 구조로 작성되어 있어요.

프레임워크에서 자동으로 등록해 준 매핑보다는 사용자가 명시적으로 추가해 준(dispatcherServlet.addHandlerMapping(new ManualHandlerMapping())) 핸들러들이 먼저 검색되도록 하기 위해 HandlerMapping 들도 마찬가지로 초기화가 지연될 수 있도록 구현해 두었습니다.

Copy link

Choose a reason for hiding this comment

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

프레임워크에서 자동으로 등록해 준 매핑보다는 사용자가 명시적으로 추가해 준(dispatcherServlet.addHandlerMapping(new ManualHandlerMapping())) 핸들러들이 먼저 검색되도록 하기 위해 HandlerMapping 들도 마찬가지로 초기화가 지연될 수 있도록 구현해 두었습니다.

의도가 너무 좋네요 👍
저도 커스텀이 우선시 되어야 한다고 생각해요.

그런데 제가 요구사항에 의한 구조적인 문제 때문에 페드로가 어디까지를 프레임워크 역할로 또는 사용자 역할로 보고 있는지가 조금 헷갈리는 것 같아요. 어색하게 느껴지는 부분이 몇 가지 있어서 질문 드릴게요.


먼저 new ManualHandlerMappingDispatcherServlet이 아닌 DispatcherServletInitializer에서 등록을 해주고 있는데
반면, new AnnotationHandlerMapping(ANNOTATION_BASED_CONTROLLER_BASE)에서
com.techcourse.controller라는 특정 애플리케이션에 종속된 패키지를 HandlerMappingRegistry 에서 선언해주고 있는 것 같아요. 이 패키지 or 핸들러 매핑은 DispatcherServletInitializer에서 주입 하지 않은 이유가 궁금해요!


그리고 여전히 각 HandlerMapping의 초기화가 외부에서 이루어져야만 하는지는 이해가 잘 가지 않아요.
HandlerMappingRegistry는 페드로 말씀처럼 사용자 추가 핸들러 매핑의 순서 조정을 위해 초기화를 지연시키는 것이 좋을 것 같아요. 그런데 HandlerMappingRegistry에서 각 핸들러 매핑의 순서가 지정될테니 각 HandlerMapping은 내부에서는 즉시 초기화가 되어도 괜찮지 않나요?

Copy link
Member Author

Choose a reason for hiding this comment

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

먼저 new ManualHandlerMapping은 DispatcherServlet이 아닌 DispatcherServletInitializer에서 등록을 해주고 있는데
반면, new AnnotationHandlerMapping(ANNOTATION_BASED_CONTROLLER_BASE)에서
com.techcourse.controller라는 특정 애플리케이션에 종속된 패키지를 HandlerMappingRegistry 에서 선언해주고 있는 것 같아요. 이 패키지 or 핸들러 매핑은 DispatcherServletInitializer에서 주입 하지 않은 이유가 궁금해요!

PR 본문에서 잠깐 언급했던 것 처럼, AnnotationHandlerMapping은 프레임워크 자체적으로 제공하는 '기능'으로 생각하고 구현했어요. @Controller 어노테이션만 붙여 놓으면 프레임워크에서 알아서 스캔해서 등록을 해 주는 기능이니, 사용자가 관여할 필요가 없다고 생각해서 DispatcherServletInitializer에서 따로 등록하지 않았습니다.

com.techcourse.controller라는 특정 애플리케이션에 종속된 패키지를 HandlerMappingRegistry 에서 선언해주고 있는 것 같아요

물론 말씀주신 것 처럼 지금은 애플리케이션 패키지 경로를 상수형태로 의존하고 있어서 책임분리가 완전히 되어 있다고 보기는 힘든데요,

미션에서 제공한 코드 상에서 DispatcherServlet이 app 모듈에 위치하고, mvc 모듈 내의 패키지인 asis와 tobe의 사전적 의미를 생각해 봤을 때 아직 패키지를 깔끔하게 분리하는 것이 무의미한 것 같아 프레임워크와 사용자 패키지 간의 최소한의 분리만 해 두었습니다. 리뷰하실 때 참고 부탁드려요!

+) mvc 모듈 내에 있는 HandlerMappingRegistry가 "com.techcourse.controller"라는 상수값을 알고 있어야 하는 것도 영 맘에 안 들긴 하네요 허허
나중에 패키지 구조가 정리되면 사용자 쪽에서 주입받거나 하는 식으로 리팩토링이 가능할 것 같아요.

PR 본문에서 이렇게 말씀드렸던 이유이기도 해요!
다음 단계를 살짝 살펴 보니 asis, tobe 패키지가 드디어(!) 제거되는 것 같아서 완전한 분리를 3단계에서 수행하기 위해 현재의 구현으로 요청 드렸어요.


HandlerMappingRegistry에서 각 핸들러 매핑의 순서가 지정될테니 각 HandlerMapping은 내부에서는 즉시 초기화가 되어도 괜찮지 않나요?

오.. 예리하시군요 +_+ 그렇게도 생각할 수 있겠어요.
저는 DispatcherServlet -> HandlerMappingRegistry 까지 전부 지연 초기화 전략을 택하고 있으니, 그 내부에서 관리되는 객체인 HandlerMapping 역시 지연 초기화를 지원하는 것이 자연스럽다고 생각했어요. 현재 HandlerMapping을 사용하는 곳이 HandlerMappingRegistry 뿐인데, HandlerMappingRegistry가 지연 초기화로 인해 아직까지 온전한 객체로 초기화되지 않은 상태에서 HandlerMapping이 완전히 초기화되어 자신이 사용되어지기를 기다리는 시점이 존재해야 할 이유가 없다고 생각했거든요.
(물론 지금은 애플리케이션 시작 직후 DispatcherServlet이 초기화되고, 그 과정에서 모든 의존성에 대해 체인 형태로 초기화가 진행되므로 큰 의미를 가지기는 어려울 수 있을 것 같네요)

하지만 객체의 사용자가 반드시 initialize() 후 사용해야 하는 것을 알고 있어야 한다는 점과, 초기화 메서드의 강제 구현으로 인해 테스트에서 불편함을 느꼈던 것은 사실이라, 이 부분은 좀 더 고민해 보고 다음 단계에서 반영해 보도록 하겠습니다🙂

@jminkkk


@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;
Copy link
Member Author

Choose a reason for hiding this comment

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

질문)
예외 발생 케이스 말고 정상 동작 케이스도 테스트를 하고 싶었는데요...
뭔가 깔끔한 테스트 방법을 찾지 못해서 로컬에서만 끄적거리고 있어요.

모킹하여 테스트하기에는 HttpServletRequestHttpSessionRequestDispatcher 같은 객체들까지 전부다 모킹을 해줘야 테스트가 돌아갈 것 같은데, 너무 구현에 의존적인 테스트가 될 것 같아요.

테스트만을 위한 게터를 열지 않으면서, 직접 HTTP 요청을 보내 보지 않고 깔끔하게 테스트할 수 있는 방법이 있을까요?

    @DisplayName("어노테이션 기반 컨트롤러를 찾아서 처리한다.")
    @Test
    void processAnnotationBasedController() {
        // given
        HttpServletRequest request = mock(HttpServletRequest.class);
        HttpServletResponse response = mock(HttpServletResponse.class);
        when(request.getMethod()).thenReturn("GET");
        when(request.getRequestURI()).thenReturn("/register");
        when(request.getRequestDispatcher("/register.jsp")).thenReturn(mock(RequestDispatcher.class));

        // when & then
        assertThatCode(() -> dispatcherServlet.service(request, response))
                .doesNotThrowAnyException();  // 맘에 안 듦
    }

    @DisplayName("Controller 인터페이스 기반 컨트롤러를 찾아서 처리한다.")
    @Disabled("안돌아감ㅠㅠ")
    @Test
    void processInterfaceBasedController() {
        // given
        HttpServletRequest request = mock(HttpServletRequest.class);
        HttpServletResponse response = mock(HttpServletResponse.class);
        when(request.getMethod()).thenReturn("GET");
        when(request.getRequestURI()).thenReturn("/login");
        when(request.getRequestDispatcher("/login.jsp")).thenReturn(mock(RequestDispatcher.class));

        // when & then
        assertThatCode(() -> dispatcherServlet.service(request, response))
                .doesNotThrowAnyException();
    }

Copy link

Choose a reason for hiding this comment

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

안녕하세요 페드로 ~!
질문 주신 부분에 대해 열심히 찾아보고 고민을 해봤는데 저도 관련 객체들을 다 모킹을 하거나 테스트용 더블을 만드는 것 밖에 생각이 안나더라구요 🥲

그러던 중에 전 기수 분중에 페드로가 테스트하고 싶은 부분과 동일한 역할을 테스트한 코드가 있는 것 같아 가져와봤어요 (덕분에 저도 보면서 재미있었네요)

현재 코드에서는 when(request.getRequestDispatcher("/register.jsp")).thenReturn(mock(RequestDispatcher.class)); 부분을 목킹하고 있긴 한데, 페드로가 테스트하고 싶은 부분이 GET /register 요청이 잘 처리 되는지라면, 이 코드가 도움이 될 수도 있을 것 같습니다

request.getRequestDispatcher("/register.jsp")가 호출되었다는 건. 현재 요청을 "/register.jsp" 페이지로 전달할 준비가 되었다는 것을 의미이니 참고해보시면 좋을 것 같아요 👍

https://github.com/woowacourse/java-mvc/pull/500/files#diff-91350e7fa663241ea8f1287c70efc63b81b42619bf9972055b5632c17e716c66

Copy link
Member Author

Choose a reason for hiding this comment

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

와 전 기수 분 코드까지 찾아봐 주시다니 감동인걸요 😄
읽어 봤는데 여전히 모킹을 해 줘야 하는 부분이 존재하긴 하지만 그래도 제가 생각했던 구조랑 가장 유사한 구현인 것 같아요!

참고하여 반영해 두었습니다 갑사합니다🔥

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();
Comment on lines +5 to +7
Copy link

Choose a reason for hiding this comment

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

이 부분도 void initialize() 이 메서드까지 구현을 강제한 이유가 궁금해요!

Copy link
Member Author

Choose a reason for hiding this comment

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

#726 (comment) 이쪽 코멘트 참고해주세요!


Object getHandler(HttpServletRequest request);
}
Loading