개발 공부/Spring

코드로 배우는 스프링 웹 프로젝트 - Part2 : 스프링 MVC 설정

maxlafe 2020. 7. 10. 23:47

코드로 배우는 스프링 웹 프로젝트 - 개정판

코드로 배우는 스프링 웹 프로젝트 - Part2 : 스프링 MVC 설정

2019년 7월 10일 인쇄판

 

Part2. 스프링 MVC 설정

 

Chapter 05. 스프링 MVC의 기본 구조

 

스프링 MVC는 스프링의 서브 프로젝트이다.

https://spring.io/projects를 를 보면 SPring Framework라는 메인 프로젝트 외에도 여러 서브 플젝이 있는데 스프링 MVC도 이 중 하나.

 

스프링은 하나의 기능을 위해 만들어진 프레임웤이 아니라 코어라고 할 수 있는 framework에서 여러 서브 플젝을 결합해 다양한 상황에 대처할 수 있도록 개발되었따.

즉 "별도의 설정이 존재할 수있다"는 개념.

Spring legacy project에서도 servlet-context.xml과 root-context.xml로 설정 파일이 분리된 것을 볼 수 있다. 스프링 MVC가 서브 프로젝트이므로 구성 방식이나 설정 역시 조금 다른 것.

 

XML 설정

Spring MVC - servlet.context.xml

Spring Core

My Batis -- root-context.xml

DB

 

Java 설정

Spring MVC -- ServletConfig.class

Spring Core

MyBatis -- RootConfig.class

DB

 

5.1 스프링 MVC 프로젝트의 내부 구조

 

내부적으로 root-context.xml로 사용하는 일반 Java 영역(흔히 POJO)과 servlet-context.xml로 설정하는 Web관련 영역을 같이 연동해 구동하게 된다.

 

WeApplicationContext 안에 MVC 설정과 일반 설정이 함께 있게 된다.

WebApplicationContext는 기존의 구조에 MVC 설정을 포함하는 구조로 만들어진다. 스프링은 원래 목적이 웹 앱을 목적으로 나온 프레임워크가 아니므로 달라지는 영역에대해선 완전히 분리하고 나서 연동하는 방식으로 구현되어 있다.

 

 

	<version>1.0.0-BUILD-SNAPSHOT</version>
	<properties>
		<java-version>1.8</java-version>
		<org.springframework-version>5.0.7.RELEASE</org.springframework-version>
		<org.aspectj-version>1.6.10</org.aspectj-version>
		<org.slf4j-version>1.6.6</org.slf4j-version>
	</properties>
	<dependencies>
			<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>
		<dependency><!--  추가  -->
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.0</version>
			<scope>provided</scope>
		</dependency>
		
		<dependency>
			<groupId>log4j</groupId>
			<artifactId>log4j</artifactId>
			<version>1.2.17</version>
  <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.5.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArgument>-Xlint:all</compilerArgument>
                    <showWarnings>true</showWarnings>
                    <showDeprecation>true</showDeprecation>
                </configuration>
            </plugin>

가능하면 Tomcat-Modules에서 /경로로 프로젝트가 실행될 수 있도록 처리하자.

 

5.1.1 Java 설정을 이용하는 경우

 

web.xml과 servlet-context.xml, root-context.xml 제거

 

            <plugin>
            	<groupId>org.apache.maven.plugins</groupId>
            	<artifactId>maven-war-plugin</artifactId>
            	<version>3.2.0</version>
            	<configuration>
            		<failOnMissingWebXml>false</failOnMissingWebXml>
            	</configuration>
            </plugin>
package org.zerock.config;

import org.springframework.context.annotation.Configuration;

@Configuration
public class RootConfig {
	
}
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import org.zerock.config.RootConfig;

public class WebConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
	@Override
	protected Class<?> [] getRootConfigClasses() {
		return new Class[] {RootConfig.class};
	}
	
	@Override
	protected Class<?> [] getServletConfigClasses() {
		return null;
	}
	
	@Override
	protected String [] getServletMappings() {
		return null;
	}
}

 servlet-context.xml을 대신하는 ServletConfig 클래스 작성.

- @EnableWebMvc 어노테이션과 WebMvcConfigurer 인터페이스를 구현하는 방식

- @Configuration과 WebMvcConfigurationSupport 클래스를 상속하는 방식 : 일반 @Configuration 우선 순위가 구분되지 않는 경우에 사용

 

package org.zerock.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

@EnableWebMvc
@ComponentScan(basePackages= {"org.zerock.controller"})
public class ServletConfig implements WebMvcConfigurer{
	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		InternalResourceViewResolver bean = new InternalResourceViewResolver();
		bean.setViewClass(JstlView.class);
		bean.setPrefix("/WEB-INF/views/");
		bean.setSuffix(".jsp");
		registry.viewResolver(bean);
	}
	
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
	}
}

WebConfigurer는 스프링 MVC와 관련된 설정을 메서드로오버라이드 하는 형태로 사용.

