개발이야기/Spring

[Code Lean Spring Web]6. Spring MVC의 Controller_Part1

cafe-jun12 2019. 11. 18. 23:16
반응형

이번 페이지에서는 스프링 MVC를 이용하는 경우 작성되는 Controller 가 어떻게 동작하는지 살펴보겠습니다. 스프링에서 Controller는 다음과 같은 특징을 가지고 있습니다.

 

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

스프링 MVC는 다른 프레임워크들과 달리 어노테이션을 중심으로 설계가 되기 때문에 어노테이션을 중점으로 정리를 해보겠습니다. 

 

@Controller, @RequestMapping


프로젝트 내 com.exe.controller라는 패키지를 생성후 SampleController라는 이름의 클래스를 생성합니다. 

 

 

SampleController

package com.exe.controller;

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


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

SampleController의 클래스에서 클래스 선언부에 @Controller 어노테이션을 작성하였습니다. 작성된 SampleController 클래스는 위의 그림 같이 자동으로 스프링의 객체로 등록이 됩니다. 여기서 servlet-context.xml 다음과 같이 작성을 합니다. 

 

<context:component-scan base-package="com.exe.controller"/>

servlet-context.xml

 

servlet-context.xml 에는 <context:component-scan>이라는 태그를 이용해서 지정된 패키지를 스캔 하도록 설정 되어 있습니다. 해당 패키지에 선언된 클래스들을 스캔하면서 스프링에서 객체 설정에 사용되는 클래스들을 파악하고 필요하다면 이를 객체로 생성해 관리하도록 합니다. 

 

 

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

@RequestMapping은 현재 클래스의 모든 메소드들의 기본적은 URL 경로가 됩니다 예를들어 SampleController클래스를 다음과 같이 '/sample/*' 이라는 경로로 지정했다면 다음과 같은 URL모두 SampleController에서 처리가 됩니다. 

 

  • /sample/aaa
  • /sample/bbb 

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

 

@RequestMapping를  테스트 하기위해서  com.exe.domain패키지 안에 SampleDTOSampleDTOList를 생성해 보았습니다. 

 

package com.exe.domain;

import lombok.Data;

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

SampleDTO

package com.exe.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<SampleDTO>();
	}
}

SampleDTOList

 

그리고 WAS를 실행했을 경우 Log확인해 살펴봅시다 

INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization started
INFO : org.springframework.web.context.support.XmlWebApplicationContext - Refreshing Root WebApplicationContext: startup date [Sun Nov 24 12:23:21 KST 2019]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from ServletContext resource [/WEB-INF/spring/root-context.xml]
INFO : org.springframework.beans.factory.support.DefaultListableBeanFactory - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@503d7773: defining beans []; root of factory hierarchy
INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 3536 ms
11월 24, 2019 12:23:24 오후 org.apache.catalina.core.ApplicationContext log
정보: Initializing Spring FrameworkServlet 'appServlet'
INFO : org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'appServlet': initialization started
INFO : org.springframework.web.context.support.XmlWebApplicationContext - Refreshing WebApplicationContext for namespace 'appServlet-servlet': startup date [Sun Nov 24 12:23:24 KST 2019]; parent: Root WebApplicationContext
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from ServletContext resource [/WEB-INF/spring/appServlet/servlet-context.xml]
INFO : org.springframework.context.annotation.ClassPathBeanDefinitionScanner - JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning
INFO : org.springframework.context.annotation.ClassPathBeanDefinitionScanner - JSR-330 'javax.inject.Named' annotation found and supported for component scanning
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
INFO : org.springframework.beans.factory.support.DefaultListableBeanFactory - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@2ba0e682: defining beans [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#0,org.springframework.format.support.FormattingConversionServiceFactoryBean#0,org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#0,org.springframework.web.servlet.handler.MappedInterceptor#0,org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#0,org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver#0,org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver#0,org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,org.springframework.web.servlet.resource.ResourceHttpRequestHandler#0,org.springframework.web.servlet.handler.SimpleUrlHandlerMapping#0,org.springframework.web.servlet.view.InternalResourceViewResolver#0,homeController,sampleController,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor#0]; parent: org.springframework.beans.factory.support.DefaultListableBeanFactory@503d7773
INFO : org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Mapped "{[/],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String com.exe.controller.HomeController.home(java.util.Locale,org.springframework.ui.Model)
INFO : org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Mapped "{[/sample/],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public void com.exe.controller.SampleController.basic()
INFO : org.springframework.web.servlet.handler.SimpleUrlHandlerMapping - Mapped URL path [/resources/**] onto handler 'org.springframework.web.servlet.resource.ResourceHttpRequestHandler#0'
INFO : org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'appServlet': initialization completed in 2097 ms

 

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

 

package com.exe.controller;

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

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 com.exe.domain.SampleDTO;
import com.exe.domain.SampleDTOList;

import lombok.extern.log4j.Log4j;

@Controller
@RequestMapping("/sample/*")
@Log4j
public class SampleController {
	
    //controller 로그 Get 방식 출력 확인 
	@RequestMapping(value="/basic",method= {RequestMethod.GET,RequestMethod.POST})
	public void basic() {
		log.info("basic get ..........");
	}
    // @GetMapping을 이용하여 로그 출력 
	@GetMapping("/basicOnlyGet")
	public void basiceGet2() {
		log.info("basic get only get .........");
	}	
    //dto 파라미터 수집  
	@GetMapping("/ex01")
	public String ex01(SampleDTO dto) {
		log.info(""+dto);
		return "ex01";
	}	
    //@RequestParam 활용한 데이터 수집 
	@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 "ex02Array";
	}
    // 객체 리스트  
	@GetMapping("/ex02Bean")
	public String ex02Bean(SampleDTOList list) {
		log.info("list dtos : "+list);
		return "ex02Bean";
	}
	
	
}

 

해당 소스틑 URL 에 하나씩 매칭을 시켜 로그를 출력을 해보면 아래와 같이 로그가 출력이 됩니다. 

 

</basic 호출시>
INFO : com.exe.controller.SampleController - basic get ..........

</basicOnlyGet 호출시 생성 로그>
INFO : com.exe.controller.SampleController - basic get only get .........

</ex01?name=aaa&age=25 호출시 생성 로그>
INFO : com.exe.controller.SampleController - SampleDTO(name=aaa, age=25)

</ex02?name=aaa&age=10 호출시 생성 로그>
INFO : com.exe.controller.SampleController - name : aaa
INFO : com.exe.controller.SampleController - age : 10

</ex02List?ids=111&ids=222&ids=333 호출시 생성로그>
</ex02Array?ids=111&ids=222&ids=333 도 동일한 결과를 나타냄>
INFO : com.exe.controller.SampleController - ids :[111, 222, 333]

</list%5B0%5D.name=aaa&list%5B1%5D.name=bbb&list%5B2%5D.name=ccc 호출시 >
INFO : com.exe.controller.SampleController - list dtos : SampleDTOList(list=[SampleDTO(name=aaa, age=0),SampleDTO(name=bbb, age=0), SampleDTO(name=ccc, age=0)])

 

 

@InitBinder


파라미터 수집을 다른 용어로는 'binding(바인딩)'이라고 합니다. 변환이 가능한 데이터는 자동으로 변환이 되지만 경우에 따라서 파라미터 타입을 변환하여 처리를 해야하는 경우도 존재할수도 있습니다. 예를 들면 '2018-01-01'과 같이 문자열로 전달된 데이터를 java.util.Date 타입으로 변환하는 작업이 존재할수 있습니다. 스프링에서는 Controller에서 파라미터를 바인딩 할때 자동으로 호출되는 @InitBinder를 이용해서 이러한 변환을 처리할수 있습니다. 

 

@InitBinder를 테스트 하기 위해서 TodoDTO 를 생성하였습니다. 

 

package com.exe.domain;

import java.util.Date;

import lombok.Data;

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

TodoDTO 클래스

 

package com.exe.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 com.exe.domain.SampleDTO;
import com.exe.domain.SampleDTOList;
import com.exe.domain.TodoDTO;

import lombok.extern.log4j.Log4j;

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


    <중략 ,,, >
    @InitBinder
	public void initBinder(WebDataBinder binder) {
		SimpleDateFormat dateFormat = new SimpleDateFormat();
		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=2019-03-05 를 호출하였을경우 서버에서는 정상적으로 파라미터를 수집하여 처리 합니다. 

 

INFO : com.exe.controller.SampleController - todo : TodoDTO(title=test, dueDate=Sat Jan 05 00:03:00 KST 2019)

 

여기서 @InitBinder 처리가 되어 있지 않다면 브라우저에서는 400에러가 발생합니다. (400 에러는 syntax)이 잘못 되었다는 의미입니다.) 

 

@DateTimeFormat


@InitBinder를 이용하여 날짜 변환을 하는 경우도 있지만 파라미터로 사용되는 인스턴스 변수에는 @DateTimeFormat을 이용하여 적용해도 변환이 가능합니다. (@DateTimeFormat을 이용하는 경우에는 @InitBinder는 필요하지 않습니다) 기존의 SampleControllerInitBinder 코드를 지우고 @DateTimeFormat 적용하여 테스트를 했을 경우 동일한 결과가 출력 됩니다. 

 

 

package com.exe.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;
	
}

TodoDTO 

 

</ex03?title=test&dueDate=2019/03/05 호출시 출력 로그>
INFO : com.exe.controller.SampleController - todo : TodoDTO(title=test, dueDate=Sat Jan 05 00:03:00 KST 2019)

 

Model이라는 데이터 전달자


Controller의 메소드를 생성할때는 Model이라는 타입을 파라미터로 지정이 가능합니다. Model이라는건 JSP에 컨트롤러에서 생성한 데이터를 담아서 전달하는 역활을 하는 존재입니다. 메서드에서 Model타입이 지정된 경우 스프링은 특별하게 Model 타입의 객체를 만들어 주입하면 됩니다. 

 

기존 Model 2 방식에서 사용하는 request.setAttribute()와 유사한 역활을 합니다. 

 

request.setAttribute(serverTime,new java.util.Date());
RequestDispatcher dispatcher = request.getRequsetDispatcher("/WEB-INF/jsp/home.jsp");
disatcher.forwart(request,response);

 

위 코드를 스프링에서는 아래와 같이 사용합니다. 

 

public String home(Model model){
	model.addttribute("serverTime",new java.util.Date());
    return  "home";
}

 

메서드의 파라미터를 Model 타입으로 선언하게 되면 자동으로 스프링 MVC에서 Model 타입의 객체를 생성해주기 때문에 개발자 입장에서는 필요한 데이터를 담아주는 작업만으로도 작업을 처리할수가 있습니다.

 

  • 리스트를 페이지 번호로 전달받고 실제 데이터를 View로 전달해야하는경우 
  • 파라미터들에 대한 처리후 결과를 전달하는 경우 

 

@ModelAttribute 어노테이션 


웹 페이지 구조는 Request에 전달된 데이터를 가지고 필요하다면 추가적인 데이터를 생성해서 화면으로 전달하는 방식으로 동작을 합니다. 스프링 MVC는 Controller는 기본적으로 Java Beans 규칙에 맞는 객체는 다시 화면으로 전달 합니다. 전달이 될때에는 클래스 앞글자는 소문다로 처리가 됩니다. 반면 기본 자료형의 경우는 파라미터로 선언하더라도 기본적으로 화면까지 전달 되지 않습니다. 하지만 @ModelAttribute는 강제로 전달받은 파라미터를 Model에 담아서 전달하도록할때 필요한 어노테이션입니다. @ModelAttributer가 걸린 파라미터는 타입에 관계 없이 Model에 담아서 전달이 되므로 유용하게 사용이 됩니다.  

 

//SampleController 에 추가 

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

따로 settAttribute 를 안해줘도 @ModelAttribute어노테이션으로 데이터가 전달이 됩니다. 

 

출력 메시지

콘솔창에 로그를 확인해 보면 다음과 같습니다. 

INFO : com.exe.controller.SampleController - dto : SampleDTO(name=aaa, age=11)
INFO : com.exe.controller.SampleController - page : 8

 Model 

 

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

RedirectAtrribute는 일회성으로 데이터를 전달하는 용도로 Redirect시 요청된 URL 로 데이터가 전달이 됩니다.  JSP/Servlet 에서 response.sendRedirect("/home.fu?name=aaa&age=10"); 과 동일한 용도로 사용이 됩니다. 

rttr.addFlashAttribute("name","AAA")
rttr.addFlashAttribute("age",10)
return "redirect:/";

 

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

 

Controller 리턴 타입


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

 

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

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

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

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

  • Model,ModelAndView : Model로 데이터를 반환하거나 화면가지 같이 지정하는 경우에 사용 (최근에는 사용 안함)

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

 

1. void 타입 

메서드의 리턴 타입을 void로 지정 하는 경우 일반적인 경우에 해당 URL의 정보를 그대로 jsp파일의 이름으로 사용합니다. 다음 SampleController 에 해당 메서드를 추가한뒤 /sample/ex05를 호출하게 되면 404페이지가 출력됩니다. 

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

 

404 페이지

에러 메세지를 보면 원인이 /WEB-INF/views/sample/ex05.jsp 파일이 없어 생기는 에러임을 확인할수 있습니다. 그래서 다음과 같이  /WEB-INF/views/sample/ 경로에 jsp 파일을 다음과 같이 추가하다면 jsp 파일이 호출되는것을 확인할수가 있습니다. 

 

ex05 jsp 파일 추가 

 

jsp 파일을 추가한되 동일한 URL을 출력한다면 해당 페이지가 출력되는것을  확인할수가 있습니다. 

 

그렇다면 이것이 어떤 설정에 의해 출력이 되는지 궁금해 할수가 있습니다. 이것은 servlet-context.xml의 아래 설정과 같이 맞물려 URL 경로를 View로 처리하기 때문에 생기는 결과 입니다. 

	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<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>

 

2. String 타입 

String의 타입의 경우는 상황에 따라 다른 화면을 보여줄 경우에 유용에 사용합니다. (if~else와 같은 처리가 있는 상황)  String 타입에는 다음과 같은 특별한 키워드를 붙여서 사용할수가 있습니다. 

 

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

3. 객체 타입 

Controller의 메서드 리턴타입을 VO(Value Object)나 DTO(Data Transfer Object)타입등 복합적인 데이터가 들어간 객체 타입으로 지정할수가 있는데 이 경우는 주로 Json데이터를 만들어 내는 용도로 사용합니다. 우선 json 데이터를 만들어 내기 위해 jackson-databind 라이브러리를 pom.xmlㄹ에 추가합니다. 

 

	<!-- 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("/ex05 ...... ");
		SampleDTO dto = new SampleDTO();
		dto.setAge(10);
		dto.setName("홍길동");
		return dto;
	}

 

 

그리고 /ex06을 호출한다면 다음과 같이 json 형식으로 출력이 됩니다. 

 

 

2. ResponseEntity 타입 

Web을 다루다 보면 HTTP 프로토콜의 헤더를 다루는 경우도 종종 있습니다. 스프링 MVC사상은 HttpServletRequest나 HttpServletResponse를 직접 핸들링 하지 않아도 이런 작업이 가능하도록 작성이 되었이기 때문에 ResponseEntity를 통해서 원하는 데이터를 얻을수 있습니다. 테스트를 하기 위해 SampleController 에 메서드를 추가합니다. 

 

@GetMapping("/ex07")
	public ResponseEntity<String> 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);
	}

 

그리고 ex07 를 출력하게 된다면 다음과 같이 json 형식으로 출력에 됩니다. 

 

 

ResponseEntity

 

ResponseEnntity 는 HttpHeaders 객체를 같이 전달할 수 있고 이를 통해서 원하는 HTTP 해더 메세지를 전달하는 것이 가능합니다. ex07()은 JSON타이이라는 헤더 메세지와 200 OK라는 상태 코드를 전송합니다. 

 

지금가지 스프링 MVC Controller 에 대한 글이였습니다. chapter 6는 너무 내용이 길거서 Part 1 과 Part 2로 나누어서 설명을 하겠습니다. 

 

긴글 읽어 주셔서 감사합니다. 

 

 

반응형