본문 바로가기

개발관련/코드로 배우는 스프링 웹 프로젝트(개정판)

Part2 - 스프링 MVC Controller Chapter06

코드로 배우는 스프링 웹 프로잭트 Part2

Chapter06 스프링 MVC Controller

 

Spring MVC를 이용하는 경우 작성되는 Controller는 다음과 같은 특징이 있습니다.

  • HttpServletRequest, HttpServletResponse를 거의 사용할 필요 없이 필요한 기능 구현
  • 다양한 타입의 파라미터 처리, 다양한 타입의 리턴 타입 사용 가능
  • GET 방식, POST 방식 등 전송 방식에 대한 처리를 어노테이션으로 처리 가능
  • 상속/인터페이스 방식 대신에 어노테이션만으로도 필요한 설정 가능

다른 프레임워크들과 다르게 어노테이션을 중심으로 구성되기 때문에 예제들을 작성할때에도 각 어노테이션의 의미에 대해서 주의해가며 학습해야 합니다.

 

6.1 @Controller, @RequestMapping

프로잭트 내 org.zerock.controller 패키지 폴더에 SampleController라는 이름의 클래스를 작성 합니다.

package org.zerock.controller;

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

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

}

SampleController의 클래스 선언부에는 @Controller라는 스프링 MVC에서 사용하는 어노테이션을 사용하고 있습니다.

작성된 SampleController 클래스는 위 그림과 같이 자동으로 스프링 객체(Bean)로 등록 되는데 servlet-context.xml에 그 이유가 있습니다.

servlet-context.xml 일부

servlet-context.xml에는 <context:component-scan>이라는 태그를 이용해서 지정된 패키지를 조사(스캔)하도록 되어 있습니다.

해당 패키지에 선언된 클래스들을 조사하면서 스프링에서 객체(Bean) 설정에 사용되는 어노테이션들을 가진 클래스들을 파악하고 필요하다면 이를 객체로 생성해서 관리하게 됩니다.

 

SampleController 클래스가 스프링에서 관리되면 화면상에는 클래스 옆에 작게 's' 모양의 아이콘이 추가 됩니다.

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

 

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

 

예를들어 SampleController 클래스를 다음과 같이 '/sample/*이라는 경로로 지정했다면 다음과 같은 URL은 모두 SampleControlller에서 처리됩니다.

  • /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에 에러가 나타날수도 있다.

Log4j의 scope(범위)가 실행시(runtime)이라서 발생하는 문제이다.

pom.xml의 Log4j의 scope를 주석처리 해주면 된다.

 

SampleControlller는 Lombok의 @Log4j를 사용합니다.

@Log4j는 @Log가 java.util.Logging을 사용하는데 반해 Log4j라이브러리를 활용합니다.

Spring Legacy Project로 생성한 프로젝트는 기본적으로 Log4j가 추가되어 있으므로 별도의 설정이 필요하지 않습니다.

 

프로젝트를 WAS(Tomcat)에서 실행해보면 스프링이 인식할 수 있는 정보가 출력되는 것을 확인 할 수 있는데, 위와 같은 경우에는 아래와 같이 로그가 보입니다.

src/resources 폴더 내에 log4j.xml의 모든 'info'를 'debug'로 수정하면 아래 로그가 보입니다.

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

 

책에서는 RequestMappingHandlerMapping - Mapping "{[/],mehtod[GET]}............

RequestMappingHandlerMapping - Mapping "{[/sample/*],mehtod[GET]}............

이런 로그가 나오는데 저는 안나와서 http://localhost:8080/sample/을 호출해서 로그가 찍히는걸 확인하는걸로 대체 하겠습니다.

 

http://localhost:8080/sample/를 호출 해보시면 View를 연결하지 않았기때문에 404 페이지를 찾을 수 없다고 나오지만

콘솔에는 우리가 작성한 basic() 메소드의 log.info 값이 찍히므로 잘 확인 한것으로 하겠습니다.

 

6.2 @RequestMapping의 변화

@Controller 어노테이션은 추가적인 속성을 지정할 수 없지만 @RequestMapping의 경우 몇 가지의 속성을 지정 할 수 있습니다.

이 중에서도 가장 만힝 사용하는 속성이 method 속성입니다.

method 속성은 흔히 GET,Post방식을 구분해서 사용할 때 사용합니다.

 