ServletConfig 클래스 역시 @ComponentScan을 이용해 다른 패키지의 스프링을 객체로 인식할 수 있다.

 

package org.zerock.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
	@Override
	protected Class<?> [] getRootConfigClasses() {
		return new Class[] {RootConfig.class};
	}
	
	@Override
	protected Class<?> [] getServletConfigClasses() {
		return new Class[] {ServletConfig.class};
	}
	
	@Override
	protected String [] getServletMappings() {
		return new String[] {"/"};
	}
}

 

5.2 예제 프로젝트의 로딩 구조

 

web.xml은 Tomcat구동과 관련된 설정이고 나머지 두 파일은 스프링과 관련

 

<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/root-context.xml</param-value>
	</context-param>
	
	<!-- Creates the Spring Container shared by all Servlets and Filters -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

web.xml의 상단에는 가장 먼저 구동하는 Context Listener가 등록되어 있다.

<context-param>에는 root-context.xml의 경로가 설정되어 있고 listener에는 스프링 MVC의 ContextLoaderListener가 등록되어 있따.

 

root-context.xml이 처리되면 파일에 있는 빈 설정들이 동작하게 된다.

 

root-context.xml에 정의된 객체들은 스프링의 영역 아넹 생성되고 객체들 간의 의존성이 처리된다.

이후에는 스프링 MVC에서 사용하는 DispatcherServlet이라는 서블릿과 관련된 설정이 작동한다.

 

<!-- Processes application requests -->
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
		
	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>

org.springframework.web.servler.DispathcherServlet 클래스는 스프링 MVC의 구조에서 가장 핵심적이다.

내부적으로 웹 처리 준비작업을 진행하는데 이때 사용하는 파일이 servlet-context.xml이다.

 

DispatcherServlet에서 XmlWebApplicationContext를 이용해 servlet-context.xml을 로딩하고 해석한다.

이 과저에서 등록된 객체들은 기존에 만들어진 객체들과 연동된다.

 

즉, root-context.xml에서 정의된 Bean들 -> servlet-context.xml에서 정의된 Bean들 -> 두 Bean들 간의 연동

 

 

5.3 스프링 MVC의 기본 사상

 

Servlet/JSP에서는 HttpServletRequest/HttpServletResponse라는 타입 객체로 브라우저에서 전송한 정보를 처리한다. 스프링 MVC의 경우 이 위에 하나의 계층을 더한다

 

개발자의 코드 영역 -- 개발자는 Servlet/JSP의 API에 신경쓰지 않고 웹 앱 제작

----

Spring MVC -- Spring MVC는 내부적으로 Servlet JSP 처리

---

Servlet/JSP

 

스프링 MVC를 이용하면 개발자들은 직접적으로 HTtpServletRequest/HttpServletResponse와 같이 Servlet/JSP의 API를 사용할 필요성이 현저히 줄어든다.

 

 

5.4 모델 2와 스프링 MVC

 

스프링 MVC 역시 내부적으론 Servlet API를 활용한다.

스플이 MVC는 모델2라는 방식으로 처리되므로 모델2를 살펴보자.

 

모델2 방식은 쉽게 말해 룆꽈 화면을 분리하는 스타일의 개발 방식

 

Request는 특별한 상황이 아닌 이상 먼저 Controller를 호출한다.

이렇게 설계하는 가장 중요한 이유는 나중에 View를 교체해도 사용자가 호출하는 URL 자체에 변화가 없게 하기 위함이다. 컨트롤러는 데이터 처리하는 존재를 이용해 데이터(Model)을 처리하고 Response할 때 필요한 데이터를 View쪽으로 전달한다. 

Servlet을 이용하는 경우 개발자들은 SErvlet API의 RequestDispatcher를 이용해 직접 처리했지만 스프링 MVC는 내부에서 이러한 처리를 할 수 있다.

 

아래는 스프링 MVC의 기본 구조.

 

(1) 사용자의 REquest는 Front-controller인 DispatcherServlet을 통해 처리한다.

프로젝트의 web.xml을 보면 모든 Request를 DispatcherServlet이 받도록 함

 

(2)(3) HandlerMapping은 Request의 처리를 담당하는 컨트롤러를 찾기 위해 존재.

HandlerMapping interface를 구현한 여러 객체들 중 RequestMappingHandlerMapping 같은 경우개발자가 @RequestMapping 어노테이션이 적용된 것을 기준으로 판단한다. 적절한 컨트롤러가 찾아졌다면 HandlerAdapter를 이용해 해당 컨트롤러를 동작시킴

 

(4) Controller는 개발자가 작성하는 클래스로 실제 Request를 처리하는 로직을 작성

이 때 View에 전달해야 하는 데이터는 주로 Model 객체에 담아 전달. Controller는 다양한 타입의 결과를 반환해 이에 대한 처리를 ViewResolver가 담당한다

 

(5) ViewResolver는 COntroller가 반환한 결과를 어떤 View를 통해 처리하는 것이 좋을지 해석한다.

