스프링 MVC

guestbook 디렉터리 구조를 Spring MVC에 맞게 아래와 같이 바꾼다.
before_after

webapp/resources/

webapp/resources/는 웹 애플리케이션의 정적 요소를 위해 만들었다. 웹 애플리케이션에서 정적인 요소란 이미지, 스타일 시트, 자바스크립트, HTML 파일을 말한다.

webapp/WEB-INF/views/

webapp/WEB-INF/views/는 jsp 파일을 위해 만들었다. 그래서 guestbook.jsp 파일을 webapp/에서 webapp/WEB-INF/views/로 옮겼다.

스프링 MVC를 적용하기 위해선 다음 과정이 필요하다.

  • pom.xml에 스프링 의존성 추가
  • web.xml에 디스패처 서블릿 설정을 추가
  • [비영어권 사이트는 web.xml에 인코딩 필터 추가]
  • 디스패처 서블릿 이름을 기반으로 스프링 설정 파일 생성 (예, 디스패처 서블릿 이름-servlet.xml)
pom.xml
<properties>
  <!--  omit -->
  <spring.version>5.3.33</spring.version>
</properties>

<!--  omit -->

<!-- [START Spring_Dependencies] -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>${spring.version}</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>${spring.version}</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-jdbc</artifactId>
  <version>${spring.version}</version>
</dependency>
<!-- [END Spring_Dependencies] -->

<!--  omit -->
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
  version="4.0">
  
  <filter>
    <filter-name>encodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
      <param-name>forceEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  
  <filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  
  <!-- [START Objectify] -->

  <!--  omit -->

  <!-- [END Objectify] -->
  
  <servlet>
    <servlet-name>guestbook</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>guestbook</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
  
</web-app>
  • 다국어 사이트를 위해 요청 문자열을 UTF-8로 인코딩하는 필터를 추가
  • 디스패처 서블릿을 guestbook이란 이름으로 추가했고 '/'로 매핑
  • welcome-file-list 제거

디스패처 서블릿 이름이 guestbook 이므로 스프링 설정 파일을 guestbook-servlet.xml란 이름으로 web.xml 파일이 있는 위치에 생성한다.

guestbook-servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:mvc="http://www.springframework.org/schema/mvc" 
  xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context.xsd
      http://www.springframework.org/schema/mvc
      http://www.springframework.org/schema/mvc/spring-mvc.xsd">
  <mvc:resources mapping="/resources/**" location="/resources/" /><!--1.정적 콘텐츠 요청 처리-->
  <mvc:annotation-driven /><!--2.애너테이션 기반으로 스프링 구동-->
  <context:component-scan base-package="net.java_school.guestbook" /><!--3.컴포넌트 스캔-->
  <bean id="internalResourceViewResolver" 
      class="org.springframework.web.servlet.view.InternalResourceViewResolver"><!--4.뷰 리졸버-->
    <property name="viewClass">
      <value>org.springframework.web.servlet.view.JstlView</value>
    </property>
    <property name="prefix">
      <value>/WEB-INF/views/</value>
    </property>
    <property name="suffix">
      <value>.jsp</value>
    </property>
  </bean>
</beans>
  1. webapp/resources 나 그 하위 디렉터리에 있는 정적 콘텐츠 (스타일 시트, 이미지, 자바스크립트 등등)에 대한 요청을 처리하기 위한 설정이다.
  2. 애너테이션 기반으로 스프링을 구동하기 위한 설정이다.
  3. 지정한 패키지에서 컴포넌트를 스캔해서 빈 컨테이너에 등록하게 하는 설정이다.
  4. 뷰 리졸버는 컨트롤러에서 온 문자열을 해석하여 뷰를 결정한다. 컨트롤러가 "guestbook/guestbook"를 리턴한다면, 위에서 설정한 뷰 리졸버는 prifix와 suffix를 사용해서 뷰를 /WEB-INF/views/guestbook/guestbook.jsp라고 해석한다.

방명록(Guestbook)을 전담하는 컨트롤러를 생성한다.

GuestbookController.java
package net.java_school.guestbook;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.googlecode.objectify.ObjectifyService;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Controller
public class GuestbookController {

  @GetMapping("/guestbook")//1."/guestbook" 요청 핸들러
  public String guestbook(String guestbookName, Model model) {
    model.addAttribute("guestbookName", guestbookName);
    return "guestbook/guestbook";
  }
    
  @PostMapping("/guestbook/sign")//2."/guestbook/sign" 요청 핸들러
  public String sign(HttpServletRequest req, HttpServletResponse resp) {
    Greeting greeting;
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();
    String guestbookName = req.getParameter("guestbookName");
    String content = req.getParameter("content");
    if (user != null) {
      greeting = new Greeting(guestbookName, content, user.getUserId(), user.getEmail());
    } else {
      greeting = new Greeting(guestbookName, content);
    }
    // Use Objectify to save the greeting and now() is used to make the call synchronously as we
    // will immediately get a new page using redirect and we want the data to be present.
    ObjectifyService.ofy().save().entity(greeting).now();
    return "redirect:/guestbook/?guestbookName=" + guestbookName;
  }
}
  1. guestbook() 메소드는 GET 방식의 "/guestbook" 요청을 담당한다.
  2. sign() 메소드는 POST 방식의 "/guestbook/sign" 요청을 담당한다. sign() 메소드 내용은 SignGuestbookServlet.java 서블릿 코드 그대로다.

컨트롤러가 제대로 동작하기 위해서 서블릿인 GuestbookServlet.java와 SignGuestbookServlet.java와 src/test/java/ 디렉터리에 있는 테스트를 위한 자바 클래스를 모두 삭제한다.

guestbook.jsp 수정

스타일 시트 파일 위치를 변경했으므로 guestbook.jsp에서 스타일 시트 경로를 수정한다.

<link type="text/css" rel="stylesheet" href="/resources/stylesheets/main.css"/>  

guestbook.jsp에서 폼 action 속성을 GuestbookController의 요청 핸들러에 맞게 수정한다.

<form action="/guestbook/sign" method="post">
<form action="/guestbook" method="get">

guestbook.jsp의 위치를 변경했으므로, guestbook.jsp에서 createLogoutURL()와 createLoginURL() 아규먼트를 그대로 두면 두 메소드는 /WEB-INF/views/guestbook/guestbook.jsp를 반환한다. 결국 404 에러를 발생한다. 두 메소드의 아규먼트를 request.getRequestURI()에서 "/guestbook/?guestbookName=" + guestbookName으로 수정한다.

<p>Hello, ${fn:escapeXml(user.nickname)}! (You can
  <a href="<%= userService.createLogoutURL("/guestbook/?guestbookName=" + guestbookName) %>">sign out</a>.)</p>
<%
  } else {
%>
<p>Hello!
  <a href="<%= userService.createLoginURL("/guestbook/?guestbookName=" + guestbookName) %>">Sign in</a>
  to include your name with greetings you post.</p>
<%
  }
%>

자바 8 환경에서 앱을 실행하려면, appengine-web.xml에 다음 설정이 있어야 한다.

<runtime>java8</runtime>

로컬 테스트

mvn clean
mvn appengine:run

http://localhost:8080/guestbook을 방문한다.

서버 테스트

mvn appengine:deploy

http://your-app-id.appspot.com/guestbook을 방문한다.

참고