For Programmer

Part2 - 스프링 MVC 의 Controller(2) 본문

Spring/스프링 프로젝트

Part2 - 스프링 MVC 의 Controller(2)

유지광이 2020. 8. 22. 21:00
728x90

5.Controller의 리턴 타입

스프링MVC 구조가 기존의 상속과 인터페이스에서 어노테이션을 사용하는 방식으로 변한 이후에 가장 큰 변화중 하나는 리턴 타입이 자유로워 졌다는 것입니다.

Controller의 메서드가 사용할 수 있는 리턴 타입은 주로 다음과 같습니다.

 

-String: jsp를 이용하는 경우에는 jsp파일의 경로와 파일이름을 나타내기 위해서 사용합니다.

-void: 호출하는 URL과 동일한 이름의 jsp를 의미합니다.

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

-ResponseEntity타입: response할 때 Http헤더 정보와 내용을 가공하는 용도로 사용합니다.

-Model,ModelAndView:Model로 데이터를 반환하거나 화면까지 같이 지정하는 경우에사용합니다.(최근에많이사용x)

-HttpHeaders:응답에 내용 없이 Http헤더 메시지만 전달하는 용도로 사용합니다.

1. void타입

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

*SampleController의 일부

@GetMapping("/ex06")
	public void ex06() {

	}

결과

sample/ex06.jsp 파일을 찾게됨

2. String타입

void 타입과 더불어서 가장 많이 사용하는 것은 String타입입니다. String타입은 상황에 따라 다른 화면을 보여줄 필요가 있을 경우에 유용하게 사용합니다.(if~else와 같은 처리가 필요한경우) 일반적으로 String타입은 현재 프로젝트의 경우 JSP파일의 이름을 의미합니다.

	@GetMapping("/ex01")
	public String ex01() {

		return "ex01";
	}

다음을 http://localhost:8090/sample/ex01 주소창에 입력하게 될경우 /WEB-INF/views/ex01.jsp 를 찾게됩니다. 또한 String타입에는 다음과 같은 특별한 키워드를 붙여서 사용할 수 있습니다.

-redirect: 리다이렉트 방식으로 강제로 원하는 주소로 보내고싶은 경우

-forward: 포워드 방식으로 코드의 흐름을 계속 이어지게 하고 싶은경우

3.객체 타입

Controller의 메서드 리턴 타입을 VO(Value Object) 나 DTO(Data Transfer Object) 타입 등 복합적인 데이터가 들어간 객체 타입으로 지정할 수 있는데, 이 경우는 주로 JSON 데이터를 만들어 내는 용도로 사용합니다.

우선 이를 위해서는 jackson-databind 라이브러리를 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>

SampleController의 일부

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

		return dto;
	}

http://localhost:8090/sample/ex06URL 요청을 해보면 다음과 같은 결과 발생

만일 Jackson-databind 라이브러리가 포함되지 않았다면 500에러 화면을 보게 됩니다. 스프링 MVC는 리턴 타입에 맞게 데이터를 변환해 주는 역할을 지정할 수 있는데 기본적으로 JSON은 처리가 되므로 별도의 설정이 필요하지않습니다.(단, 스프링3버젼까지는 별도의 Converter를 작성해야만 했습니다.)

또한 json타입으로 표시됩니다.

4.ResponseEntity 타입

Web을 다루다 보면 HTTP 프로토콜의 헤더를 다루는 경우도 종종있습니다. 스프링MVC의 사상은 HttpServeltRequest나 HttpServletResponse를 직접 핸들링하지 않아도 이런 작업이 가능하도록 작성되었기 때문에 이러한 처리를 위해 ResponseEntity를 통해서 원하는 헤더 정보나 데이터를 전달할 수 있습니다.

 

SampleController 일부

	@GetMapping("/ex07")
	public ResponseEntity<String> ex07() {
		log.info("/ex07..............");
		String msg = "{\"name\": \"홍길동\"}";

		HttpHeaders header = new HttpHeaders();
		header.add("Content-type", "application/json;charset=UTF-8");
		return new ResponseEntity<String>(msg, header, HttpStatus.OK);
	}

