Skip to content

Commit

Permalink
[MVC 구현하기 - 2단계] 스플릿-박상현 미션 제출합니다. (#480)
Browse files Browse the repository at this point in the history
* refactor: mvc모듈에서 새로운 class 로드를 막기 위한 reflection 로직 변경

getHandler() 리턴 객체 타입 변경

* refactor: 기존 유저, UserRepository 변경

* refactor: JspView 에 viewName 필드, render 기능 완성

* refactor: DispatcherServlet 에 AnnotationHandlerMapping 필드 추가, 처리 로직 변경

* refactor: RegisterController 어노테이션 기반으로 변경

* refactor: /register 경로에 대한 처리 제거

* test: Controller 테스트 작성

* feat: HandlerExecution 생성시 클래스, 메서드 검증 로직 추가

* refactor: Exception 검증 범위를 좁히기 위한 메서드 분리

* fix: validateHandlerMethod 메서드 검증 로직 수정

* refactor: HandlerMapping, HandlerAdapter 추상화

* feat: sonarcloud report 설정 추가

* refactor: 쓰지않는 메서드, JsonView render 메서드 처리 변겨

* refactor: 사용되지 않는 메서드, 필드 제거, JsonView render 메서드 처리 변경
  • Loading branch information
splitCoding authored Sep 23, 2023
1 parent 093c61c commit 25abc90
Show file tree
Hide file tree
Showing 29 changed files with 671 additions and 131 deletions.
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
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2"
testImplementation "org.mockito:mockito-core:5.4.0"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.2"

testImplementation "org.apache.httpcomponents:httpclient:4.5.13"
}

idea {
Expand Down
54 changes: 33 additions & 21 deletions app/src/main/java/com/techcourse/DispatcherServlet.java
Original file line number Diff line number Diff line change
@@ -1,51 +1,63 @@
package com.techcourse;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import webmvc.org.springframework.web.servlet.view.JspView;
import webmvc.org.springframework.web.servlet.ModelAndView;
import webmvc.org.springframework.web.servlet.View;
import webmvc.org.springframework.web.servlet.mvc.tobe.exception.HandlerAdapterNotExistException;
import webmvc.org.springframework.web.servlet.mvc.tobe.exception.HandlerNotExistException;
import webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter.AnnotationHandlerAdapter;
import webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter.ControllerHandlerAdapter;
import webmvc.org.springframework.web.servlet.mvc.tobe.handleradapter.HandlerAdapters;
import webmvc.org.springframework.web.servlet.mvc.tobe.handlermapping.AnnotationHandlerMapping;
import webmvc.org.springframework.web.servlet.mvc.tobe.handlermapping.HandlerMappings;

public class DispatcherServlet extends HttpServlet {

private static final long serialVersionUID = 1L;
private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class);

private ManualHandlerMapping manualHandlerMapping;

public DispatcherServlet() {
}
private HandlerMappings handlerMappings = new HandlerMappings();
private HandlerAdapters handlerAdapters = new HandlerAdapters();

@Override
public void init() {
manualHandlerMapping = new ManualHandlerMapping();
manualHandlerMapping.initialize();
final String packageName = getClass().getPackageName();
handlerMappings.addHandlerMapping(new ManualHandlerMapping());
handlerMappings.addHandlerMapping(new AnnotationHandlerMapping(packageName));
handlerAdapters.addAdapter(new ControllerHandlerAdapter());
handlerAdapters.addAdapter(new AnnotationHandlerAdapter());
handlerMappings.initialize();
}

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

try {
final var controller = manualHandlerMapping.getHandler(requestURI);
final var viewName = controller.execute(request, response);
move(viewName, request, response);
} catch (Throwable e) {
log.error("Exception : {}", e.getMessage(), e);
final Object handler = handlerMappings.getHandler(request);
final ModelAndView modelAndView = handlerAdapters.handle(handler, request, response);
final View view = modelAndView.getView();
view.render(Collections.emptyMap(), request, response);
} catch (HandlerNotExistException | HandlerAdapterNotExistException exception) {
setNotFound(request, response);
} catch (Exception 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);
private void setNotFound(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
final RequestDispatcher requestDispatcher = request.getRequestDispatcher("404.jsp");
response.setStatus(404);
requestDispatcher.forward(request, response);
}
}
30 changes: 21 additions & 9 deletions app/src/main/java/com/techcourse/ManualHandlerMapping.java
Original file line number Diff line number Diff line change
@@ -1,35 +1,47 @@
package com.techcourse;

import com.techcourse.controller.*;
import com.techcourse.controller.LoginController;
import com.techcourse.controller.LoginViewController;
import com.techcourse.controller.LogoutController;
import com.techcourse.controller.RegisterViewController;
import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import webmvc.org.springframework.web.servlet.mvc.asis.Controller;
import webmvc.org.springframework.web.servlet.mvc.asis.ForwardController;
import webmvc.org.springframework.web.servlet.mvc.tobe.handlermapping.HandlerMapping;

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!");
controllers.keySet()
.forEach(path -> log.info("Path : {}, Controller : {}", path, controllers.get(path).getClass()));
.forEach(path -> log.info("Path : {}, Controller : {}", path,
controllers.get(path).getClass()));
}