스프링 4.3 버전부터는 이러한 @RequestMapping을 줄여서 사용할 수 있는 @GetMapping, @PostMapping이 등장하는데 축약적인 표현이므로, 아래와 같이 교체해서 학습 하는 것이 좋습니다.

 

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 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 방식 모두를 지원해야 하는 경우에 배열로 처리해서 지정할 수 있습니다.

일반적인 경우에는 GET,POST방식만을 사용하지만 최근에는 PUT,DELETE 방식 등도 점점 많이 사용하고 있습니다.

@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;
}

SampleDTO 클래스는 Lombok의 @Data 어노테이션을 이용해 처리 합니다.

@Data를 이용하게 되면 getter/setter, equals(), toString() 등의 메서드를 자동으로 생성되기 때문에 편리합니다.

 

SampleController의 메서드가 SampleDTO를 파라미터를 사용하게 되면 자동으로 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 {

//중략

	@GetMapping("/ex01")
	public String ex01(SampleDTO dto) {
		
		log.info(" "+dto);
		
		return "ex01";
	}
}

SampleController의 경로가 '/sample/'이므로 ex01 메서드를 호출하는 경로는 '/sample/ex01/'이 됩니다.

이 메서드에는 @GetMapping이 사용 되었으므로 필요한 파라미터를 URL뒤에 ?name=aaa&age=10 이런식으로 추가해서 호출 할 수있습니다.

 

http://localhost:8080/sample/ex01?name=aaa&age=10 을 입력하면 SampleDTO 객체안에 name과 age 속성이 제대로 수집된 것을 확인 할수 있습니다.

 

6.3.1 파라미터의 수집과 변환

Controller가 파라미터를 수집하는 방식은 파라미터 타입에 따라 자동으로 변환하는 방식을 이용합니다.

예를 들어 SampleDTO에는 int 타입으로 선언된 age가 자동으로 숫자로 변환되는 것을 볼 수 있습니다.

 

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

SampleController 클래스 내에 ex02 메서드를 추가 합니다.

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

ex02() 메서드는 파라미터에 @RequestParam 어노테이션을 이용해 작성 되었는데, @RequestParam은 파라미터로 사용된 변수의 이름과 전달되는 파라미터의 이름이 다른 경우 유용하게 사용됩니다.

 

지금까지는 변수명과 파라미터명이 동일하기 때문에 사용할 필요가 없었습니다만 @RequestParam의 소개 차원에서 사용해 보았습니다.

 

http://localhost:8080/sample/ex02?name=aaa&age=10 를 호출하면 이전과 같이 동일하게 데이터가 수집 된것을 확인 할 수 있습니다.

 

6.3.2 리스트, 배열 처리

동일한 이름의 파라미터가 여러 개 전달되는 경우에는 ArrayList<> 등을 이요해 처리가 가능 합니다.

package org.zerock.controller;

import java.util.ArrayList;

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.springframework.web.bind.annotation.RequestParam;
import org.zerock.domain.SampleDTO;

import lombok.extern.log4j.Log4j;


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

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

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

 

위 코드의 경우 ids라는 이름의 파라미터가 여러 개 전달되더라도 ArrayList<String>이 생성되어 자동으로 수집됩니다.

 

http://localhost:8080/sample/ex02List?ids=111&ids=222&ids=333 을 호출합니다.

 

배열의 경우도 동일하게 처리가 가능 합니다.

http://localhost:8080/sample/ex02Array?ids=111&ids=222&ids=333 을 호출합니다.

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

 

6.3.3 객체 리스트

만일 전달하는 데이터가 SampleDTO와 같이 객체 타입이고 여러 개를 처리해야 한다면 약간의 작업을 통해 한번에 처리 할 수 있습니다.

 

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<>();
	}
}

SampleController에서는 SampleDTOList 타입을 파라미터로 사용하는 메서드를 작성합니다.

	@GetMapping("/ex02Bean")
	public String ex02Bean(SampleDTOList list) {
		
		log.info("List dtos : "+ list);
		
		return "ex02Bean";
	}

 

파라미터는[인덱스]와 같은 형식으로 전달해서 처리할 수 있습니다.

http://localhost:8080/sample/ex02Bean?list[0].name=aaa&list[1].name=bbb&list[2].name=ccc 를 호출합니다.

Tomcat은 경로에 따라 위와 같은 문자열에서 '[]' 문자를 특수문자로 허용하지 않을 수 있습니다.

 

JavaScript를 이용하는 경우에는 encodeURIComponent()와 같은 방법으로 해결할 수 있으나,