servlet-context.xml에 정의된 InternalResourceViewResolver 설정이 흔히 사용됨

 

(6)(7) View는 실제로 응답해야 하는 데이터를 JSP 등을 이용해 생성. 응답은 DispatcherServlet을 통해 전송됨

 

모든 Request는 DispatcherServlet을 통하도록 설계되는데 이런 방식은 Front-controller라고 한다. 전체 흐름을 강제로 제한할 수 있다.

 

 

 

Chapter06. 스프링 MVC의 Controller

 

스프링 MVC를 이용하는 경우 Controller는 다음과 같은 특징이 있다.

 

- HttpServletRequest, Response가 거의 사용될 필요 없이 필요한 기능 구현

- 다양한 타입 parameter 처리, return type 사용 가능

- GET, POST 방식 등 전송 방식에 대한 처리를 annotation으로

- 상속/interface 대신 annotation만으로도 설정 가능

 

6.1 @Controller, @RequestMapping

 

org.zerock.controller 내에 SampleController class

 

package org.zerock.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/sample/*")
public class SampleController {

}

@Controller라는 annotation 적용, 작성된 클래스는 스프링의 객체로 등록되는데, servlet-context.xml의 이 부분 때문에 그러하다.

	
	<context:component-scan base-package="org.zerock.controller" />
	

지정된 패키지를 스캔하도록 설정되어 있다. 

객체로 등록된 클래스는 's' 모양이 함께 표시된다.

클래스 선언부에는 @Controller와 함께 @RequestMapping을 많이 사용한다.

 

@RequestMapping은 현재 클래스의 모든 메서드들의 기본 URL 경로가 된다.

예를 들어 SampleController 클래스는 다음과 같이 "/sample/*" 경로로 지정했다면 다음 경로는 모두 이 클래스에서 처리된다. (ex /sample/aaa, /sample/bbb)

 

@RequestMapping 어노테이션은 클래스의 선언과 메서드 선언에 사용할 수 있다.

 

package org.zerock.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import lombok.extern.log4j.Log4j;

@Controller
@RequestMapping("/sample/*")
@Log4j
public class SampleController {
	@RequestMapping("")
	public void basic() {
		log.info("basic.......");
	}
}

 

여기서 Log4j 에러가 난다면 pom.xml에서 scope-runtime 속성을 제거한다.

 

 

 

현재 프로젝트의 경우 /와 /sample/*는 호출이 가능한 경로임을 확인할 수 있다.

 

6.2 @RequestMapping의 변화

 

@Controller 어노테이션은 추가 속성을 지정할 수 없지만 @RequestMapping의 경우 몇 가지 속성 추가 가능.

 

@RequestMapping을 줄여서 사용할 수 있는 @GetMapping, @PostMapping이 등장한다.

 

package org.zerock.controller;

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

import lombok.extern.log4j.Log4j;

@Controller
@RequestMapping("/sample/*")
@Log4j
public class SampleController {
	@RequestMapping("")
	public void basic() {
		log.info("basic.......");
	}
	
	@RequestMapping(value="/basic", method= {RequestMethod.GET, RequestMethod.POST})
	public void basicGet() {
		log.info("basic get........");
	}
	
	@GetMapping("/basicOnlyGet")
	public void basicGet2() {
		log.info("basic get only get.........");
	}
}

@RequestMApping은 GET, POST 방식 모두 지원해야 하는 경우 배열로 처리해 지정 가능.

@GetMapping은 GET 방식에만 사용 가능

 

 

6.3 Controller의 파라미터 수집

 

Controller를 작성하면 가장 편리한 기능이 파라미터가 자동으로 수집되는 것이다.

매번 request.getParameter()를 하는 불편함을 없앨 수 있다.

 

org.zerock.domain 밑에 SampleDTO 작성

 

package org.zerock.domain;

import lombok.Data;

@Data
public class SampleDTO {
	private String name;
	private int age;
}

SampleController의 메서드가 SampletDTO를 파라미터로 사용하면 자동으로 setter 메서드가 파라미터를 수집한다.

 

package org.zerock.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.zerock.domain.SampleDTO;

import lombok.extern.log4j.Log4j;

@Controller
@RequestMapping("/sample/*")
@Log4j
public class SampleController {
	@RequestMapping("")
	public void basic() {
		log.info("basic.......");
	}
	
	@RequestMapping(value="/basic", method= {RequestMethod.GET, RequestMethod.POST})
	public void basicGet() {
		log.info("basic get........");
	}
	
	@GetMapping("/basicOnlyGet")
	public void basicGet2() {
		log.info("basic get only get.........");
	}
	
	@GetMapping("/ex01")
	public String ex01(SampleDTO dto) {
		log.info("" + dto);
		return "ex01";
	}
}

SampleController의 경로가 /sample/* 이므로 ex01() 메서드를 호출하는 경로는 /sample/ex01이다.

메서드가 @GetMapping이므로 URL 뒤에 ?name=AAA&age=10 형태로 매개변수 추가 가능.

 

 

 

 

6.3.1 파라미터의 수집과 변환

 

Controller가 파라미터 수집하는 방식은 파라미터 타입에 따라 자동으로 변환하는 방식.

 

기본 자료형이나 문자열 등을 이용한다면 파라미터의 타입만을 맞게 선언해주는 방식을 사용할 수 있다.

 

	
	@GetMapping("/ex02")
	public String ex02(@RequestParam("name") String name, @RequestParam("age") int age) {
		log.info("name: " + name);
		log.info("age: " + age);
		return "ex02";
	}
}

 

 

 

6.3.2 리스트, 배열 처리

 

동일한 이름의 파라미터가 여러 개 전달되면 ArrayList<>이용

 

	@GetMapping("/ex02List")
	public String ex02List(@RequestParam("ids") ArrayList<String> ids) {
		log.info("ids: " + ids);
		return "ex02List";
	}

스프링은 파라미터 타입을 보고 객체를 생성하므로 파라미터 타입은 List<>와 같이 인터페이스가 아닌 실제 클래스 타입으로 지정.

	@GetMapping("/ex02Array")
	public String ex02Array(@RequestParam("ids") String[] ids) {
		log.info("ids: " + Arrays.toString(ids));
		return "ex02List";
	}

배열의 경우도 비슷하게 처리 가능

 

 

6.3.3 객체 리스트

 

만일 전달하는 데이터가 SampleDTO 처럼 객체 타입이고 여러 개를 처리해야 한다면, 한 번에 처리 가능.

SampleDTOList 클래스 설계

 

package org.zerock.domain;

import java.util.ArrayList;
import java.util.List;

import lombok.Data;

@Data
public class SampleDTOList {
	private List<SampleDTO> list;
	
	public SampleDTOList() {
		list = new ArrayList<>();
	}
}
@GetMapping("/ex02Bean")
	public String ex02Bean(SampleDTOList list) {
		log.info("list dtos: " + list);
		return "ex02Bean";
	}

프로젝트 경로/sample/ex02Bean?list[0].name=aaa&list[2].name=bbb

 

Tomcat은 버전에 따라 [] 문자를 특수문자로 허용하지 않을 수 있다.

[는 %5B로, ]는 %5D로 변경하면 된다.

 

list%5B0%5D.name=aaa&list%5B1%5D.name=bbb

 

 

 

6.3.4 @InitBinder

 

파라미터의 수집을 binding이라고 한다. 변환 가능한 데이터는 자동 변환되지만 경우에 따라 파라미터를 변환해 처리해야 할 수도 있다.

 

org.zerock.domain 밑에 TodoDTO.java

 

package org.zerock.domain;

import lombok.Data;

@Data
public class TodoDTO {
	private String title;
	private Date dueDate;
}
	@InitBinder
	public void initBinder(WebDataBinder binder) {
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
		binder.registerCustomEditor(java.util.Date.class, new CustomDateEditor(dateFormat, false));
	}
	
	@GetMapping("/ex03")
	public String ex03(TodoDTO todo) {
		log.info("todo: " + todo);
		return "ex03";
	}

http://localhost:8080/sample/ex03?title=test&dueDate=2018-01-01

 

 

6.3.5 @DateTimeFormat

 

@InitBinder를 이용해 날짜를 변환할 수도 있지만 parameter로 사용되는 인스턴스 변수에 @DateTimeFormat을 적용해도 변환이 가능.

package org.zerock.domain;

import java.sql.Date;

import org.springframework.format.annotation.DateTimeFormat;

import lombok.Data;

@Data
public class TodoDTO {
	private String title;
	
	@DateTimeFormat(pattern="yyyy/MM/dd")
	private Date dueDate;
}

 

 

이 경우 InitBinder는 필요하지 않다.

(지워줘야 오류 안 생김)

 

** 오류 생김

 

 

6.4 Model이라는 데이터 전달자

 

Controller의 메서드를 작성할 때 특별히 Model이라는 타입을 파라미터로 지정할 수 있다. Model 객체는 JSP에 컨트롤러에서 생성된 데이터를 담아 전달하는 역할을 한다.

이를 이용해 JSP와 같은 뷰로 전달해야 하는 데이터를 보낼 수 있따.

메서드의 파라미터에 Model 타입이 지정된 경우 스프링은 특별하게 Model 타입의 객체를 만들어 메서드에 주입한다.

 

Servlet에서 모델 2방식으로 데이터 전달하는 방식

 

request.setAttribute("serverTime", new java.util.Date());

RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/jsp/home.jsp");

dispatcher.forward(request, response);

 

스프링 MVC에서 Model을 이용한 데이터 전달

public String home(Model model) {

    model.setAttribute("serverTime", new java.util.Date());

    return "home";

}

 

메서드의 파라미터를 Model로 선언하면 자도응로 스프링 MVC에서 Model 타입의 객체를 만들어준다.

Model을 사용해야 하는 경우는 주로 Controller에 전달된 데이터를 이용해 추가적인 데이터를 가져와야 하는 상황.

 

- 리스트 페이지 번호를 파라미터로 전달받고 실제 데이터를 Viewㄹ 전달하는 경우

- 파라미터들에 대한 처리 후 결과 전달하는 경우

 

6.4.1 @ModelAttribute 어노테이션

 

웹페이지 구조는 REquest에 전달된 데이터를 가지고 필요 시 추가 데이터를 화면에 전달하는 방식으로 동작함

Model의 경우 파라미터로 전달된 데이터는 존재하지 않지만 화면에서 필요한 데이터를 전달하기 위해 사용함

예를 들어 페이지 번호는 파라미터로 전달되지만 결과 데이터를 전달하려면 Model에 담아 전달

 

스프링 MVC의 Controller는 기본적으로 Java beans 규칙에 맞는 객체를 다시 화면으로 객체를 전달한다.

Java Beans의 규칙은 좁은 의미에서 단순히 생성자가 없거나 빈 생성자를 가져야 하며 getter/setter를 가진 클래스의 객체를 의미.

반면 기본 자료형은 파라미터로 선언해도 화면까지 전달되진 않는다.

 

	@GetMapping("/ex04")
	public String ex04(SampleDTO dto, int page) {
		log.info("dto: "+dto);
		log.info("page: " + page);
		return "/sample/ex04";
	}

 

 

src/main/WEB-INF/views 폴더 안에 sample 폴더 생성, ex04.jsp 생성

 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h2> SAMPLEDTO ${sampleDTO } }</h2>
	<h2> PAGE ${page }</h2>
</body>
</html>

위와 같이 page는 전달되지 않음

 

@ModelATtribute는 강제로 전달받은 파라미터를 Model에 담아 전달하도록 하는 어노테이션.

 

	@GetMapping("/ex04")
	public String ex04(SampleDTO dto, @ModelAttribute("page") int page) {
		log.info("dto: "+dto);
		log.info("page: " + page);
		return "/sample/ex04";
	}

 

6.4.2 RedirectAttributes

 

Model 타입과 더불어 스프링 MVC가 자동으로 전달해주는 타입 중 RedirectAttributes 타입도 있다. 이것은 일회성으로 데이터를 전달하는 용도이다.

 

Servlet에서 redirect 방식

response.sendRedirect("/home?name=aaa&age=10");

 

스프링 MVC를 이용하는 redirect 처리

rttr.addFlashAttribute("name", "AAA");

rttr.addFlashAttribute("age", 10);

return "redirect:/";

 

Model과 같이 파라미터로 선언해서 사용하고, addFlashATtribute(이름, 값) 메서드로 화면에 한번만 사용하는 데이터를 전달하기 위해 사용한다.

 

 

 

6.5 Controller의 리턴 타입

 

스프링 MVC 구조가 기존의 상속과 interface에서 어노테이션을 사용하는 방식으로 변한 후 리턴 타입이 자유로워졌다.

Controller의 메서드가 사용할 수 있는 리턴 타입

- STring : jsp를 이용하는 경우 jsp의 경로와 파일 이름을 나타내기 위해 사용

- void : 호출하는 URL과 동일한 이름의 jsp 

- VO, DTO : 주로 JSON 타입의 데이터를 만들어 반환하는 용도

- ResponseEntity : response할 떄 Http 헤더 정보와 내용을 가공하는 용도

- Model, ModelAndView : Model로 데이터를 반환하거나 화면까지 같이 지정하는 경우

- HttpHeaders : 응답에 내용 없이 http 헤더 메시지만 전달

 

6.5.1 void 타입

 

메서드의 리턴 타입을 void로 하는 경우 일반적으로 해당 URL의 경로를 그대로 jsp 파일의 이름으로 사용한다.

 

	@GetMapping("/ex05")
	public void ex05() {
		log.info("/ex05............");
	}

여기서 웹페이지에는 다음과 같은 메시지가 뜬다.

 

 

이는 URL 경로를 View로 처리하는 servlet-context.xml의 아래 설정과 맞물린다.

 

	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>

 

6.5.2 String타입

 

void와 더불어 많이 사용됨. 상황에 따라 다른 화면을 보여줄 필요가 있을 경우 유용함.

일반적으로 String 타입은 현재 프로젝트의 경우 JSP 파일의 이름을 의미한다.

 

return "home"; 이라고 되어 있다면 home이라는 문자열을 리턴했으므로 /WEB_INF/views/home.jsp가 경로가 될 것이다.

String 타입엔 다음과 같은 특별한 키워드를 붙일 수 있다.

 

- redirect

- forward

 

 

6.5.3 객체 타입

VO나 DTO 타입 등 복합적인 데이터가 들어간 객체 타입으로 지정할 수 있는데 이 경우 주로 JSON 데이터 생성에 사용한다.

 

pom.xml에 추가

 

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.4</version>
</dependency>

 

스프링 MVC는 자동으로 브라우저에 JSON 타입으로 객체를 변환해 전달한다.

개발자 도구로 보면 서버에서 전송하는 MIME 타입이 application/json이다. 만일 Jackson-databind 라이브러리가 포함되어 있지 않으면 에러를 보인다.

	@GetMapping("/ex06")
	public @ResponseBody SampleDTO ex06() {
		log.info("/ex06...........");
		SampleDTO dto = new SampleDTO();
		dto.setAge(10);
		dto.setName("홍길동");
		return dto;
	}

 

 

6.5.4 ResponseEntity 타입

 

web을 다루다 보면 HTTP 헤더를 다루는 경우도 종종 있다. 스프링 MVC의 사상은 HttpServletRequest나 Response를 직접 핸들링하지 않아도 이런 작업이 가능하도록 작성되었으므로 이러한 처리를 위해 ResponseEntity를 통해 원하는 헤더 정보나 데이터를 전달할 수 있다.

	@GetMapping("/ex07")
	public ResponseEntity<String> ex07() {
		log.info("/ex07");
		// {"name": "홍길동"}
		String msg="{\"name\":\"홍길동\"}";
		
		HttpHeaders header = new HttpHeaders();
		header.add("Content-Type", "application/json;charset=UTF-8");
		return new ResponseEntity<>(msg, header, HttpStatus.OK);
	}

 

ResponseEntity는 HttpHeaders 객체를 같이 전달할 수 있고 이를 통해 원하는 HTTP 헤더 메시지를 가공하는 것이 가능하다. ex07()의 경우 브라우저에는 JSON 타입이라는 헤더 메시지와 200 OK라는 상태 코드를 전송한다.

6.5.5 파일 업로드 처리

 

Controller의 많은 작업은 스프링 MVC를 통해 처리하기 때문에 개발자는 자신이 해야 하는 것에만 집중할 수 있지만 파일 업로드엔 조금 신경 써야 한다. 파일 업로드를 하기 위해선 전달되는 파일 데이터를 분석해야 하는데 이를 위해서 servlet 3.0까지는 commons의 파일 업로드를 이용하거나 cos.jar 등을 이용해 처리해 왔다.

Servlet3.0 이후 기본적으로 업로드되는 파일을 처리할 수 있는 기능이 추가되며 더 이상 추가 라이브러리가 필요치 않게 되었다.

 

조금 아쉬운 점은 Spring Legacy Project로 생성하는 플젝의 경우 Servlet 2.5 기준이라 3.0 이후 지원 설정을 사용하기 어렵단 것이다.

<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.3</version>
</dependency>

 

C 드라이브 아래 upload/tmp 폴더 생성

 

servlet-context.xml 설정

 

스프링 MVC의 특정 빈을 설정해 파일을 처리한다. 다른 객체 설정과 달리 파일 업로드에는 반드시 id 속성의 값을 multipartResolver로 정확하게 지정해 주어야 한다.

	<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
		<beans:property name="defaultEncoding" value="utf-8"></beans:property>
		<!--  1024 * 1024 * 10 bytes	10MB -->
		<beans:property name="maxUploadSize" value="104857560"></beans:property>
		<!--  1024 * 1024 * 2 bytes		2MB -->
		<beans:property name="maxUploadSizePerFile" value="2097152"></beans:property>
		<beans:property name="uploadTempDir" value="file:/C:/upload/tmp"></beans:property>
		<beans:property name="maxInMemorySize" value="10485756"></beans:property>
	</beans:bean>
	

 

maxUploadSize는 한 번의 Request로 전달될 수 있는 최대의 크기, maxInMemorySize는 메모리 상에서 유지하는 최대의 크기이다. 만일 이 크기 이상은 uploadTempDir에 임시 파일 형태로 보관된다. uploadTempDir에서 절대 경로를 이용하려면 URI 형태로 시작해야 하므로 file:/로 시작하게 된다. defaultEncoding은 파일의 이름이 한글이어도 깨지지 않도록 처리.

 

 

	@GetMapping("/exUpload")
	public void exUpload() {
		log.info("/exUpload.......");
	}

exUpload.jsp를 아래와 같이 생성.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<form action="/sample/exUploadPost" method="post" enctype="multipart/form-data">
		<div>
			<input type='file' name='files'>
		</div>
		<div>
			<input type='file' name='files'>
		</div>
		<div>
			<input type='file' name='files'>
		</div>
		<div>
			<input type='file' name='files'>
		</div>
		<div>
			<input type='file' name='files'>
		</div>
		<div>
			<input type='submit'>
		</div>
	</form>
</body>
</html>

exUpload.jsp의 action 속성이 exUploadPost로 작성되었으므로 SampleController에도 추가.

 

@PostMapping("/exUploadPost")
	public void exUploadPost(ArrayList<MultipartFile> files) {
		files.forEach(file-> {
			log.info("---------------------------------");
			log.info("name:" + file.getOriginalFilename());
			log.info("size:" + file.getSize());
		});
	}

스프링 MVC는 전달되는 파라미터가 동일한 이름으로 여러 개 존재하면 배열로 처리가 가능하므로 파라미터를 MultipartFile 배열 타입으로 작성한다. 실제로 파일을 업로드해보면 아래와 같은 크기를 볼 수 있다.

 

최종 업로드를 하려면 byte[]를 처리해야 해서 업로드 후에는 에러로 뜬다.

 

 

JAVA 설정을 이용하는 경우

 

@Bean을 사용하긴 하지만 id 속성을 같이 부여한다.

 

package org.zerock.config;

import java.io.IOException;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.core.io.FileSystemResource;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

@EnableWebMvc
@ComponentScan(basePackages= {"org.zerock.controller"})
public class ServletConfig implements WebMvcConfigurer{
	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		InternalResourceViewResolver bean = new InternalResourceViewResolver();
		bean.setViewClass(JstlView.class);
		bean.setPrefix("/WEB-INF/views/");
		bean.setSuffix(".jsp");
		registry.viewResolver(bean);
	}
	
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
	}
	
	@Bean(name="multipartResolver")
	public CommonsMultipartResolver getResolver() throws IOException {
		CommonsMultipartResolver resolver = new CommonsMultipartResolver();
		//10MB
		resolver.setMaxUploadSize(1024*1024*10);
		//2MB
		resolver.setMaxUploadSize(1024 * 1024 * 2);
		//1MB
		resolver.setMaxInMemorySize(1024 * 1024);
		//temp upload
		resolver.setUploadTempDir(new FileSystemResource("C:\\upload\\tmp"));
		resolver.setDefaultEncoding("UTF-8");
		return resolver;
	}
}

 

6.6 Controller의 Exception 처리

 

- @ExceptionHandler와 @ControllerAdvice를 이용한 처리

- @ResponseEntity를 이용하는 예외 메시지 구성

 

6.6.1 @ControllerAdvice

 

AOP를 이용하는 방식으로, AOP는 간단히 언급하자면 핵심 로직은 아니지만 공통적인 관심사는 분리하자는 개념이다. Controller를 작성할 때 메서드의 모든 예외사항을 전부 핸들링해야 한다면 중복적이고 많은 코드를 작성해야 하지만 AOP 방식을 이용하면 공통 예외사항에 대해 별도로 @ControllerAdvice를 이용해 분리하는 방식이다.

 

org.zerock.exception 패키지 생성 CommonExceptionAdvice 클래스 생성.

 

package org.zerock.exception;

import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import lombok.extern.log4j.Log4j;

@ControllerAdvice
@Log4j
public class CommonExceptionAdvice {
	@ExceptionHandler(Exception.class)
	public String except(Exception ex, Model model) {
		log.error("Exception........" + ex.getMessage());
		model.addAttribute("exception", ex);
		log.error(model);
		return "error_page";
	}
}

@ControllerAdvice는 해당 객체가 스프링의 컨트롤러에서 발생하는 예외를 처리하는 존재임을 명시. @ExceptionHandler는 해당 메서드가 ()들어가는 예외 타입을 처리하는 것을 의미. @ExceptionHandler 어노테이션 속성으로는 @Exception 클래스 타입을 지정할 수 있다. 위와 같은 경우 Exception.class를 지정했으므로 모든 예외에 대한 처리가 exception만을 통해 처리된다.

 

만일 특정 타입의 예외를 다루고 싶다면 구체적인 예외 클래스를 지정해야 한다.

JSP에서도 구체적인 메세지를 보고 싶다면 Model을 이용해 전달하는 것이 좋다.

 

org.zerock.exception 패키지를 servlet-context.xml에 등록.

 

	<context:component-scan base-package="org.zerock.controller" />
	<context:component-scan base-package="org.zerock.exception" />
	

CommonExceptionAdvice의 except 리턴값은 문자열이므로 JSP 파일의 경로가 된다.

views 바로 밑에 생성.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" import="java.util.*" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h4><c:out value="${exception.getMessage() }" ></c:out></h4>
	<ul>
		<c:forEach items="${exception.getStackTrace() }" var="stack">
			<li>
				<c:out value="${stack} "></c:out>
			</li>
		</c:forEach>
	</ul>
</body>
</html>

 

 

 

Java 설정을 이용하는 경우

 

ServletConfig에 exception 패키지 추가.

@EnableWebMvc
@ComponentScan(basePackages= {"org.zerock.controller", "org.zerock.exception"})
public class ServletConfig implements WebMvcConfigurer{
	@Override

 

6.6.2 404 에러 페이지

 

WAS의 구동 중 가장 흔한 에러와 관련된 HTTP 상태 코드는 404와 500. 500은 Internal Server Error이므로 @ExceptionHandler를 이용하면 되지만 잘못된 URL을 호출할 떄 보이는 404는 조금 다르게 처리하는 것이 좋다.

 

 

	<!-- Processes application requests -->
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
		</init-param>
		<init-param>
			<param-name>throwExceptionIfNoHandlerFound</param-name>
			<param-value>true</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>

스프링 MVC의 모든 요청은 DispatcherServlet으로 처리되므로 web.xml 수정

 

package org.zerock.exception;

import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.http.HttpStatus;
import lombok.extern.log4j.Log4j;

@ControllerAdvice
@Log4j
public class CommonExceptionAdvice {
	@ExceptionHandler(Exception.class)
	public String except(Exception ex, Model model) {
		log.error("Exception........" + ex.getMessage());
		model.addAttribute("exception", ex);
		log.error(model);
		return "error_page";
	}
	
	@ExceptionHandler(NoHandlerFoundException.class)
	@ResponseStatus(HttpStatus.NOT_FOUND)
	public String handle404(NoHandlerFoundException ex) {
		return "custom404";
	}
}

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h1>해당 URL은 존재하지 않습니다.</h1>
</body>
</html>

 

Java 설정을 이용하는 경우

 

web.xml에 설정한 throwExceptionNoHandlerFound를 설정하기 위해선 서블릿 3.0 이상을 이용해야만 하고 WebConfig 클래스를 아래와 같이 수정해야만 한다.

 

package org.zerock.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import javax.servlet.ServletRegistration;

public class WebConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
	@Override
	protected Class<?> [] getRootConfigClasses() {
		return new Class[] {RootConfig.class};
	}
	
	@Override
	protected Class<?> [] getServletConfigClasses() {
		return new Class[] {ServletConfig.class};
	}
	
	@Override
	protected String [] getServletMappings() {
		return new String[] {"/"};
	}
	
	
	
	@Override
	protected void customizeRegistration(ServletRegistration.DynamicRegistration) {
		registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
	}
}

 

 

지금까지 진행된 SampleController.java

 

package org.zerock.controller;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;

import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.zerock.domain.SampleDTO;
import org.zerock.domain.SampleDTOList;
import org.zerock.domain.TodoDTO;

import lombok.extern.log4j.Log4j;

@Controller
@RequestMapping("/sample/*")
@Log4j
public class SampleController {
	@RequestMapping("")
	public void basic() {
		log.info("basic.......");
	}
	
	@RequestMapping(value="/basic", method= {RequestMethod.GET, RequestMethod.POST})
	public void basicGet() {
		log.info("basic get........");
	}
	
	@GetMapping("/basicOnlyGet")
	public void basicGet2() {
		log.info("basic get only get.........");
	}
	
	@GetMapping("/ex01")
	public String ex01(SampleDTO dto) {
		log.info("" + dto);
		return "ex01";
	}
	
	@GetMapping("/ex02")
	public String ex02(@RequestParam("name") String name, @RequestParam("age") int age) {
		log.info("name: " + name);
		log.info("age: " + age);
		return "ex02";
	}
	
	@GetMapping("/ex02List")
	public String ex02List(@RequestParam("ids") ArrayList<String> ids) {
		log.info("ids: " + ids);
		return "ex02List";
	}
	
	@GetMapping("/ex02Array")
	public String ex02Array(@RequestParam("ids") String[] ids) {
		log.info("ids: " + Arrays.toString(ids));
		return "ex02List";
	}
	
	@GetMapping("/ex02Bean")
	public String ex02Bean(SampleDTOList list) {
		log.info("list dtos: " + list);
		return "ex02Bean";
	}
	
	
	@InitBinder
	public void initBinder(WebDataBinder binder) {
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
		binder.registerCustomEditor(java.util.Date.class, new CustomDateEditor(dateFormat, false));
	}

	@GetMapping("/ex03")
	public String ex03(TodoDTO todo) {
		log.info("todo: " + todo);
		return "ex03";
	}
	
	@GetMapping("/ex04")
	public String ex04(SampleDTO dto, @ModelAttribute("page") int page) {
		log.info("dto: "+dto);
		log.info("page: " + page);
		return "/sample/ex04";
	}
	
	@GetMapping("/ex05")
	public void ex05() {
		log.info("/ex05............");
	}
	
	@GetMapping("/ex06")
	public @ResponseBody SampleDTO ex06() {
		log.info("/ex06...........");
		SampleDTO dto = new SampleDTO();
		dto.setAge(10);
		dto.setName("홍길동");
		return dto;
	}
	
	@GetMapping("/ex07")
	public ResponseEntity<String> ex07() {
		log.info("/ex07");
		// {"name": "홍길동"}
		String msg="{\"name\":\"홍길동\"}";
		
		HttpHeaders header = new HttpHeaders();
		header.add("Content-Type", "application/json;charset=UTF-8");
		return new ResponseEntity<>(msg, header, HttpStatus.OK);
	}
	
	@GetMapping("/exUpload")
	public void exUpload() {
		log.info("/exUpload.......");
	}
	
	@PostMapping("/exUploadPost")
	public void exUploadPost(ArrayList<MultipartFile> files) {
		files.forEach(file-> {
			log.info("---------------------------------");
			log.info("name:" + file.getOriginalFilename());
			log.info("size:" + file.getSize());
		});
	}
	

}