public Controller getHandler(final String requestURI) {
@Override
public Optional<Object> getHandler(final HttpServletRequest request) {
final String requestURI = request.getRequestURI();
log.debug("Request Mapping Uri : {}", requestURI);
return controllers.get(requestURI);

if (controllers.containsKey(requestURI)) {
return Optional.of(controllers.get(requestURI));
}
return Optional.empty();
}
}
38 changes: 28 additions & 10 deletions app/src/main/java/com/techcourse/controller/RegisterController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,38 @@

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

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

@Override
public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception {
final var user = new User(2,
req.getParameter("account"),
req.getParameter("password"),
req.getParameter("email"));
InMemoryUserRepository.save(user);
@RequestMapping(value = "/register", method = RequestMethod.GET)
public ModelAndView show(final HttpServletRequest req, final HttpServletResponse res) {
final String redirectViewName = "redirect:register.jsp";

return new ModelAndView(new JspView(redirectViewName));
}

@RequestMapping(value = "/register", method = RequestMethod.POST)
public ModelAndView registerUser(final HttpServletRequest req, final HttpServletResponse res) {
final String redirectViewName = "redirect:/index.jsp";
saveUser(req);

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

private void saveUser(final HttpServletRequest request) {
final var user = new User(
request.getParameter("account"),
request.getParameter("password"),
request.getParameter("email")
);
InMemoryUserRepository.save(user);
}
}
19 changes: 11 additions & 8 deletions app/src/main/java/com/techcourse/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@

public class User {

private final long id;
private long id;
private final String account;
private final String password;
private final String email;

public User(long id, String account, String password, String email) {
this.id = id;
public User(final String account, final String password, final String email) {
this.account = account;
this.password = password;
this.email = email;
}

public void setId(final long id) {
this.id = id;
}

public boolean checkPassword(String password) {
return this.password.equals(password);
}
Expand All @@ -25,10 +28,10 @@ public String getAccount() {
@Override
public String toString() {
return "User{" +
"id=" + id +
", account='" + account + '\'' +
", email='" + email + '\'' +
", password='" + password + '\'' +
'}';
"id=" + id +
", account='" + account + '\'' +
", email='" + email + '\'' +
", password='" + password + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
package com.techcourse.repository;

import com.techcourse.domain.User;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

public class InMemoryUserRepository {

private static final Map<String, User> database = new ConcurrentHashMap<>();
private static final AtomicLong RESERVED_ID = new AtomicLong(0);

public static final Map<String, User> database = new ConcurrentHashMap<>();

static {
final var user = new User(1, "gugu", "password", "[email protected]");
database.put(user.getAccount(), user);
save(new User("gugu", "password", "[email protected]"));
}

public static void save(User user) {
public static void save(final User user) {
user.setId(RESERVED_ID.incrementAndGet());
database.put(user.getAccount(), user);
}

public static Optional<User> findByAccount(String account) {
public static Optional<User> findByAccount(final String account) {
return Optional.ofNullable(database.get(account));
}

private InMemoryUserRepository() {}
private InMemoryUserRepository() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.techcourse.controller;

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

import java.util.ArrayList;
import java.util.List;
import org.apache.http.HttpResponse;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("/login 경로 요청 테스트")
class LoginControllerTest extends UsingTomcatTest {

@DisplayName("POST로 로그인 정보를 form-data 형식으로 본문에 담아 요청시 유저가 있을 경우, 302 응답 코드와 Location 헤더에 /index.jsp 를 담아 응답한다.")
@Test
void login_success() {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
//given
final HttpPost httpPost = new HttpPost(tomcatUrl + "/login");

final List<BasicNameValuePair> formData = new ArrayList<>();
formData.add(new BasicNameValuePair("account", "gugu"));
formData.add(new BasicNameValuePair("password", "password"));

final UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formData);
httpPost.setEntity(urlEncodedFormEntity);

//when
final HttpResponse response = httpClient.execute(httpPost);

//then
final int actualStatusCode = response.getStatusLine().getStatusCode();
final String actualLocationHeaderValue = response.getFirstHeader("Location").getValue();

assertThat(actualStatusCode).isEqualTo(302);
assertThat(actualLocationHeaderValue).isEqualTo("/index.jsp");
} catch (Exception e) {
Assertions.fail();
e.printStackTrace();
}
}

@DisplayName("POST로 로그인 정보를 form-data 형식으로 본문에 담아 요청시 유저가 없을 경우, 302 응답 코드와 Location 헤더에 /401.jsp 를 담아 응답한다.")
@Test
void login_fail() {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
//given
final HttpPost httpPost = new HttpPost(tomcatUrl + "/login");

final List<BasicNameValuePair> formData = new ArrayList<>();
formData.add(new BasicNameValuePair("account", "gugu"));
formData.add(new BasicNameValuePair("password", "wrong"));

final UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(formData);
httpPost.setEntity(urlEncodedFormEntity);

//when
final HttpResponse response = httpClient.execute(httpPost);

//then
final int actualStatusCode = response.getStatusLine().getStatusCode();
final String actualLocationHeaderValue = response.getFirstHeader("Location").getValue();

assertThat(actualStatusCode).isEqualTo(302);
assertThat(actualLocationHeaderValue).isEqualTo("/401.jsp");
} catch (Exception e) {
Assertions.fail();
e.printStackTrace();
}
}
}
Loading

0 comments on commit 25abc90

Please sign in to comment.