java-school logo

구글 앱 엔진 프로젝트에 Spring MVC 적용하기

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

webapp/resources/는 웹 애플리케이션의 정적 요소를 위해 만들었다.
웹 애플리케이션에서 정적인 요소란 이미지, 스타일 시트, 자바스크립트, HTML 파일을 말한다.
webapp/WEB-INF/views/는 jsp 파일을 위해 만들었다.
그래서 guestbook.jsp 파일은 webapp/guestbook.jsp에서 webapp/WEB-INF/views/guestbook/guestbook.jsp로 옮겼다.
스프링 프로젝트에선 src/main/webapp에는 favicon.ico 파일 외 어떤 파일도 두지 않는 게 좋다.
favicon.ico는 웹 브라우저 주소창 왼쪽에 보이는 작은 이미지를 말한다.

웹 애플리케이션에 스프링 MVC를 적용하기 위해선 다음 과정이 필요하다.

pom.xml
<properties>
	<!--  omit -->
	<spring.version>4.3.9.RELEASE</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 
  version="2.5"
  xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <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] -->
    <filter>
        <filter-name>ObjectifyFilter</filter-name>
        <filter-class>com.googlecode.objectify.ObjectifyFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>ObjectifyFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <listener>
        <listener-class>com.example.guestbook.OfyHelper</listener-class>
    </listener>
    <!-- [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" 
    xmlns:p="http://www.springframework.org/schema/p"
    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라고 해석할 것이다.

com.example.guestbook 패키지에 방명록(Guestbook)을 전담하는 컨트롤러를 생성한다.

GuestbookController.java
package net.java_school.guestbook;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

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 {

	@RequestMapping(value="/guestbook", method=RequestMethod.GET)//1."/guestbook" 요청 핸들러
	public String guestbook(String guestbookName, Model model) {
		model.addAttribute("guestbookName", guestbookName);
		return "guestbook/guestbook";
	}
    
	@RequestMapping(value="/guestbook/sign", method=RequestMethod.POST)//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 서블릿 코드 그대로다.

guestbook.jsp 수정

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

<link type="text/css" rel="stylesheet" href="/resources/stylesheet/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으로 수정한다.

appengine-web.xml 수정

자바 8 환경에서 앱을 실행하려면, 다음을 appengine-web.xml에 추가해야 한다.
이 설정을 추가하지 않으면 Spring에서 에러가 발생한다.

<runtime>java8</runtime>

로컬 테스트

명령 프롬프트에서 다음을 실행하여 로컬 테스트를 진행한다.

mvn clean
mvn appengine:devserver

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

서버 테스트

mvn appengine:update

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

참고