요청주소로 실행 결과

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

 

6.파일 업로드 처리

Controller의 많은 작업은 스프링MVC를 통해서 처리하기 때문에 개발자는 자신이 해야하는 역할에만 집중해서 코드를 작성해야하지만 조금 신경써야하는 부분이 있다면 파일을 업로드 하는 부분에대한 처리입니다. 파일 업로드를 하기 위해서는 전달되는 파일 데이터를 분석해야 하는데, 이를 위해서 Servlet3.0 전까지는 commons의 파일업로드를 이용하거나 cos.jar 등을 이용해서 처리 해왔습니다. Servlet3.0 이후(Tomcat7.0)에는 기본적으로 업로드되는 파일을 처리할 수 있는 기능이 추가되어 있으므로 더 이상 추가적인 라이브러리가 필요하지 않습니다. 단, 'Spring Legacy Project' 로 생성되는 프로젝트의 경우 Servlet2.5를 기준으로 생성되기 때문에 3.0이후에 지원되는 설정을 사용하기 어렵다는점입니다. 3.0 이상의 파일업로드 방식은 후반부에 별도 파트에서 다루도록 하고 지금은 일반적으로 많이 사용하는 commons-fileupload를 이용하도록 하겠습니다.

     pom.xml에 다음 commons-fileupload 라이브러리 추가

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

그 후 파일이 임시로 업로드될 폴더를 따로 만들어줍니다.( 예시) C:\Users\Administrator\Desktop\Spring MVC miniProject\upload\tmp)

servlet-context.xml 추가되는 코드

<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:/Users/Administrator/Desktop/Spring MVC miniProject/upload/tmp"></beans:property>
		<beans:property name="maxInMemorySize" value="10485756"></beans:property>
	</beans:bean>

*maxUploadSize는 한 번의 Request로 전달될 수 있는 최대의 크기를 의미하고, maxUploadSizePerFile은 하나의 파일 최대 크기, maxInMemorySize는 메모리상에서 유지하는 최대의 크기를 의미합니다. 만일 이 크기 이상의 데이터는 uploadTempDir에 임시 파일의 형태로 보관됩니다. uploadTempDir에서 절대 경로를 이용하려면 URI형태로 제공해야 하기 때문에 'file:/' 로 시작하도록 합니다. defaultEncoding은 업로드 하는 파일의 이름이 한글일 경우 깨지는 문제를 처리합니다.

SampleController의 일부

	@GetMapping("/exUpload")
	public void exUpload() {

		log.info("/exUpload.........");

	}

