스프링 MVC
1. 스프링 MVC 를 이용한 간단한 웹 애플리케이션 개발
프론트 컨트롤러는 스프링 MVC 의 중심 컴포넌트이다. 아주 단순한 스프링 MVC 애플리케이션이라면 자바 웹 배포 서술자(web.xml 파일이나 ServletContainerInitializer) 에 프론트 컨트롤러의 서블릿만 구성하면 된다. 보통 디스패처 서플릿이라 일컫는 스프링 MVC 컨트롤러는 코어 자바 EE 패턴 중 하나인 프론트 컨트롤러 패턴을 구현한 것으로, MVC 프레임워크에서 모든 웹 요청은 반드시 디스패처 서블릿을 거쳐 처리된다.
스프링 MVC 애플리케이션에 들어온 웹 요청은 제일 먼저 컨트롤러가 접수하고 스프링 웹 애플리케이션 컨텍스트 또는 컨트롤러 자체에 붙인 어노테이션을 이용해 여러 컴포넌트를 조직한다. 위의 그림은 스프링 MVC 에서 요청을 처리하는 흐름이다.
스프링 컨트롤러 클래스에는 @Controller 또는 @RestController 를 붙인다. @Controller 를 붙인 클래스에 요청이 들어오면 스프링은 적합한 Handler Method 를 찾는다. 컨트롤러에는 요청을 처리할 메소드를 하나 이상 매핑하는데, 해당 메소드에 @RequestMapping 을 붙여 핸들러 메소드로 선언한다.
핸들러 메소드의 시그니처는 여느 자바 클래스처럼 정해진 규격이 존재하지 않는다. 메소드명을 임의로 정해도 되고 인자도 다양하게 정의할 수 있으며 어플리케이션 로직에 따라 어떤 값이라도 반환할 수 있다. 이해를 위해 올바른 인자형을 몇 가지 정리해보자면 다음과 같다.
- HttpServletRequest 또는 HttpServletResponse
- 임의형 요청 매개변수 (@RequestParam 을 붙인다)
- 임의형 모델 속성 (@ModelAttribute 를 붙인다)
- 요청 내에 포함된 쿠키값 (@CookieValue 를 붙인다)
- 핸들러 메소드가 모델에 속성을 추가하기 위해 사용하는 Map 또는 ModelMap
- 핸들러 메소드가 객체 바인딩/유효성을 검증한 결과를 가져올 때 필요한 Errors 또는 BindingResult
- 핸들러 메소드가 세션 처리를 완료했음을 알릴 때 사용하는 SessionStatus
컨트롤러는 우선 적절한 핸들러 메소드를 선택하고 이 메소드에 요청 객체를 전달해 처리 로직을 실행한다. 대개 컨트롤러는 백엔드 서비스에 요청 처리를 위임하는 게 보통이고, 핸들러 메소드는 다양한 타입의 인자값에 어떤 정보를 더하거나 삭제해 스프링 MVC 의 흐름을 이어가는 형태로 구성한다.
핸들러 메소드는 요청 처리 후, 제어권을 뷰로 넘긴다. 제어권을 넘길 뷰는 핸들러 메소드의 반환값으로 지정하는데, 직접적인 뷰 구현체보다 파일 확장자가 없는 논리 뷰로 나타내는 편이 유연해서 좋다. 핸들러 메소드는 논리 뷰 이름에 해당하는 String 값을 반환하는 경우가 대부분이다. 반환값을 void 로 설정하면 핸들러 메소드나 컨트롤러 이름에 따라 기본적인 논리 뷰가 자동으로 결정된다.
뷰는 핸들러 메소드의 인자값을 얼마든지 가져올 수 있기 때문에 핸들러 메소드가 논리 뷰 이름을 반환할 경우에도 컨트롤러 → 뷰로 정보를 전달하는 데 아무 영향이 없다. 예를 들어 Map 과 SessionStatus 형 객체를 인자로 받은 핸들러 메소드가 이 내용을 수정해도 이 메소드가 반환하는 뷰에서 똑같이 수정된 객체를 바라볼 수 있다.
컨트롤러 클래스는 뷰를 받고 View Resolver (뷰 해석기)를 이용해 논리 뷰 이름을 실제 뷰 구현체로 해석한다. ViewResolver 인터페이스를 구현한 뷰 해석기는 웹 어플리케이션 컨텍스트에 빈으로 구성하며 논리 뷰 이름을 받아 실제 뷰 구현체를 돌려준다.
컨트롤러 클래스가 논리 뷰 이름을 뷰 구현체로 해석하면 각 뷰의 로직에 따라 핸들러 메소드가 전달한 객체를 Rendering 한다. 뷰의 임무는 어디까지나 핸들러 메소드에 추가된 객체를 유저에게 정확히 보여주는 일이다.
스프링 MVC 애플리케이션 설정
자바 EE 명세에 웹 아카이브 (WAR ; Web ARchive 파일) 를 구성하는 자바 웹 애플리케이션의 디렉토리 구조가 명시되어 있다. 가령, 웹 배포 서술자 (web.xml) 는 WEB-INF 루트에 두거나, 하나 이상의 ServletContainerInitializer 구현 클래스로 구성해야 하고 웹 애플리케이션에 필요한 각종 JAR 파일은 각각 WEB-INF/classes 와 WEB-INF/lib 에 넣어둬야 한다.
NOTE.
스프링 MVC 를 이용해 웹 어플리케이션을 개발하려면 표준 스프링 의존체와 더불어 스프링 웹, 스프링 MVC 의존체를 클래스패스에 추가해야 한다.
Maven 프로젝트는 pom.xml 파일에 다음 코드를 추가한다.
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency>
Gradle 프로젝트는 build.gradle 에 다음 코드를 추가한다.
dependencies { compile "org.springframework:spring-webmvc:${springVersion}" }
CSS 파일과 이미지 파일은 WEB-INF 디렉토리 밖에 위치해 유저가 URL 로 직접 접근할 수 있도록 한다. 스프링 MVC 에서 JSP 파일은 일종의 템플릿 역할을 한다. JSP 는 프레임워크가 동적 콘텐츠를 생성하려고 읽는 파일이므로 WEB-INF 디렉토리 안에 두고 유저가 직접 접근하는 것을 차단한다. 하지만 어떤 어플리케이션 서버는 WEB-INF 내부에 파일을 두면 웹 어플리케이션이 내부적으로 읽을 수 없어 WEB-INF 밖에 둬야 하는 경우도 있다.
구성 파일 작성
웹 배포 서술자 (web.xml 또는 ServletContainerInitializer) 는 자바 웹 어플리케이션의 필수 구성 파일이다. 이 파일에 어플리케이션 서블릿을 정의하고 웹 요청 매핑 정보를 기술한다. 스프링 MVC 의 최전방 컨트롤러에 해당하는 디스패처서블릿 인스턴스는 필요 시 여러 개 정의할 수도 있지만 보통 하나를 사용한다.
대규모 어플리케이션에서 디스패처 서블릿 인스턴스를 여러개 두면 인스턴스마다 특정 URL 을 전담하도록 설계할 수 있어 코드를 관리하기 쉬워진다. 또 개발 팀원 간 서로 방해하지 않고 각자 어플리케이션 로직에 집중할 수도 있다.
package com.apress.springrecipes.court.web;
public class CourtServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext context)
throws ServletException {
...
DispatcherServlet dispatcherServlet = new DispatcherServlet(applicationContext);
ServletRegistration.Dynamic courtRegistration =
context.addServlet("court", dispatcherServlet);
courtRegistration.setLoadOnStartup(1);
courtRegistration.addMapping("/");
}
}
CourtServletContainerInitializer 클래스에서 정의한 DispatcherServlet 은 스프링 MVC 의 핵심 서블릿 클래스로, 웹 요청을 받아 적절한 핸들러에 송부한다. 이 서블릿 이름은 court 라 짓고 "/" 가 포함된 모든 URL 을 매핑한다. URL 패턴은 좀 더 잘게 나눠 저장할 수도 있다. 대규모 어플리케이션이라면 이런 서블릿을 여럿 만들어 URL 패턴 별로 위임하는 게 더 바람직할 수 있다.
CourtServletContainerInitializer 를 스프링이 감지하려면 javax.servlet.ServletContainerInitializer 라는 파일에 다음과 같이 패키지까지 포함된 전체 명칭을 적고 META-INF/services 디렉토리에 추가한다. 서블릿 컨테이너는 이 파일을 로드해 어플리케이션을 시동할 때 사용한다.
com.apress.springrecipes.court.web.CourtServletContainerInitializer
그리고 @Configuration 을 붙인 CourtConfiguration 클래스를 추가하고 @ComponentScan 으로 com.apress.springrecipes.court 패키지를 스캔하여 감지한 빈들을 동작하도록 지시한다.
스프링 MVC 컨트롤러 작성
@Controller 를 붙인 컨트롤러 클래스는 어노테이션만 붙어 있을 뿐 특정 인터페이스를 구현하거나 특정 베이스 클래스를 상속한 클래스가 아니라 평범한 자바 클래스에 불과하다. 컨트롤러에는 하나 이상의 작업을 수행할 하나 이상의 핸들러 메소드를 정의하고 핸들러 메소드에는 앞서 언급한 바와 같이 어떤 정해진 틀 없이 다양한 인자를 선언할 수 있다.
@RequestMapping 은 클래스나 메소드 레벨에 부착 가능한 어노테이션이다. 먼저 컨트롤러 클래스에는 URL 패턴을, 핸들러 메소드에는 HTTP 메소드를 매핑하는 전략을 보자.
@Controller
@RequestMapping("/welcome")
public class WelcomeController {
@RequestMapping(method = RequestMethod.GET)
public String welcome(Model model) {
Date today = new Date();
model.addAttribute("today", today);
return "welcome";
}
}
WelcomeController 클래스는 Date 객체를 생성해 오늘 날짜를 설정하고, 입력받은 Model 객체에 추가해 뷰에서 화면에 표시하도록 한다. @Controller 는 스프링 MVC 컨트롤러 클래스임을 선언하는 어노테이션이다. @RequestMapping 은 프로퍼티를 지정할 수 있고, 클래스/메소드 레벨에 붙일 수 있다. 이 클래스에 붙인 @RequestMapping 의 속성값 "/welcome" 은 이 컨트롤러를 깨어나게 할 URL 이다. URL 이 "/welcome" 인 요청은 모두 이 컨트롤러 클래스로 처리하겠다는 표현이다.
컨트롤러 클래스가 요청을 받게 되면 일단 기본 HTTP GET 핸들러로 선언한 메소드로 넘긴다. 컨트롤러 클래스에서 기본 GET 핸들러 메소드는 @RequestMapping(method = RequestMethod.GET) 을 붙인 welcome() 메소드이다. 기본 HTTP GET 핸들러 메소드가 없으면 ServletException 예외가 발생하므로 스프링 MVC 컨트롤러라면 최소한 URL 경로와 기본 HTTP GET 핸들러 메소드 정도는 갖춰야 한다.
URL 경로와 기본 HTTP GET 핸들러 메소드를 모두 선언한 @RequestMapping 은 다음과 같이 메소드 레벨에도 붙일 수 있다.
@Controller
public class WelcomeController {
@RequestMapping(value = "/welcome", method = RequestMethod.GET)
public String welcome(Model model) { ... }
}
이는 위에서 선언한 바와 동일하게 동작한다. value 속성은 핸들러 메소드가 매핑될 URL 을, method 속성은 이 메소드가 컨트롤러의 기본 GET 핸들러 메소드임을 나타낸다. 이 외에도 @GetMapping / @PostMapping 등 편의성 어노테이션을 활용하면 더 간결하게 작성할 수 있다.
위의 @RequestMapping( ... ) 과 @GetMapping("/welcome") 은 같은 의미를 갖는다.
다음은 주어진 코트의 예약 내역을 조회하는 컨트롤러 코드이다.
@Controller
@RequestMapping("/reservationQuery")
public class ReservationQueryController {
@Autowired
private final ReservationService reservationService;
@GetMapping
public void setupForm() {}
@PostMapping
public String submitForm(@RequestParam("courtName") @NotNull String courtName,
Model model) {
List<Reservation> reservations = reservationService.query(courtName);
model.addAttrivute("reservations", reservation);
return "reservationQuery";
}
}
위 컨트롤러 코드는 앞서 예시한 코드와 달리, setupForm() 메소드가 기본 GET 핸들러 메소드임에도 어떤 매개변수도, 반환값도, 메소드 바디도 존재하지 않는다. 이는 두 가지를 의미하는데, 첫 번째로 입력 매개변수와 메소드 바디가 없는 건 컨트롤러에서 데이터는 하나도 추가되지 않으니 구현체 템플릿에서 하드코딩된 데이터를 뷰에서 보여준다는 것이다. 두 번째로 반환값이 없는 것은 기본 뷰의 이름이 요청 URL 에 의해 결정된다는 것이다. 예를 들어, 요청 URL 이 "/reservationQuery" 면 reservationQuery 라는 이름의 뷰가 반환되는 셈이다.
이미 알다시피 HTTP 요청은 대부분 GET 방식이고, POST 방식은 보통 사용자가 HTML 폼을 전송할 때에 사용된다. 따라서 어플리케이션 뷰 관점에서는 HTML 폼을 초기 로드할 경우 호출하는 메소드와 HTML 폼 전송 시 호출하는 메소드를 각각 두는 편이 더 명확하게 어플리케이션을 정의할 수 있다.
위에서 submitForm() 메소드는 두 입력 매개변수를 받는다. 첫 번째로 @RequestParam("courtName") 은 요청 매개변수 중 courtName 을 추출해 사용하겠다는 선언이다. "/reservationQuery?courtName=<코트명>" URL 로 POST 요청을 하면 코트명 을 courtName 이라는 변수로 받게 된다. 두 번째로 Model 은 나중에 반환할 뷰에 넘길 데이터를 담아둘 객체이다.
이 메소드는 제일 마지막에 "reservationQuery" 를 반환하는데, 이는 위에서 언급한 바와 같이 reservationQuery 뷰를 반환하는 것이다. 이는 또한 setupForm() 메소드가 아무 반환값이 없어 URL 을 반환하는 것과 동일하다.
JSP 뷰 작성
스프링 MVC 에는 JSP, HTML, PDF, 엑셀 워크시트, XML, JSON, 아톰, RSS 피드, JasperReports 및 각종 서드파티 뷰 구현체 등 여러 가지 표현 기술별로 다양한 뷰가 준비되어 있다. 스프링 MVC 애플리케이션의 뷰는 JSTL (Java Standard Tag Library) 이 가미된 JSP 템플릿이 대부분이다. web.xml 파일에 정의된 DispatcherServlet 은 핸들러가 전달한 논리적인 뷰 이름을 실제 렌더링할 뷰 구현체로 해석한다. 이를테면 CourtConfiguration 구성 클래스에서 다음과 같이 InternalResourceViewResolver 빈을 구성하면 웹 어플리케이션 컨텍스트가 논리 뷰 이름을 /WEB-INF/jsp 디렉토리에 있는 실제 JSP 파일로 해석한다.
@Bean
public InternalResourceViewResolver internalResourceViewResolver {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
즉, 컨트롤러가 reservationQuery 라는 논리 뷰 이름을 넘기면 /WEB-INF/jsp/reservationQuery.jsp 라는 뷰 구현체로 처리가 위임된다.
welcome 컨트롤러용 JSP 템플릿 (welcome.jsp) 을 다음과 같이 작성한다.
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
<title>Welcome</title>
</head>
<body>
<h2>Welcome to Court Reservation System</h2>
Today is <fmt:formatDate value="${today}" pattern="yyyy-MM-dd"/>.
</body>
</html>
JSTL fmt 태그 라이브러리를 이용해 모델 속성 today 를 "yyyy-MM-dd" 형식으로 맞췄다. 태그 라이브러리는 JSP 템플릿의 최상단에 반드시 선언해야 한다.
다음은 ReservationQuery 컨트롤러용 JSP 템플릿 (reservationQuery.jsp) 이다.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
<title>Reservation Query</title>
</head>
<body>
<form method="post">
Court Name
<input type="text" name="courtName" value="${courtName}"/>
<input type="submit" value="Query"/>
</form>
<table border="1">
<tr>
<th>Court Name</th>
<th>Date</th>
<th>Hour</th>
<th>Player</th>
</tr>
<c:forEach items="${reservations}" var="reservation">
<tr>
<td>${reservation.courtName}</td>
<td><fmt:formatDate value="${reservation.date}" pattern="yyyy-MM-dd"/></td>
<td>${reservation.hour}</td>
<td>${reservation.player.name}</td>
</tr>
</c:forEach>
</table>
</body>
</html>
유저가 코트 이름을 입력하는 폼이 하나 있고, <c:forEach> 태그를 사용해 reservations 객체를 순회하며 HTML <table> 엘리먼트를 생성한다.
WebApplicationInitializer 로 애플리케이션 시동
앞서 웹 애플리케이션을 시동하려면 CourtServletContainerInitliazer 를 작성하면서 META-INF/services/javax.servletServletContainerInitializer 파일도 함께 만들어야 한다고 설명했다. 이 작업을 개발자가 직접 할 수도 있지만 스프링의 SpringServletContainerInitializer 를 쓰면 매우 간편하다. ServletContainerInitializer 인터페이스를 구현한 SpringServletContainerInitalizer 는 클래스패스에서 WebApplicationInitializer 인터페이스 구현체를 찾는다. WebApplicationInitializer 인터페이스 구현체는 이미 스프링에 몇 가지 준비되어 있기 때문에 편하게 골라 사용이 가능하다. AbstractAnnotationConfigDispatcherServletInitilaizer 도 그 중 하나이다.
public class CourtWebApplicationInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{ServiceConfiguration.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{WebConfiguration.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
이와 같이 클래스를 잓어하면 DispatcherServlet 은 이미 생성된 것과 다름없다. 덕분에 개발자는 getServletMappings() 메소드에서 매핑을 설정하고 getServletConfigClasses() 메소드에서 로드할 구성 클래스를 지정하는 일만 신경쓰면 된다. 서블릿 다음의 ContextLoaderListener 컴포넌트 역시 선택적으로 구성할 수 있다. ServletContextListener 인터페이스의 구현체인 ContextLoaderListener 는 ApplicationContext 를 생성하고 ApplicationContext 가 바로 DispatcherServlet 에서 상위 ApplicationContext 로 사용된다. 여러 서블릿이 같은 빈에 접근할 때 편리한 메커니즘이다.
#Reference.
위에서 사용한 예제 코드는 https://github.com/nililee/spring-5-recipes 의 코드를 참고한 것입니다. 전체 코드가 필요한 경우는 해당 경로에서 소스 코드를 참고하면 됩니다.
'Study > Spring' 카테고리의 다른 글
[Spring 5 Recipes] Spring 5 Recipes 3장 정리 #2 (0) | 2021.09.30 |
---|---|
[Spring] 스프링 디스패처 서블릿 (Dispatcher Servlet) (0) | 2021.09.30 |
[Spring 5 Recipes] Spring 5 Recipes 2장 정리 #8 (0) | 2021.09.28 |
[Spring 5 Recipes] Spring 5 Recipes 2장 정리 #7 (0) | 2021.09.27 |
[Spring 5 Recipes] Spring 5 Recipes 2장 정리 #6 (0) | 2021.09.24 |