현재 예제의 경우에는 '[' = '%5B' , ']' = '%5D' 로 변경하도록 합니다.

http://localhost:8080/sample/ex02Bean?list%5B0%5D.name=aaa&list%5B1%5D.name=bbb&list%5B2%5D.name=ccc

 

URL을 호출하면 다음과 같이 여러개의 SampleDTO 객체를 생성하는 것과 []순서에 맞게 생성된것을 확인 할 수 있습니다.

 

6.3.4 @InitBinder

파라미터의 수집을 다른 용어로는 binding(바인딩) 이라고 합니다.

변환이 가능한 데이터는 자동으로 변환되지만 경우에 따라서는 파라미터를 변환해서 처리해야 하는 경우도 존재합니다.

 

예를들어 화면에는 '2023-01-01' 과 같이 문자열로 전달된 데이터를 java.util.Data 타입으로 변환하는 작업이 그러합니다.

 

스프링 Controller에서는 파라미터를 바인딩 할때 자동으로 호출되는 @InitBinder를 이용해서 이러한 변환을 처리할 수 있습니다.

 

org.zerock.domian 패키지에 TodoDTO라는 클래스를 작성합니다.

package org.zerock.domain;

import java.util.Date;

import lombok.Data;

@Data
public class TodoDTO {
	private String title;
	private Date dueDate;
}

TodoDTO에는 특별하게 dueDate 변수의 타입이 java.util.Date 타입입니다.

사용자가 '2023-01-02' 라는 값을 변환하고자 할때 타입관련 문제가 생기게 됩니다.

이러한 문제의 간단한 해결책은 @InitBinder를 이용하는 것입니다.

 

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.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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
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 {

	@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=2023-01-01

을 호출 했을때 서버에서는 정상적으로 파라미터를 수집해서 처리합니다.

반면에 @InitBinder 처리가 되지 않았다면 브라우저에서는 400 에러가 발생하는 것을 확인 할 수 있습니다.

400에러는 요청 구문(syntax)이 잘못 되었다는 메세지입니다.

 

날짜가 정상적으로 처리되어도 아직 jsp는 없으므로 404 - 찾을 수 없음이 발생 합니다.

 

6.3.5 @DateTimeFormat

@InitBinder를 이용해서 날짜를 변환할 수 도 있지만, 파라미터로 사용되는 인스턴스를 변수에 @DateTimeFormat을 적용해도 변환이 가능 합니다.

 

 

package org.zerock.domain;

import java.util.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;
	
	
}

@DateTimeFormat을 이용하는 경우에는 @InitBinder는 필요하지 않습니다.

@InitBinder어노테이션으로 선언된 클래스를 주석처리 합니다.

 

http://localhost:8080/sample/ex03?title=test&dueDate=2023/01/01 을 호출하면

이렇게 타입이 변환 된것을 확인 할 수 있습니다.

 

6.4 Model이라는 데이터 전달자

Controller의 메서드를 작성할 때는 특별하게 Model 이라는 타입을 파라미터로 지정할 수 있습니다.

Model 객체는 JSP 컨트롤러에서 생성된 데이터를 담아서 전달하는 역할을 하는 존재입니다.

이를 이용해서 JSP와 같은 뷰(View)로 전달해야 한느 데이터를 담아서 보낼 수 있습니다.

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

 

Model은 모델 2 방식에서 사용하는 Request.setAttribute()와 유사한 역할을 합니다.

Servlet을 이용해 본 적이 있다면 다음과 같은 코드에 익숙할 것입니다.

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

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

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

dispatcher.forward(request, response);

 

위 코드를 스프링에서는 Model을 이용해서 다음과 같이 처리하게 됩니다.

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