exUpload.jsp (views/sample/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>

SampleController의 일부

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

톰캣서버 실행 후 파일을 2개 업로드 할 경우 로그창 

*Java 설정을 이용하는 경우 

Java설정을 이용하는 경우에는 @Bean을 이용해서 처리하기는 하지만 id속성을 같이 붙여합니다.

ServletConfig 클래스의 일부 

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 {

	....생략

	@Bean(name = "multipartResolver")
	public CommonsMultipartResolver getResolver() throws IOException {
		CommonsMultipartResolver resolver = new CommonsMultipartResolver();

		// 10MB
		resolver.setMaxUploadSize(1024 * 1024 * 10);

		// 2MB
		resolver.setMaxUploadSizePerFile(1024 * 1024 * 2);

		// 1MB
		resolver.setMaxInMemorySize(1024 * 1024);

		// temp upload
		resolver.setUploadTempDir(new FileSystemResource("C:/Users/Administrator/Desktop/Spring MVC miniProject/upload/tmp"));

		resolver.setDefaultEncoding("UTF-8");

		return resolver;
	}
}

나머지는 xml에서 했던것과 동일한 방식으로 처리 하시면 됩니다.

7.Controller의 Exception 처리

Controller를 작성할 때 예외 상황을 고려하면 처리해야 하는 작업이 엄청나게 늘어날 수 밖에 없습니다. 따라서 스프링MVC에서는 이러한 작업을 다음과 같은 방식으로 처리 할  수 있습니다.

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

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

7.1 @ControllerAdvice

@ControllerAdvice는 AOP(Aspect-Oriented-Programming)을 이용하는 방식입니다. AOP에 대해서는 별도의 파트에서 설명하겠지만, 간단히 언급하자면 핵심적인 로직은 아니지만 프로그램에서 필요한 '공통적인 관심사(cross-concern)는 분리' 하자는 개념입니다. AOP방식을 이용하면 공통적인 예외사항에 대해서는 @ControllerAdvice를 이용해서 분리하는 방식입니다. org.zerock.exception이라는 패키지를 생성하고 CommonExceptionAdvice 클래스를 생성합니다.

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는 해당 메서드가 ()들어가는 예외 타입을 처리한다는 것을 의미합니다. 어노테이션 속성으로는 Exception클래스 타입을 지정할 수 있습니다. 위와같은 경우는 Exception.class를 지정하였으므로 모든 예외에 대한 처리가 except()만을 이용해서 처리할 수 있습니다.

만일 특정한 타입의 예외를 다루고 싶다면 Exception.class대신에 구체적인 예외클래스를 지정해야합니다. JSP화면에서도 구체적인 메시지를 보고싶다면 Model을 이용하여 전달하여야 합니다. (org.zerock.exception패키지는 servlet-context.xml에서 인식하지 않기 때문에 <component-scan>을 이용해서 해당 패키지의 내용을 조사하도록 해야합니다.)

 

servlet-context.xml 일부

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

또한 return값으로 "error_page"를 반환하고 있기 때문에 /WEB-INF/views/error_page.jsp를 만들어줍니다.

 

error_page.jsp

<%@ 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 PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; 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>

 만약 page 인자를 URL에 전달하지 않을 경우 발생하는 에러페이지

*Java설정을 이용하는 경우

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

*exception 패키지를 스캔할 수 있도록 등록만 해주면 된다.

 

7.2 404 에러 페이지

WAS의 구동 중 가장 흔한 에러와 관련된 HTTP 상태 코드는 '404'와 '500'에러 코드입니다. 500 메시지는 'Internal Server Error' 이므로 @ExceptionHandler를 이용해서 처리되지만, 잘못된 URL을 호출할 때 보이는 404에러 메시지의 경우는 조금 다르게 처리하는 것이 좋습니다. 스프링MVC의 모든 요청은 DispatcherServlet을 이용해서 처리되므로 404에러도 같이 처리할 수 있도록 web.xml을 수정합니다.

 

web.xml의 일부

<!-- 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>

그 후 org.zerock.exception.CommonExceptionAdvice에는 다음과 같이 메서드를 추가합니다.

 

CommonExceptionAdvice 클래스의 일부

	@ExceptionHandler(NoHandlerFoundException.class)
	@ResponseStatus(value = HttpStatus.NOT_FOUND)
	public String handle404(NoHandlerFoundException ex) {

		return "custom404";
	}

그 후 /WEB-INF/views/custom404.jsp 생성

 

custom404.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
  <h1>해당 URL은 존재하지 않습니다.</h1>
</body>
</html>

존재하지 않는 URL호출하면 발생하게 되는 페이지

*Java설정을 이용하는 경우

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

 

WebConfig 클래스

package org.zerock.config;

import javax.servlet.ServletRegistration;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebConfig extends AbstractAnnotationConfigDispatcherServletInitializer {

	// 프로젝트에서 사용할 Bean들을 정의하기 위한 클래스를 지정한다.
	@Override
	protected Class<?>[] getRootConfigClasses() {

		return new Class[] { RootConfig.class };
	}

	// Spring MVC 프로젝트 설정을 위한 클래스를 지정한다.
	@Override
	protected Class<?>[] getServletConfigClasses() {
		return new Class[] { ServletConfig.class };
	}

	// DispatcherServlet에 매핑할 요청 주소를 셋팅한다.
	@Override
	protected String[] getServletMappings() {
		return new String[] { "/" };
	}

	@Override
	protected void customizeRegistration(ServletRegistration.Dynamic registration) {
		registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
	}
}
728x90
Comments