public String home(Model model){
	model.addAttribute("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를 가진 클래스의 객체를 의미 합니다.

 

앞의 예제에서 파라미터로 사용된 SampleDTO의 경우는 Java Beans의 규칙에 맞기 때문에 자동으로 다시 화면까지 전달 됩니다.

 

반면에 기본 자료형의 경우는 파라미터로 선언하더라도 기본적으로 화면까지 전달되지는 않습니다.

아래 코드를 SampleController.java에 추가 해줍니다.

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

 

ex04는 SampleDTO 타입과 int 타입의 데이터를 파라미터로 사용합니다.

결괄르 확인하기 위해서 '/WEB-INF/views' 폴더아래 sample폴더를 생성하고 리턴값에서 사용한 'ex04'에 해당하는 ex04.jsp를 작성 합니다

 

<%@ page language="java" contentType="text/html; charset=EUC-KR" pageEncoding="EUC-KR"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="EUC-KR">
<title>Insert title here</title>
</head>
<body>

	<h2>SampleDTO ${sampleDTO}</h2>
	<h2>Page ${page}</h2>

</body>
</html>

 

서버를 실행하고 브라우저를 통해서 'http://localhost:8080/sample/ex04?name=aaa&age=11&page=9'와 같이 호출하면 SampleDTO만 호출 되고 int 타입으로 된 page는 화면에 전달되지 않은 것을 볼 수 있습니다.

@ModelAttribute는 강제로 전달받은 파라미터를 Model에 담아서 전달하도록 할 떄 필요한 어노테이션 입니다.

@ModelAttribute가 걸린 파라미터는 타입에 상관없이 무조건 Model에 담아 전달 되므로, 파라미터로 전달된 데이터를 다시 화면에서 사용 할 경우 유용합니다.

 

기존 코드에서 int 타입의 데이터가 화면까지 전달되지 않았으므로, @ModelAttribute를 이용해서 전달 해보겠습니다.

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

@ModelAttribute가 붙은 파라미터는 화면까지 전달되므로 브라우저를 통해서 호출하면 ${page}가 호출된 것을 확인할 수 있습니다.

6.4.2 RedirectAttributes

Model타입과 더불어 스프링 MVC가 자동으로 전달해 주는 타입중에는 RedirectAttribute 타입이 존재합니다.

RedirectAttribute는 조금 특별하게 일회성으로 데이터를 전달하는 용도로 사용됩니다.

 

RedirectAttribute 는 기존에 Servlet에서는 response.sendRedirect()를 사용할 떄와 동일한 용도로 사용 됩니다.

//Servlet에서 redirect 방식

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

스프링 MVC를 사용하면 아래와 같이 변경됩니다.

//스프링 MVC를 사용하는 redirect 처리

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

return "redirect:/";

RedirectAttribute는 Model과 같이 파라미터로 선언해서 사용하고, addFlashAttribute(이름,값) 메서드를 이용해서 화면ㅇ네 한 번만 사용하고 다음에는 사용하지 않는 데이터를 전달하기 위해 사용합니다.

 

RedirectAttribute의 용도는 PART3에서 예제를 작성할 때 여러번 사용 할 것입니다.

 

 

6.5 Controller의 리턴 타입

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

 

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............");
	}

브라우저에 SampleController의 경로에 ex05()의 경로를 합처 /sample/ex05를 호출하면 아래와 같은 메세지를 보게 됩니다.

 

에러메세지를 자세히 보면 '/WEB-INF/views/sample/ex05.jsp'가 존재하지 않아서 생기는 문제라는 걸 알 수 있습니다.

이것은 servlet-context.xml의 아래 설정과 같이 맞물려 URL 경로를 View로 처리하기 때문에 생기는 결과입니다.

servlet-context.xml

6.5.2 String 타입

void 타입과 더불어서 가장 많이 사용하는 return 타입 입니다.

String 타입은 상황에 따라 다른 화면을 보여 줄 필요가 있을때 사용합니다. (if ~ else와 같은 처리가 필요할때)

 

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

프로젝트 생성 시 기본으로 만들어 진 HomeController의 코드를 보면 String을 반환타입으로 사용 하는 것을 볼 수 있습니다.

 

package org.zerock.controller;

import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

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

/**
 * Handles requests for the application home page.
 */
@Controller
public class HomeController {
	
	private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
	
	/**
	 * Simply selects the home view to render by returning its name.
	 */
	@RequestMapping(value = "/", method = RequestMethod.GET)
	public String home(Locale locale, Model model) {
		logger.info("Welcome home! The client locale is {}.", locale);
		
		Date date = new Date();
		DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
		
		String formattedDate = dateFormat.format(date);
		
		model.addAttribute("serverTime", formattedDate );
		
		return "home";
	}
	
}

home() 메서드는 'home'이라는 문자열을 리턴했기 때문에 '/WEB-INF/views/home.jsp'경로가 됩니다.

 

String 타입은 다음과 같은 특별한 키워드를 붙여서 사용할 수 있습니다.

  • redirect : 리다이렉트 방식으로 처리 하는 경우
  • forward : 포워드 방식으로 처리하는 경우

6.5.3 객체 타입

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

 

우선 이를 위해서는 jackson-databind 라이브러리를 pom.xml에 추가 합니다.

		<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;
			
	}

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

개발자 도구를 이용해 살펴보면(F12) 서버에서 전송하는 MIME 타입이 'application/json'으로 처리되는 것을 볼 수 있습니다.

만인 jackson-databind 라이브러리가 포함되지 않았다면 500에러 화면을 마주하게 됩니다.

 

스프링 MVC는 리턴 타입에 맞게 데이터를 변환해주는 역할을 지정할 수 있는데 기본적으로 JSON은 처리가 되므로 별도의 설정이 필요하지 않습니다.

(스프링 3까지는 별도의 Converter를 작성해야 했습니다.)

 

6.5.4 ResponseEntity 타입

Web을 다루다보면 HTTP 프로토콜의 헤더를 다루는 경우도 종종 있습니다.

스프링 MVC는 HttpServletRequest나 HttpServletResponse를 직접 핸들링 하지 않아도 이런 작업이 가능하게 구현 되었기 때문에 이런 처리를 위해 ResponseEntity를 통해서 원하는 헤더 정보나 데이터를 전달할 수 있습니다.

	@GetMapping("/ex07")
	public ResponseEntity<String> ex07(){
		log.info("ex07............................");
		
		String msg = "{\"name\" : \"홍길동\"}";
		
		org.springframework.http.HttpHeaders header = new org.springframework.http.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(Tomcat 7.0)에는 기본적으로 업로드 되는 파일을 처리할 수 있는 기능이 추가되어 있습니다.

 

조금 아쉬운 점은 'Spring Legacy Project'로 생성되는 프로잭트의 경우 Servlet 2.5를 기준으로 생성되기 때문에 3.0이후에 지원되는 설정을 사용하기 어렵습니다.

 

3.0 이상의 파일 업로드 방식은 후반부에 다시 다루도록 하고 일반적으로 많이 사용되는 commoms-fileupload를 이용해보겠습니다.

 

 

pom.xml에 commons-fileupload를 추가해줍니다.

		<dependency>
			<groupId>commons-fileupload</groupId>
			<artifactId>commons-fileupload</artifactId>
			<version>1.3.3</version>
		</dependency>

라이브러리를 추가 한 후 파일이 임시 저장될 폴더를 C드라이브 아래 upload/tmp로 작성 합니다

 

servlet-context.xml 설정

servlet-context.xml은 스프링 MVC의 특정한 객체(빈)를 설정해서 파일을 처리합니다.

다른 객체(Bean)를 설정하는 것과 달리 파일 업로드의 경우에는 반드시 id 속성의 값을 'multipartResolver'로 정확하게 지정해야 하므로 주의가 필요합니다.

<!-- 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:/upload/tmp"></beans:property>
		<beans:property name="maxInMemorySize" value="10485756"></beans:property>
	
	</beans:bean>

maxuploadSize는 한번의 Request로 전달될 수 있는 최대 크기를 의미합니다.

maxUploadSizePerFile은 하나의 파일 최대 크키

maxInMemorySize는 메모리상에서 유지하는 최대의 크기를 의미합니다.

만일 이 크기 이상의 데이터는 uploadTempDir에 임시 파일 형태로 보관됩니다.

uplocadTempDir에서 절대경로를 이용하려면 URI형태로 제공해야 하기 떄문에 'file:/' 형태로 시작 합니다.

defaultEncoding은 업로드 하는 파일의 이름이 한글일 경우 깨지는 문제를 처리 합니다.

 

SampleController에서 다음과 같이 get방식으로 파일을 업로드 할 화면을 처리 합니다.

//SampleController에 추가
	@GetMapping("/exUpload")
	public void exUpload() {
		log.info("exUpload....................");
	}

파일을 업로드 해 볼 jsp를 생성 합니다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; 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는 여러 개의 파일을 한꺼번에 업로드 하는 예제로 작성해 봅니다.

 

exUpload.jsp의 action 속성값은 '/sample/exUploadPost'로 작성 하였으므로, SampleController에 exUploadPost를 추가 합니다.

package org.zerock.controller;

import java.util.ArrayList;
import java.util.Arrays;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
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 {
	
    @GetMapping("/exUploadPost")
	public void exUploadPost(ArrayList<MultipartFile> files) {
		
		files.forEach(file -> {
			log.info("-------------------------------------");
			log.info("name : "+file.getOriginalFilename());
			log.info("size : "+file.getSize());
			
		});
	}
}

스프링 MVC는 전달되는 파라미터가 동일한 이름으로 여러 개 존재하면 배열로 처리가 가능하므로 파라미터를 multipartFile의 배열 타입으로 작성 합니다.

실제로 파일을 로드해보면 아래와 같은 결과를 볼 수 있습니다.

 

한 파일의 최대 크기가 2MB 이므로 2MB보다 작은 파일을 업로드 합니다.

 

 

아래와 같은 로그가 찍히면서 업로드 정보가 올바르게 처리되는 것을 보여주고 있습니다.

최종 업로드를 하려면 byte[]를 처리해야 한느데 예제는 아직 처리하지 않은 상태입니다.

(책의 뒤쪽에서 예제로 다룰 예정입니다.)

 

6.6 Controller의 Exception 처리

Controller를 작성할 때 예외 상황을 고려하면 처리해야 하는 작업이 엄청나게 늘어날 수 밖에 없습니다.

스프링 MVC에서는 이러한 작업을 다음과 같은 방식으로 처리할 수 있습니다.

  • @ExceptionHandler와 @ControllerAdvice를 이용한 처리
  • @ResponseEntity를 이용하는 예외 구성 메세지

6.6.1 @ControllerAdvice

@ControllerAdvice는 뒤에서 배우게 되는 AOP(Aspect-Oriented-Programming)를 이용하는 방식입니다.

AOP는 프로그램의 핵심 로직은 아니지만 공통으로 필요한 관심사(cross-concern)는 분리하자는 개념입니다.

 

Controller를 작성할때 메서드의 모든 예외사항을 전부 핸들링 해야 한다면 중복적이고 많은 양의 코드를 작성해야 하지만,AOP 방식을 이용하면 공통적인 예외사항에 대해서는 별도로 @ControllerAdvice를 이용해서 분리하는 방식입니다.

 

예제를 위해 프로젝트에 org.zerock.exception이라는 패키지를 생성하고 CommonExceptionAdvice 클래스를 생성 합니다.

 

CommonExceptionAdvice는 @ControllerAcvice 어노테이션을 적용하지만 예외처리를 목적으로 생성하는 클래스이므로 별도의 로직을 처리하지는 않습니다.

package org.zerock.exception;

import org.springframework.http.HttpStatus;
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 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";
	}
}

CommonExceptionAdvice 클래스에는 @ControllerAdvice라는 어노테이션과 @ExceptionHandler라는 어노테이션을 사용하고 있습니다.

@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.controller"></context:component-scan>
	<context:component-scan base-package="org.zerock.exception"></context:component-scan>

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

JSP는 error_page.jsp 이므로 /WEB-INF/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 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>

예외 메세지가 정상적으로 출력되는지 확인 해보려면 고의로 숫자나 날짜등의 파라미터 값을 변환에 문제가 있게 호출해 봅니다.

 

int 타입의 age에 문자열을 넣어 호출해 보겠습니다.

http://localhost:8080/sample/ex04?nameaaa&age=bbb&page=9

 

6.6.2 404 에러 페이지

WAS의 구동 중 가장 흔한 에러와 관련된 HTTP 상태 코드는 404와 500 에러 코드 일 것입니다.

500 메세지는 Internal Server Error 이므로 @ExceptionHandler를 이용해서 처리 되지만, 잘못된 URL를 호출할 떄 보이는 404 에러 메세지의 경우는 조금 다르게 처리하는것이 좋습니다.

 

서블릿이나 JSP를 이용했던 개발시에는 web.xml에 별도의 에러페이지를 지정할 수 있었습니다.

하지만 에러 발생시에는 추가적인 작업이 어렵기 때문에 스프링을 이용해서 404와 같이 WAS 내부에서 발생하는 에러를 처리하는 방식을 알아두는 것이 좋습니다.

 

스프링 MVC 의 모든 요청은 DispatcherServlet을 이용해서 처리되므로 404에러도 같이 처리할 수 있도록 web.xml을 수정 합니다.

		<init-param>
			<param-name>throwExceptionIfNoHandlerFound</param-name>
			<param-value>true</param-value>
		</init-param

 

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

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

		return "custom404";
	}

에러메세지는 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를 호출하면 custom404.jsp가 보이는 것을 확인할 수 있습니다.