[Spring] ViewResolver 설정


ViewResolver 설정


뷰 영역 구현

컨트롤러는 최종적으로 결과를 출력할 뷰와 뷰에 전달할 객체를 담고 있는 ModelAndView 객체를 리턴한다.

DispatherServlet은 ViewResolver를 사용하여 결과를 출력할 View 객체를 구하고, 구한 View 객체를 이용하여 내용을 생성한다.


1. 컨트롤러 구현 및 설정 추가

컨트롤러를 구현하려면 먼저 @Contoller 어노테이션을 클래스에 적용한다. 그리고, @RequestMapping 어노테이션을 이용해서 클라이언트의 요청을 처리할 메서드를 지정한다.

package madvirus.spring.chap06.controller;

import java.util.Calendar;

import org.springframework.stereotype.Controller;

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

import org.springframework.web.servlet.ModelAndView;


// @Controller 어노테이션은 해당 클래스가 스프링 MVC의 컨트롤러를 구현한 클래스라는 것을 지정한다. 

@Controller

public class HelloController {


// @RequestMapping 어노테이션은 값으로 지정한 요청 경로를 처리한 메서드를 설정한다. 

// 이 경우 http://host:port[/컨텍스트 경로]/hello.do 요청을 HelloController 클래스의 hello() 메서드가 처리하게 된다.

@RequestMapping("/hello.do")

public ModelAndView hello() {

//ModelAndView는 컨트롤러의 처리 결과를 보여줄 뷰와 뷰에서 출력할 모델을 지정할 때 사용된다.

ModelAndView mav = new ModelAndView();

// 컨트롤러의 처리 결과를 보여줄 뷰의 이름을 'hello'를 지정한다.

mav.setViewName("hello");

// 모델에 'greeting'이라는 이름으로 String 타입의 값을 추가하였다.

mav.addObject("greeting", getGreeting());

return mav;

}

// 스프링은 ModelAndView 뿐만 아니라 String이나 modelMap, 또는 Map과 같은 타입을 이용해서 뷰 이름과 모델 정보를 설정할 수 있도록 하고 있다.

private String getGreeting() {

int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);

if (hour >= 6 && hour <= 10) {

return "좋은 아침입니다.";

} else if (hour >= 12 && hour <= 15) {

return "점심 식사는 하셨나요?";

} else if (hour >= 18 && hour <= 22) {

return "좋은 밤 되세요";

}

return "안녕하세요";

}

}


DispatherServlet은 스프링 컨테이너에서 컨트롤러 객체를 검색하기 때문에 스프링 설정 파일에 컨트롤러를 빈으로 등록해 주어야 한다.

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

      http://www.springframework.org/schema/context

        http://www.springframework.org/schema/context/spring-context-3.0.xsd">


<bean id="helloController" class="madvirus.spring.chap06.controller.HelloController" />

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">

<property name="prefix" value="/WEB-INF/view/" />

<property name="suffix" value=".jsp" />

</bean>

</beans>

스프링 2.5 버전까지는 @Controller 어노테이션이 적용된 클래스를 컨트롤러로 사용하려면 별도의 DefaultAnnoationHandlerMapping과 AnnotationHandlerAdapter를 스프링 설정 파일에 등록해 주어야 한다. 하지만 스프링 3.0부터는 이 두 빈 객체를 기본 구현체로 사용하기 때문에 별도의 설정을 하지 않을 경우 @Controller 어노테이션이 적용된 클래스를 컨트롤러 구현체로 사용할 수 있게 된다.


2. 설정 파일에 ViewResolver 설정 추가

컨트롤러 클래스는 직접 또는 간접적으로 ModelAndView 객체를 생성하게 된다.

예를 들어 앞서 작성한 HelloController 클래스는 다음과 같이 ModelAndView 객체를 생성해서 리턴하였다.


public ModelAndView hello() {

ModelAndView mav = new ModelAndView();

// 뷰이름

mav.setViewName("hello");// setViewName() 메서드를 이용하여 뷰 이름을 지정한 모습

mav.addObject("greeting", getGreeting());

return mav;

}

컨트롤러의 처리 결과를 보여줄 뷰의 이름을 'hello'를 지정하였는데, DispatherServlet은 이 뷰 이름과 매칭되는 뷰 구현체를 찾기 위해 ViewResolver를 사용한다.


JSP를 뷰 기술로 사용할 경우 다음과 같이 InternalResourceViewResolver 구현체를 빈으로 등록해주면 된다.

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">

<property name="prefix" value="WEB-INF/view"/>

<property name="suffix" value=".jsp/>

// 이는 ViewResolver가 "WEB-INF/view/뷰이름.jsp"를 뷰 JSP로 사용한다는 것을 의미한다. 

// 즉, 앞의 예에서 HelloController는 뷰 이름으로 "hello"를 리턴하므로, 실제로 사용되는 뷰 파일은 "WEB-INF/view/hello.jsp"파일이 된다.

</bean>


InternalResourceViewResolver는 컨트롤러가 지정한 뷰 이름으로부터 실제로 사용될 뷰를 선택하는데, 이 때 컨트롤러가 지정한 뷰 이름 앞뒤로 prefix 프로퍼티와 suffix 프로퍼티를 추가한 값이 실제로 사용될 자원의 경로가 된다.


※ ViewResolver 구현 클래스

스프링 컨트롤러는 뷰에 의존적이지 않다. 컨트롤러는 아래 코드와 같이 결과를 생성할 뷰의 이름만 지정할 뿐이다.

컨트롤러가 지정한 뷰 이름으로부터 응답 결과 화면을 생성하는 View 객체는 ViewResolver가 구한다.

스프링은 몇 가지 ViewResolver 구현 클래스를 제공하고 있는데, 이중 주료 ViewResolver 구현 클래스는 다음과 같다.


3. ViewResolver 인터페이스


package org.springframework.web.servlet;


import java.util.Locale;

public interface ViewResolver{

View resolveViewName(String viewName, Locale locale) throws Exception;

}

ViewResolver는 뷰 이름과 지역화를 위한 Locale을 파라미터로 전달받으며, 매핑되는 View 객체를 리턴한다. 만약, 매핑되는 View 객체가 존재하지 않으면 null을 리턴한다.


4. View 객체

ViewResolver는 응답 결과를 생성할 뷰 객체를 리턴한다. 모든 뷰 클래스는 View 인터페이스를 구현하고 있으며, View 인터페이스는 다음과 같이 정의되어 있다.


public interface View {

    String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";

    String getContentType();

    void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;

}


getContentType() 메서드는 "text/html"과 같은 응답 결과의 컨텐츠 타입을 리턴한다. render() 메서드는 실제로 응답 결과를 생성한다. render() 메서드의 첫 번째 파라미터인 model에는

컨트롤러가 리턴한 ModelAndView 객체의 모델 데이터가 전달된다. 각각의 View 객체는 이 모델 데이터로부터 응답 결과를 생성하는데 필요한 정보를 구한다.


'Programing > Spring' 카테고리의 다른 글

[Spring] AOP 개요  (0) 2014.12.22
[Spring] Locale 처리  (0) 2014.12.21
[Spring] @RequestBody 어노테이션과 @ReponseBody 어노테이션의 사용  (1) 2014.12.18
[Spring] 파일업로드 처리  (2) 2014.12.18
[Spring] 요청 URI 매칭  (0) 2014.12.18

[Spring] @RequestBody 어노테이션과 @ReponseBody 어노테이션의 사용


@RequestBody 어노테이션과 @ReponseBody 어노테이션의 사용


웹 서비스와 REST 방식이 시스템을 구성하는 주요 요소로 자리 잡으면서 웹 시스템간에 XML이나 JSON 등의 형식으로 데이터를 주고 받는 경우가 증가하고 있다.

이에 따라 스프링 MVC도 클라이언트에서 전송한 XML 데이터나 JSON 또는 기타 데이터를 컨트롤러에서 DOM 객체나 자바 객체로 변환해서 받을 수있는 기능(수신)을 제공하고 있으며,

비슷하게 자바 객체를 XML이나 JSON 또는 기타 형식으로 변환해서 전송할 수 있는 기능(송신)을 제공하고 있다.


@RequestBody 어노테이션과 @ResponseBody 어노테이션은 각각 HTTP 요청 몸체를 자바 객체로 변환하고 자바 객체를 HTTP 응답 몸체로 변환하는 데 사용된다.

@RequestBody 어노테이션을 이용하면 HTTP 요청 몸체를 자바 객체로 전달받을 수 있다. 비슷하게 @ResponseBody 어노테이션을 이용하면 자바 객체를 HTTP 응답 몸체로 전송할 수 있다.


import org.springframework.stereotype.Controller;

import org.springframework.util.MultiValueMap;

import org.springframework.web.bind.annotation.RequestBody;

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

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.ResponseBody;


@Controller

public class SimpleConverterController {


@RequestMapping(value = "/test/simpleTest.do", method = RequestMethod.GET)

public String simpleTestForm() {

return "test/simpleTestForm";

}


@RequestMapping(method = RequestMethod.POST)

// @RequestBody 어노테이션은 @RequestMapping에 의해 POST 방식으로 전송된 HTTP 요청 데이터를 String 타입의 body 파라미터로 전달된다.(수신)

// 그리고 @ResponseBody 어노테이션이 @RequestMapping 메서드에서 적용되면 해당 메서드의 리턴 값을 HTTP 응답 데이터로 사용한다.

// simpleTest() 메서드의 리턴 값이 String 타입이므로 String 데이터를 HTTP 응답 데이터로 전송한다.(송신)

@ResponseBody

public String simpleTest(@RequestBody String body) {

return body;

}

}


SimpleConverterController에 GET 방식으로 요청이 전달되면 simpleTestForm 뷰가 사용되는데, 이 뷰가 아래와 같은 폼을 생성한다고 해보자

<form method="POST">

이름: <input type="text" name="name" /> <br/>

나이: <input type="text" name="age" />

<input type="submit" />

</form>


이때 HTTP 몸체로 전송되는 데이터는 다음과 같은 형식을 취한다.

name=%C3%D6%B9%FC%B1%D5%$age=34


SimpleConverterController 컨트롤러는 POST 방식으로 요청이 들어오는 경우 @RequestBody 어노테이션이 적용되어 있으므로 그대로 결과 값으로 리턴한다.

그런데, @ResponseBody 어노테이션이 적용되어 있으므로 결과적으로 HTTP 요청 몸체 데이터가 HTTP 응답 몸체로 전송된다.


스프링 MVC는 HttpMessageConverter를 이용해서 자바 객체와 HTTP 요청/응답 몸체 사이의 변환을 처리하는데, @ResponseBody 어노테이션 적용 메서드의 리턴 타입이 String인 경우

HTTP 응답 데이터의 컨텐츠 타입은 "text/plain;charset=ISO-8859-1"이 된다.


1. HttpMessageConverter를 이용한 변환 처리

AnnotationMethodHandlerAdapter 클래스는 @RequestBody 어노테이션이 적용된 파라미터나 @ResponseBody 어노테이션이 적용된 메서드에 대해 HttpMessageConverter를 사용해서 변환을 처리한다. 주요 HttpMessageConverter 구현 클래스는 다음과 같다.


 구현 클래스

 설 명

 ByteArrayHttpMessageConverter(*)

 HTTP 메시지와 byte 배열 사이의 변환을 처리한다. 컨텐츠 타입은  application/octet-stream이다.

 StringHttpMessageConverter(*)

 HTTP 메시지와 String 사이의 변환을 처리한다. 컨텐츠 타입은  text/plain;charset=ISO-8859-1이다.

 FormHttpMessageConverter(*)

 HTML 폼 데이터를 MultiValueMap으로 전달받을 때 사용된다. 지원하는 컨텐  츠 타입은 application-x-www-form-urlencorded이다.

 SourceHttpMessageConverter(*)

 HTTP 메시지와 javax.xml.transform.Source 사이 변환을 처리한다. 컨텐츠 타  입은 application/xml 또는 text/xml이다.

 MarshallingHttpMessageConverter(*)

 스프링의 Marshaller와 unMarshaller를 이용해서 XML HTTP 메시지와 객체 사  이의 변환을 처리한다. 컨텐츠 타입은 application/xml 또는 text/xml이다.

 MappingJacksonHttpMessageConverter(*)

 Jackson 라이브러리를 이용해서 JSON HTTP 메시지와 객체 사이의 변환을 처  리한다. 컨텐츠 타입은 applicaion/json이다.


만약 기본 구현 클래스가 아닌 다른 구현 클래스를 사용하려면 다음과 같이 AnnotationMethodHandlerAdapter에 명시적으로 구현 클래스 목록을 지정해 주어야 한다.

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">

<property name="cacheSeconds" value="0" />

<property name="alwaysUseFullPath" value="true" />

<property name="webBindingInitializer">

<bean class="madvirus.spring.chap06.binder.CustomWebBindingInitializer" />

</property>

<property name="messageConverters">

<list>

<ref bean="byteArrayHttpMessageConverter" />

<ref bean="marshallingHttpMessageConverter" />

<!--<ref bean="stringHttpMessageConverter" />-->

<!--<ref bean="formHttpMessageConverter" />-->

<!--<ref bean="sourceHttpMessageConverter" />-->

<!--<ref bean="jsonHttpMessageConverter" />-->

</list>

</property>

</bean>


<bean id="byteArrayHttpMessageConverter" class="org.springframework.http.converter.ByteArrayHttpMessageConverter" />

<bean id="marshallingHttpMessageConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">

<property name="marshaller" ref="jaxb2Marshaller" />

<property name="unmarshaller" ref="jaxb2Marshaller" />

</bean>


예를 들어, 다음과 같이 JAXB 2의 어노테이션이 적용된 자바 객체가 있다고 하자

public class WriteArticleServiceImpl{@XmlAccessorType(XmlAccessType.FIELD)

@XmlRootElement(name="message-list")

public class GuestMessageList{

@XmlElement(name="message")

private List<GuestMessage> message;

...

}


@XmlAccessorType(XmlAccessType.FIELD)

@XmlType(name="", propOrder={"id", "message", "creationTime"})

public class GuestMessage{

private Integer id;

private String message;

private Date creationTime;

}


컨트롤러에서는 아래 코드와 같이 @ResponseBody 어노테이션을 이용해서 GuestMessageList를 리턴하도록 구현할 수 있을 것이다.

@Controller

public class GuestMessageController {


@RequestMapping(value = "/guestmessage/xml.do", method = RequestMethod.GET)

@ResponseBody

public GuestMessageList listXml() {

GuestMessageList list = ...;

return list;


}

...

}


JAXB2를 이용해서 자바 객체를 XML 응답으로 전송하려면 MarshallingHttpMessageConverter의 marshaller와 unmarshaller로 스프링 OXM 모듈의 Jaxb2Marshaller를 사용하면 된다.

<bean id="marshallingHttpMessageConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">

<property name="marshaller" ref="jaxb2Marshaller" />

<property name="unmarshaller" ref="jaxb2Marshaller" />

</bean>


<bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">

<property name="classesToBeBound">

<list>

<value>madvirus.spring.chap06.model.GuestMessageList</value>

</list>

</property>

</bean>

이제 스프링 MVC는 MarshallingHttpMessageConverter를 이용해서 GuestMessageList 객체를 XML로 변환할 수 있게 된다.


2. Content-Type과 Accept 헤더 기반의 변환 처리

AnnotationMethodHandlerAdapter가 HttpMessageConverter를 이용해서 요청 몸체 데이터를 @RequestBody 어노테이션이 적용된 자바 객체로 변환할 때에는 HTTP 요청 헤더의 Content-Type 헤더에 명시된 미디어 타입(MIME)을 지원하는 HttpMessageConverter 구현체를 선택한다.

예를 들어, 요청 미디어 타입이 application/xml 이고 @RequestBody 어노테이션이 적용된 파라미터가 Source 타입인 경우 SourceHttpMessageConverter가 선택된다.

비슷하게 @ResponseBody 어노테이션을 이용해서 리턴한 객체를 HTTP 응답 객체로 변환할 때에는 HTTP 요청 헤더의 Accept 헤더에 명시된 미디어 타입을 지원하는 HttpMessageConveter 구현체를 선택한다.

예를 들어, Accept 헤더에 명시된 값이 application/json이고 @ResponseBody 어노테이션이 적용된 메서드의 리턴 타입이 자바 객체인 경우

MappingJacksonHttpMessageConverter가 선택된다. 예를 들어, 자바스크립트레서 아래와 같은 코드를 이용해서 Accept 헤더의 값으로 "application/json"을 지정했다고 하자.


<script type="text/javascript">

...

xmlhttp.open("GET","json.do",true);

xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

xmlhttp.setRequestHeader("Accept", "application/json");

xmlhttp.send(null);

...

</script>


이 경우 다음과 같이 @ResponseBody 어노테이션이 적용된 메서드의 리턴 객체를 변환할 때 MappingJacksonHttpMessageConverter를 사용하게 된다.

@RequestMapping(value = "/guestmessage/json.do", method = RequestMethod.GET, headers = "accept=application/json")

@ResponseBody

public GuestMessageList listJson() {

...

}


3. MappingJacksonJsonView를 이용한 JSON 응답 생성

스프링3 버전은 자바 객체를 JSON으로 변환해서 보여주는 뷰 구현 클래스인 MappingJacksonView를 제공하고 있다. 


아래 코드는 MappingJacksonJsonView의 설정 예를 보여주고 있다.

<bean id="viewResolver" class="org.springframework.web.servlet.view.BeanNameViewResolver" />


<bean id="pageJsonReport" class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" />


컨트롤러에서는 다음과 같이 뷰 이름으로 MappingJacksonJsonView 타입의 빈을 설정해주면 된다.

@RequestMapping("/pageReport")

public ModelAndView pdfReport() {

List<PageRank> pageRanks = new ArrayList<PageRank>();

pageRanks.add(new PageRank(1, "/bbs/mir2/list"));

pageRanks.add(new PageRank(2, "/bbs/mir3/list"));

pageRanks.add(new PageRank(3, "/bbs/changchun2/list"));

return new ModelAndView("pageReport", "pageRanks", pageRanks);

}


MappingJacksonJsonView는 모델에 저장된 모든 객체를 JSON 형식으로 변환해준다.

{"report":{"pageRanks":["rank":1,"page"."/bbs/mir2/list"},{"rank":2,"page":"/bbs/mir3/list},{"rank":3,"page":"/bbs/changchun2/list"}]}}


예를 들어,  컨틀롤러에서 모델에 다음과 같이 값을 설정했다고 해보자

public String controllerMethod(Model model){

...

model.addAttribute("report", report);

model.addAttribute("summary", summary);

...

}


 컨트롤러에서 위와 같이 모델을 설정한 경우 MappingJacksonJsonView는 다음과 같은 형식의 JSON 응답 결과를 생성한다.

{"report":.... "summary":...}



'Programing > Spring' 카테고리의 다른 글

[Spring] Locale 처리  (0) 2014.12.21
[Spring] ViewResolver 설정  (0) 2014.12.18
[Spring] 파일업로드 처리  (2) 2014.12.18
[Spring] 요청 URI 매칭  (0) 2014.12.18
[Spring] 모델 생성하기  (1) 2014.12.18

[Spring] 파일업로드 처리


파일 업로드 처리


인코딩 타입이 Multipart인 경우 파라미터나 업로드한 파일을 구하려면 전송 데이터를 알맞게 처리해 주어야 한다.

스프링은 Multipart 지원 기능을 제공하고 있기 때문에, 이 기능을 이용하면 추가적인 처리없이 Multipart 형식으로 전송된 파라미터와 파일 정보를 쉽게 구할 수 있다.


1. MultipartResolver 설정

Multipart 지원 기능을 이용하려면 먼저 MultipartResolver를 스프링 설정 파일에 등록해 주어야 한다. MultipartResolver는 Multipart 형식으로 데이터가 전송된 경우, 해당 데이터를 스프링 MVC에서 사용할 수 있도록

변환해준다. 예를 들어, @PathVariable 어노테이션을 이용해서 Multipart로 전송된 파라미터와 파일을 사용할 수 있도록 해준다.


스프링이 기본으로 제공하는 MultipartResolver는 CommmnosMultipartResolver이다. CommmnsMultipartResolver는 Commons FileUpload API를 이용해서 Multipart를 처리해준다.

CommmonsMultipartResolver를 MultipartResolver로 사용하려면 다음과 같이 빈 이름으로 "multipartResolver"를 사용해서 등록하면 된다.


<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean>

cf.) DispatcherServlet은 이름이 "multipartResolver"인 빈을 사용하기 때문에 다른 이름을 지정할 경우 MultipartResolver로 사용되지 않는다.


※ CommonsMultipartResolver 클래스의 프로퍼티

 프로퍼티

 타입

설명 

 maxUploadSize

 long

 최대 업로드 가능한 바이트 크기, -1은 제한이 없음을 의미한다. 기본 값은 -1이다.

 maxInMemorysize

 int 

 디스크에 임시 파일을 생성하기 전에 메모리에 보관할 수 있는 최대 바이트 크기, 

 기본 값은 10240 바이트이다.

 defaultEncording

 String 

 요청을 파싱할 때 사용할 캐릭터 인코딩, 지정하지 않은 경  우 HttpServletRequest.setEncording() 메서드로 지정한 캐릭터 셋이 사용된다. 

 아무 값도 없을 경우 ISO-8859-1을 사용한다.


2. @RequestParam 어노테이션을 이용한 업로드 파일 접근

업로드한 파일을 전달받는 첫 번째 방법은 @RequestParam 어노테이션이 적용된 MultipartFile 타입의 파라미터를 사용하는 것이다. 예를 들어, HTML 입력폼이 다음과 같이 작성되어 있다고 해보자

<form action="submitReport1.do" method="post" enctype="multipart/form-data">

학번: <input type="text" name="studentNumber" />

<br/>

리포트파일: <input type="file" name="report" />

<br/>

<input type="submit" />

</form>

위 HTML 코드에서 파일은 report 파라미터를 통해서 전달된다. 


이 경우 다음 코드와 같이 @RequestParam 어노테이션과 MultipartFile 타입의 파라미터를 이용해서 업로드 파일 데이터를 전달받을 수 있다.

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.multipart.MultipartFile;


@Controller

public class ReportSubmissionController {


@RequestMapping(value = "/report/submitReport1.do", method = RequestMethod.POST)

public String submitReport1(

@RequestParam("studentNumber") String studentNumber,

// MultipartFile이 제공하는 메서드를 이용해서 업로드 데이터 접근

@RequestParam("report") MultipartFile report) {

printInfo(studentNumber, report);

return "report/submissionComplete";

}

...

}

MultipartFile 인터페이스는 스프링에서 업로드 한 파일을 표현할 때 사용되는 인터페이스로서, MultipartFile 인터페이스를 이용해서 업로드 한 파일의 이름, 실제 데이터, 파일 크기 등을 구할 수 있다.


3. MultipartHttpServletRequest를 이용한 업로드 파일 접근


업로드한 파일을 전달받는 두 번째 방법은 MultipartHttpServletRequest 인터페이스를 이용하는 것이다.

import org.springframework.web.multipart.MultipartFile;

import org.springframework.web.multipart.MultipartHttpServletRequest;


@Controller

public class ReportSubmissionController {

@RequestMapping(value = "/report/submitReport2.do", method = RequestMethod.POST)

public String submitReport2(MultipartHttpServletRequest request) {

String studentNumber = request.getParameter("studentNumber");

MultipartFile report = request.getFile("report");

printInfo(studentNumber, report);

return "report/submissionComplete";

}

}

MultopartHttpServletRequest 인터페이스는 스프링이 제공하는 인터페이스로서, Multipart 요청이 들어올 때 내부적으로 원본 HttpServletRequest 대신 사용되는 Multipart 요청이 들어올 때 

내부적으로 원본 HttpServletRequest 대신 사용되는 인터페이스이다. MultipartHttpServletRequest 인터페이스는 실제로는 어떤 메서드도 선언하고 있지 않으며,  HttpServletRequest 인터페이스와 MultipartRequest 인터페이스를 상속받고 있다. MultipartHttpServletRequest 인터페이스는 javax.servlet.HttpServletRequest 인터페이스를 상속받기 때문에 웹 요청 정보를 구하기 위한 getParameter()나 getHeader()와 같은 메서드를 사용할 수 있으며, 추가로 MultipartRequest 인터페이스가 제공하는 Multipart 관련 메서드를 사용할 수 있다.


※ MultipartRequest 인터페이스의 파일 관련 주요 메서드

 메서드

설 명 

 Iterator<String> getFileNames()

 업로드 된 파일들의 이름 목록을 제공하는 Iterator를 구한다.

 MultipartFile getfile(String name)

 파라미터 이름이 name이 업로드 파일 정보를 구한다.

 List<MultipartFile> getFiles(String name)

 파라미터 이름이 name인 업로드 파일 정보 목록을 구한다.

 Map<String, MultipartFile> getFileMap()

 파라미터 이름을 키로 파라미터에 해당하는 파일 정보를 값으로 하는 Map을 구한다.


4. 커맨드 객체를 통한 업로드 파일 접근


커맨드 객체를 이용해도 업로드 한 파일을 전달받을 수 있다. 단지 커맨드 클래스에 파라미터와 동일한 이름의 MultipartFile 타입 프로퍼티를 추가해주기만 하면 된다.

예를 들어, 업로드 파일의 파라미터 이름이 "report"인 경우, 다음과 같이 "report" 프로퍼티를 커맨드 클래스에 추가해 주면 된다.


import org.springframework.web.multipart.MultipartFile;


public class ReportCommand {


private String studentNumber;

private MultipartFile report;


public String getStudentNumber() {

return studentNumber;

}


public void setStudentNumber(String studentNumber) {

this.studentNumber = studentNumber;

}


public MultipartFile getReport() {

return report;

}


public void setReport(MultipartFile report) {

this.report = report;

}

}


위 코드와 같이 MultipartFile 타입의 프로퍼티를 커맨드 클래스에 추가해주었다면, @RequestMapping 메서드의 커맨드 객체로 사용함으로써 업로드 파일 정보를 커맨드 객체를 통해서 전달받을 수 있게 된다.

@Controller

public class ReportSubmissionController{

...

@RequestMapping(value="/report/submitReport3.do",medhod = RequestMethod.POST)

public String submitReport3(ReportCommand reportCommand){

...

}

}


5. MultupartFile 인터페이스 사용


org.springframework.web.MutipartFile 인터페이스는 업로드 한 파일 및 파일 데이터를 표현하기 위한 용도로 사용된다.

MultipartFile 인터페이스가 제공하는 주요 메서드는 다음과 같다.


※ MulripartFile 인터페이스의 주요 메서드

 메서드

 설 명

 String getName()

 파라미터 이름을 구한다.

 String getOriginalFilename()

 업로드 한 파일의 이름을 구한다.

 String isEmpty()

 업로드 한 파일이 존재하지 않는 경우 true를 리턴한다.

 long getSize()

 업로드 한 파일의 크기를 구한다.

 byte[ ] getBytes() throws IOExcetion

 업로드 한 파일 데이터를 구한다.

 InputStream getInputStream() throws  IOException

 업로드한 파일 데이터를 읽어노는 InputStream을 구한다. InputStream의 사용  이 끝나면 알맞게 종료해  주어야 한다.

 void transferTo(File dest) throws IOException

 업로드 한 파일 데이터를 지정한 파일에 저장한다.


업로드 한 파일 데이터를 구하는 가장 단순한 방법은 MultipartFile.getByte() 메서드를 이용하는 것이다. 바이트 배열을 구한 뒤에 파일이나 DB등에 저장하면된다.

if(mutipartFile.isEmpty()){

byte[ ] fileData = multipartFile.getBytes();

//byte 배열을 파일/DB/네트워크 등으로 전송

...

}


업로드 한 파일 데이터를 특정 파일로 저장하고 싶다면 MultipartFile.transferTo() 메서드를 사용하는 것이 편리하다.

if(mutipartFile.isEmpty()){

File file = new File(fileName);

multipartFile.transferTo(file);

...

}



[Spring] 요청 URI 매칭


요청 URI 매칭


클라이언트가 요청한 URL과 @RequestMapping 어노테이션의 값을 이용해서 클라이언트의 요청을 어떤 컨트롤러의 어떤 메서드가 처리할 지가 결정된다.

매칭이 어떻게 이루어지는 지 알지 못할 경우 클라이언트의 요청 URL과 매칭되는 메서드를 설정할 수 없기 때문에, 어떻게 매칭이 이루어지는 지 아는 것은 작지만 매우 중요하다.

본 절에서는 매칭과 관련된 기본 내용인 경로와 @RequestMapping 어노테이션 간의 관계 그리고 @PathVariable을 이용한 URI 템플릿에 대해서 살펴보자.


1. 전체 경로와 서블릿 기반 경로 매칭 설정

    <servlet>

        <servlet-name>dispatcher</servlet-name>

        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

        <load-on-startup>1</load-on-startup>

    </servlet>

    

    <servlet-mapping>

        <servlet-name>dispatcher</servlet-name>

        <url-pattern>*.do</url-pattern>

        <url-pattern>/game/*</url-pattern>

    </servlet-mapping>

위 코드는 *.do로 오는 요청과 /game/으로 오는 요청을 dispatcher 서블릿이 처리하도록 설정하고 있다. 이 경우 다음의 두 @RequestMapping은 어떻게 할까?


@RequestMapping("/search/game.do")

public String search(...){

...

}

 /search/game.do 요청은 <url-pattern>이 ".do"인 확장자 기반의 매칭이 되므로 서블릿 경로가 ""가 된다. 따라서, /search/game.do"이므로 요청은 값이 "search/game.do"인 @RequestMapping 어노테이션과 매칭된다.


@RequestMapping("/game/info")

public String info(...){

...

}

info() 메서드는 /game/info요청을 처리하지 않는다. 서블릿 매핑 설정에서 <url-pattern> 의 값을 "/game/*" 으로 설정했기 때문이다.

<url-pattern>의 값으로 디렉터리를 포함한 패턴을 지정하게 되면 서블릿 경로는 "/game"이 되며, 서블릿 경로를 제외한 나머지  경로를 이용해서 @RequestMapping 어노테이션의 값과 매칭 여부를 판단하게 되므로,

실제 비교할 때 사용되는 요청 URI는 "/info"가 된다. @RequestMapping 어노테이션의 값은 "/game/info" 이므로 비교할 요청 URI인 "info"와는 매칭되지 않는다.


코드만 보면 /search/game.do에 대한 요청은 search()메서드가 처리하고 /game/info에 대한 요청은 info() 메서드가 처리한다고 생각하기 쉽다.

하지만 search() 메서드만 /search/game.do에 대한 요청을 처리하고, info() 메서드는 /game/info요청을 처리하지 않는다.


서블릿 경로를 제외한 나머지 경로를 사용하는 이유는 DispatcherServlet이 기본적으로 사용하는 HandlerMapping 구현체(DefalutAnnotationHandlerMapping)와 HandlerAdapter 구현체(AnnotationMethodAdapter)

가 전체 경로를 사용하지 않도록 설정되어 있기 때문이다. 

DispatherServlet은 DispatherServlet이 사용하는 스프링 설정 파일에 HandlerMapping 구현 클래스와 HandlerAdapter 구현 클래스가 빈으로 등록되어 있으면, 기본 구현체를 사용하지 않고 설정 파일에 등록된 구현체를 사용하게 된다.(기본구현체: DefaultAnnotationServlet 과 AnnotationMethodHandlerAdapter)


2. @PathVariable 어노테이션을 이용한 URI 템플릿


RESTful 서비스가 유행하면서 URI를 다음과 같이 REST 방식으로 구성하는 경우가 늘어나고 있다.

- http://somehost/users/madvius

- http://somehost/games

- http://somehost/forun/board1/10

기본에는 http://somehost/users/userinfo?id=madvius와 같이 파라미터를 이용해서 아이디나 이름을 전달받았다면, 이제는 URI에 아이디나 이름 등이 포함되도록 URL을 구성하고 있다.

URI 템플릿을 이용하면 REST 방식의 URL 매칭을 쉽게 처리할 수 있다. URI 템플릿을 사용하는 방법은 매우 간단하며, 다음과 같이 두 가지만 추가로 작업해주면 된다.


- @RequestMapping: 어노테이션의 값으로(템플릿 변수)

- @PathVariable: 어노테이션을 이용해서{템플릿 변수}와 동일한 이름을 갖는 파라미터를 추가한다.


import org.springframework.web.bind.annotation.PathVariable;

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


@Controller

public class CharacterInfoController {

@RequestMapping("/game/users/{userId}/chracters/{chracterId}")

public String characterInfo(@PathVariable String userId/*madvirus*/, @PathVariable int characterId /*1*/, ModelMap model) {

model.addAttribute("userId", userId);

model.addAttribute("characterId", characterId);

return "game/character/info";

}

}

위 코드를 보면 @RequestMapping 어노테이션은 이름이 uesrId와 characterId인 변수를 포함하고 있다. 이들 변수는 @PathVariable 어노테이션이 적용된 동일한 이름을 갖는 파라미터에 매칭된다. 따라서 요청 URI가 "/game/users/madivirus/character/1"이면, characterInfo() 메서드의 userId 파라미터와 chracterId 파라미터의 값은 각각 "madvirus"와 1이 된다.


만약 파라미터 이름과 URI 템플릿의 변수 이름이 동일하지 않다면, 다음과 같이 @PathVariable에 매칭될 템플릿 변수 이름을 지정해주면 된다.

@RequestMapping("/game/users/{userId}")

public String userInfo(@PathVariable("userId") String id, ModelMap model){ //userId != id

...

}


3. @RequestMapping 어노테이션의 추가 설정 방법

@RequestMapping 어노테이션을 클래스와 메서드에 함께 적용할 경우, 메서드에 적용한 @RequestMapping 어노테이션의 값은 클래스에 적용한 @RequestMapping 어노테이션의 값은 클래스에 적용한 @RequestMapping 어노테이션의 값을 기본 경로로 사용하게 된다.


@Controller

@RequestMapping("/game/users/{userId}")

public class CharacterInfoController {


@RequestMapping("/characters/{characterId}")

public String characterInfo(@PathVariable String userId,

@PathVariable int characterId, ModelMap model) {

model.addAttribute("userId", userId);

model.addAttribute("characterId", characterId);

return "game/character/info";

}

}

위 코드에서 characterInfo() 메서드에 적용된 @RequestMapping 어노테이션의 값은 "/chracters/{characterId}"인데, 실제로 매칭되는 값은 클래스에 적용된 @RequestMapping 어노테이션의 값을 포함한

"/game/users/{userId}/chracters/{chracterId}"가 된다.


cf.) @RequestMapping 어노테이션은 Ant 스타일의 패턴을 지원한다. 따라서, "*"나 "**"를 값으로 사용할 수 있다.

- ?: 1개의 문자와 매칭

- *: 0개 이상의 문자와 매칭

- **: 0개 이상의 디렉터리와 매칭



[Servlet] 서블릿 이벤트


서블릿 이벤트

서블릿은 다양한 시점에 발생되는 이벤트와 이벤트를 처리하기 위한 인터페이스를 정의하고 있다. 이들 이벤트와 인터페이스를 이용하면 웹 어플리케이션에서 필요로 하는 데이터의 초기화나 요청 처리 등을 추적할 수 있게 된다. 서블릿 규약은 다양한 이벤트를 처리할 수 있는 인터페이스를 정의하고 있는데, 이 장에서는 그 중에서 ServletContextListener이다. 데이터 저장 영역(application, session, request)에 데이터가 들어가고 나가는 혹은 그 객체가 생성되고 소멸되는 일련의 작업들에 대해 컨트롤한다.


1. 이벤트가 사용되는 경우

1) 컨텍스트가 초기화되는 경우

2) 세션이 생기거나 소멸되는 경우

3) 속성이 바뀌는 경우

--> 이런 이벤트에 대해서 미리 web.xml 파일에 등록해두면 웹 서버는 자동으로 이벤트를 감지하고 우리가 지정한 클래스 내의 메서드를 실행해준다. 이벤트는 외부의 변화보다는 내부의 변화를 처리하는 경우가 많다. 서블릿에서 이벤트 클래스를 실행되게 하려면 무엇보다 web.xml 파일의 설정이 중요하다. 초기에 웹 서버가 구동하면서 해당 이벤트를 대기 상태로 두기 때문이다. 


2. ServletContextListener를 이용한 이벤트 처리

웹 컨테이너는 웹 어플리케이션(컨텍스트)이 시작되거나 종료되는 시점에 특정 클래스의 메서드를 실행할 수 있는 기능을 제공하고 있다.

이 기능을 사용하면 웹 어플리케이션을 실행하는 데 필요한 초기화 작업이나 웹 애플리케이션이 종료된 후 사용된 자원을 반환하는 등의 작업을 수행할 수 있다.

웹 어플리케이션이 시작되고 종료될 때 특정한 기능을 실행하려면 다음과 같이 코드를 작성해야 한다.

1 javax.servlet.ServletContextListener 인터페이스를 구현한 클래스를 작성한다.

2 web.xml 파일에 1에서 작성한 클래스를 등록한다.


javax.servlet.ServletContextListener 인터페이스는 웹 어플리케이션이 시작되거나 종료될 때 호출되는 메서드를 정의한 인터페이스로서, 다음과 같은 두 개의 메서드가 정의되어 있다.

public void contextInitialized(SerlvetContextEvent sce) 웹 애플리케이션이 초기화될 때 호출된다.

public void contextDestoryed(ServletContextEvent sce) 웹 애플리케이션이 종료될 때 호출된다.


ServletContextListener 인터페이스를 구현한 클래스가 웹 애플리케이션이 시작되거나 종료될 때 실행되도록 하려면 web.xml <listener> 태그와 </listener-class> 태그를 사용해서 완전한 클래스 이름을 명시해주면 된다.
<web-app>
<listener>
<listener-class>mvcjsp.jdbc.loader.DBCPInitiListener</listener-class>
</listener>
<listener>
<!-- 웹 애플리케이션의 시작/종료 이벤트를 처리할 리스너 클래스의 완전한 이름을 값으로 갖는다. -->
<listener-class>mvcjsp.jdbc.chap24.CodeInitListener</listener-class>
</listener>
</web-app>

ServletContextListener 인터페이스에 정의된 두 메서드는 모두 파라미터로 javax.servlet.ServletContextEvent 타입의 객체를 전달 받는데, ServletContextEvent 클래스는 시작되거나 종료된 웹 애플리케이션 컨텍스트를 구할 수 있는 getServletContext() 메서드를 제공하고 있다.
public ServletContext getServletContext()

<web-app>
<context-param>
<param-name>jdbcdriver</param-name>
<param-value>com.mysql.jdbc.Driver</param-value>
</context-param>
</web-app>

cf.) getServletContext() 메서드가 리턴하는 ServletContext 객체는 JSP의 application 기본 객체와 동일한 객체로서, ServletContext 객체를 이용하면 web.xml 파일에 설정된 어플리케이션 초기화 파라미터를 
구할 수 있다. 어플리케이션 초기화 파라미터는 <context-param> 태그를 사용해서 설정한다.

(1) ServletContext가 제공하는 초기화 파라미터 관련 메서드
1) String getInitParameter(String name)
지정한 이름을 갖는 초기화 파라미터의 값을 리턴한다. 존재하지 않을 경우 null을 리턴한다. name 파라미터에는 <param-name> 태그로 지정한 이름을 입력한다.

2) java.util.Enumeration.getInitParameterNames()
web.xml 파일에 정의된 초기화 파라미터의 이름 목록을 Enumeration 타입으로 리턴한다.
--> 초기화 파라미터는 주로 웹 어플리케이션을 구동하는 데 필요한 초기화 작업을 수행하는데 필요한 값을 설저하는 데 사용한다.

※ 리스너의 실행 순서
<listener>
<listener-class>mvcjsp.jdbc.loader.AListener</listener-class>
</listener>
<listener>
<listener-class>mvcjsp.jdbc.loader.BListener</listener-class>
</listener>
web.xml에 한 개이상의 리스너가 등록되어 있는 경우, contextInitialized() 메서드는 등록된 순서대로 실행되고 contextDestoryed() 메서드는 등록된 반대 순서대로 실행된다. 즉, 위 코드의 경우 웹 애플리케이션이 시작될 때는 AListener가 먼저 실행되고 그 다음에 BListenter가 실행된다. 반대로 웹 어플리케이션이 종료될 때는 BListener가 실행되고 그 다음에 AListener가 실행된다.

3. web.xml에서 사용되는 태그

(1) <listener> 태그는 서블릿의 이벤트를 대기 상태로 둔다.

cp.) <listener> 태그를 가지고 설정할 수 있는 이벤트는 다음과 같다.

- ServletContextListener 이벤트 

ServletContext 객체가 초기화되거나 소멸될 때 발생하는 이벤트이다. contextInitialized(),contextDestoryed() 메서드가 있다.


- ServletContextattributeListener 이벤트

ServletContext 객체에 속성이 추가되거나 삭제되거나, 수정될 때 발생하는 이벤트이다. attributeAdded(), attributeRemoved(), attributereplaced() 메서드가 있다.


- HttpSessionListener 이벤트

HttpSession 객체가 생성되거나 소멸될 때 발생하는 이벤트이다. sessionCreated(), sessionDestroyed() 메서드가 있다.


- HttpSessionAttributeListener 이벤트

ServletRequest 객체가 초기화되거나 소멸될 때 발생하는 이벤트이다. requestInitialzed(), requestDestroyed() 메서드가 있다.


- ServletRequestAttributedListener 이벤트

ServletRequest 객체에 속성이 추가되거나 삭제되거나, 수정될 때 발생하는 이벤트이다. attributeAdded(), attributeRemoved(), attributeReplaced() 메서드가 있다.


(2) <context-param> 태그는 초기화 매개 변수가 된다.


(3) 예제 1

1) web.xml

<display-name>Round18</display-name>

<context-param>

<param-name>co_name</param-name>

<param-value>승현 주식회사</param-value>

</context-param>

<context-param>

<param-name>co_tel</param-name>

<param-value>02-1234-1234</param-value>

</context-param>

<context-param>

<param-name>admin_email</param-name>

<param-value>kimsh@sh.com</param-value>

</context-param>


2) 자바코드

import java.io.*;

import javax.servlet.*;

import javax.servlet.http.*;


public class Round18_03_Servlet extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response)

throws ServletException, IOException {

ServletContext context = this.getServletContext();

String co_name = context.getInitParameter("co_name");

String co_tel = context.getInitParameter("co_tel");

String admin_email = context.getInitParameter("admin_email");

response.setContentType("text/html;charset=euc-kr");

PrintWriter out = response.getWriter();

out.println("<html><head>");

out.println("<style type='text/css'>");

out.println(".n_bo { border:none }");

out.println("</style>");

out.println("</head><body><center>");

out.println("회사상호 : ");

out.println("<input type='text' class='n_bo' value='" + co_name + "'/><br/>");

out.println("회사전번 : ");

out.println("<input type='text' class='n_bo' value='" + co_tel + "'/><br/>");

out.println("대표메일 : ");

out.println("<input type='text' class='n_bo' value='" + admin_email + "'/><br/>");

out.println("</center></body></html>");

}

}

화면 출력)

회사상호: 승현 주식회사

회사전번: 02-1234-1234

대표메일: kimsh@sh.com


이렇게 ServletContext 객체를 가지고 web.xml 파일에 등록된 초기화 매개변수들을 가져와서 사용할 수 있다. 그러나 ServletContext 객체와 관련된 이벤트에서는 이들 값의 추가, 삭제, 수정에 대한 정보를 확인할 수 없다. 이벤트가 관리하는 값들은 오직 해당 객체의 속성들뿐이며, 매개변수는 관리 대상에서 제외된다.


(4) 예제2

이제 ServletContext 객체와 관련된 이벤트를 처리해 보도록 하자. ServletContext 객체의 변화에 대해 반응하는 메서드들은 ServletContextListener와 ServletContextAttributeListener 인터페이스에 정의되어 있기 때문에 이를 구현해 주어야 한다.

1) web.xml : 태그 순서 주의!

<listener>

<display-name>Context Listener</display-name>

<listener-class>Round18_04_Servlet_Listener</listener-class>

</listener>

<context-param>

<param-name>co_name</param-name>

<param-value>승현 주식회사</param-value>

</context-param>


2) 자바코드

import javax.servlet.*;


public class Round18_04_Servlet_Listener implements ServletContextListener, ServletContextAttributeListener {


public void contextInitialized(ServletContextEvent e) {

  // 톰캣이 구동될 때 실행된다.

System.out.println("ServletContext 가 초기화 되었습니다.");

System.out.println("init context = " + e.getServletContext());

}

public void contextDestroyed(ServletContextEvent e) {

  // 톰캣이 종료될 때 실행된다.

System.out.println("ServletContext 가 소멸 되었습니다.");

System.out.println("dest context = " + e.getServletContext());

}

public void attributeAdded(ServletContextAttributeEvent e) {

        // ServletContext 객체에 속성이 새로 추가될 때 실행된다.

System.out.println("Context 영역에 값이 추가 되었습니다.");

System.out.println("added = " + e.getName() + " : " + e.getValue());

}

public void attributeRemoved(ServletContextAttributeEvent e) {

        // ServletContext 객체의 속성이 삭제될 때 실행된다.

System.out.println("Context 영역에 값이 삭제 되었습니다.");

System.out.println("removed = " + e.getName() + " : " + e.getValue());

}

public void attributeReplaced(ServletContextAttributeEvent e) {

        // ServletContext 객체의 속성이 수정될 때 수정 직전에 실행된다.

System.out.println("Context 영역에 값이 변경 되었습니다.");

System.out.println("replaced = " + e.getName() + " : " + e.getValue());

}

}


(5) 예제 3

import java.io.*;

import javax.servlet.*;

import javax.servlet.http.*;


public class Round18_04_Servlet extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

response.setContentType("text/html;charset=euc-kr");

PrintWriter out = response.getWriter();

out.println("<html><body><center>");

out.println("<form method='post'>");

out.println("<input type='submit' value='Context 값 할당 하기'/>");

out.println("</form></center></body></html>");

}

public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

ServletContext context = this.getServletContext();

context.setAttribute("my_name", "김승현");

context.setAttribute("my_name", "승현");

context.removeAttribute("my_name");

response.setContentType("text/html;charset=euc-kr");

PrintWriter out = response.getWriter();

out.println("<html><body><center>");

out.println("Context 값 추가, 삭제, 변경 성공!");

out.println("</center></body></html>");

}

}

ServletContext 객체의 속성을 바꾸는 서블릿 코드이다.

웹서버가 구동시점에는 최초로 contextInitialzed() 메서드가 실행되어 ServletContext 객체가 초기화된다.

URL 패턴에 맞추어 서블릿을 실행하면  페이지가 열리고 아무런 이벤트도 발생하지 않는다. 여기에서 Context 값 할당 하기 단추를 누르면 doPost() 메서드가 실행되면서 ServletContextAttributeListener 이벤트가 발생하여 차례로 추가, 삭제, 수정 메시지를 확인할 수 있다.

이상과 같이 이벤트를 web.xml 파일에 등록하면 웹에서 특정 동작에 이벤트가 발생해서 해당 메서드를 실행하게 된다.


'Programing > JSP/Servlet' 카테고리의 다른 글

[JSP] HTTP 헤더  (0) 2014.12.25
[JSP] 커넥션 풀  (2) 2014.12.19
[Servlet] 서블릿 필터  (0) 2014.12.10
[Servlet] 서블릿 기초  (0) 2014.12.10
[Servlet] 데이터 저장 영역  (0) 2014.12.10

[Spring] 모델 생성하기


모델 생성하기


@RequestMapping 어노테이션이 적용된 메서드의 파라미터의 리턴 타입으로 ModelAndView, Model, ModelMap, Map, 커맨드 객체 등을 이용해서 모델을 뷰에 전달하게 된다.

본 절에서는 컨트롤러가 이들 클래스를 이용해서 모델을 어떻게 뷰에 전달하는 지 살펴보자.


1. 뷰에 전달되는 모델 데이터

@RequestMapping 메서드가 ModelAndView, Model, Map을 리턴하는 경우 이들에 담긴 모델 데이터가 뷰에 전달된다. 또한, 추가로 다음의 항목도 뷰에 함께 모델로 전달된다.

- 커맨드 객체

- @ModelAttribute 어노테이션이 적용된 메서드가 리턴한 객체

- 메서드의 Map, Model, ModelMap 타입의 파라미터를 통해 설정된 모델


import org.springframework.stereotype.Controller;

import org.springframework.ui,ModelMap;

import org.springframework.web.bind.annotation.ModelAttribute;

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

import org.springframework.web.servlet.ModelAndView;


@Controller

public class GameSearchController {

private SearchService searchService;

@ModelAttribute("searchTypeList")

  // 이 코드에서 @ModelAttribute 어노테이션이 적용된 referenceSearchTypeList() 메서드가 리턴한 모델 객체는 "searchTypeList"라는 이름으로 뷰에 전달된다.

public List<SearchType> referenceSearchTypeList() {

List<SearchType> options = new ArrayList<SearchType>();

options.add(new SearchType(1, "전체"));

options.add(new SearchType(2, "아이템"));

options.add(new SearchType(3, "캐릭터"));

return options;

}

@RequestMapping("/search/game.do")

public ModelAndView search(@ModelAttribute("command") SearchCommand command, ModelMap model) {

String[ ] queryList = getPopularQueryList();

model.addAttribute("popularQueryList", queryList);

ModelAndView mav = new ModelAndView("search/game");

SearchResult result = searchService.search(command);

mav.addObject("searchResult", result);

return mav;

}

  //@RequestMapping 어노테이션이 적용된 search() 메서드는 파라미터로 커맨드 객체와 ModelMap 객체를 전달받는다. search() 메서드 내부에서는 ModelMap 타입 파라미터인

// model을 이용해서 "popularQueryList"이름의 모델 객체를 뷰에 전달한다. 또한, 내부적으로 ModelAndView 타입의 mav 객체를 만든 뒤 mav를 이용해서 "searchResult"라는 이름의

// 모델 객체를 뷰에 전달한다.

}


이들 모델 객체는 서로 다른 방법으로 생성되었지만, DIspatcherServlet은 이들 모델 객체에 저장된 모델 데이터를 모두 뷰에 전달한다.

따라서 뷰 코드에서는 다음과 같이 이들 모델 객체를 모두 사용할 수 있게 된다.

<body>

인기 키워드: <c:forEach var="popularQuery" items="${popularQueryList}">${popularQuery} </c:forEach>

<form action="game.do">

<select name="type">

<c:forEach var="searchType" items="${searchTypeList}">

<option value="${searchType.code}" <c:if test="${command.type == searchType.code}">selected</c:if>>

${searchType.text}</option>

</c:forEach>

</select>

<input type="text" name="query" value="${command.query}"/>

<input type="submit" value="검색" />

</form>

검색 결과: ${searchResult}

</body>


2. Map, Model, ModelMap을 통한 모델 설정

Map, Model, ModelMap을 이용하면 뷰에 전달할 모델을 생성할 수 있다.


(1) 첫 번째 방법은 이들 세 가지 타입 중 한 가지를 파라미터로 전달받을 것이다.

@RequsetMapping("search1.do")

public String search1(Map model){

model.put("result", searchResult);

}


@RequsetMapping("search2.do")

public String search2(Model model){

model.addAttribute("result", searchResult);

}


@RequsetMapping("search3.do"){

public String search3(ModelMap model){

model.addAttribute("result", searchResult);

}


(2) 두 번째 방법은 Map과 Model을 리턴하는 것이다. 이 둘은 모두 인터페이스이기 때문에 실제로는 인터페이스를 구현한 클래스의 객체를 생성해서 리턴하면 된다. 

@RequestMapping("/search1.do")

public Map<String, Object> search1(){

...

HashMap<String, Object> model = new HashMap<String, Object>();

model.put("result", searchResult);

return model;

}


@RequestMapping("/search2.do")

public Model search2(){

...

Model model = new ExtendedModelMap();

model.addAttribute("result", searchResult);

return model;

}

스프링은 org.springframework.ui.Model 인터페이스의 구현 클래스인 ExtendedModelMap 클래스를 동일한 패키지에 제공하고 있으므로, ExtendedModelMap 클래스를 이용해서 모델을 설정하면 된다.

Map의 경우 많이 사용하는 java.util.HashMap 클래스를 이용해서 모델을 설정한다.


(3) Model 인터페이스의 주요 메서드

org.springframework.ui.Model 인터페이스는 모델을 설정할 수 있도록 다음과 같은 메서드를 제공하고 있다.


1 Model addAttribute(String name, Object value)

value 객체를 name 이름으로 추가한다. 뷰 코드에서는 name으로 지정한 이름을 통해서 value를 사용한다.


2 Model addAttribute(Object value)

value를 추가한다. value의 패키지 이름을 제외한 단순 클래스 이름을 모델 이름으로 사용한다. 이 때 첫 글자는 소문자로 처리한다.

value가 배열이거나 컬렉션인 경우 첫 번째 원소의 클래스 이름 뒤에 "List"를 붙인 걸 모델 이름으로 사용한다.

이 경우에도 클래스 이름의 첫자는 소문자로 처리한다.


3 Model addAllAttribute(Collection<?> values)

addAttribute(Object value) 메서드를 이용해서 컬렉션에 포함된 객체들을 차례대로 추가한다.


4 Model AddAllAttributes(Map<String, ?> attributes)

Map에 초함된 <키, 값>에 대해 키를 모델 이름으로 사용해서 값을 모델로 추가한다.


5 Model mergeAttributes(Map<String, ?> attributes)

Map에 포함된 <키, 값>을 현재 모델에 추가한다. 단, 키와 동일한 이름을 갖는 모델 객체가 존재하지 않는 경우에만 추가된다.


6 boolean containsAttributes(String name)

지정한 이름의 모델 객체를 포함하고 있는 경우 true를 리턴한다.


다음 코드는 컨트롤러 요청 처리 메서드에서 Model 타입의 파라미터를 이용해서 모델을 설정하는 예를 보여주고 있다.

@RequestMapping("/search/game.do")

public String search(SearchCommand command, Model model){

...

model.addAttrubute("searchResult", result);

model.addAttribute("searchTypeList", searchTypeList);

...

}


컨트롤러 요청 처리 메서드에서 Model 타입을 리턴하고 싶다면 Model 인터페이스의 구현 클래스인 org.springframework.ui.ExtendedModelMap 클래스를 사용하면 된다.

@RequestMapping("search/gmae.do")

public Model search(SearchCommand command){

...

Model model = new ExtendedModelMap();

model.addAttribute("searchResult", result);

model.addAttribute("searchTypeList", searchTypeList);

return model;

}


(4) ModelMap 클래스
org.springframework.ui.ModelMap 클래스는 Map의 구현 클래스로서 제공하는, 컨트롤러 메서드의 파라미터를 통해서 전달받는다.

@RequestMapping("search/game.do")
public String search(SearchCommand command, ModelMap model){
...
model.addAttribute("result", searchResult);
...
}
ModelMap 클래스가 제공하는 모델 설정 관련 메서드는 Model 인터페이스와 동일하므로 추가 설명은 하지 않겠다.

3. ModelAndView를 통한 모델 설정

org.springframework.web.servlet.ModelAndView 클래스는 컨트롤러의 처리 결과를 보여 줄 뷰에 전달할 값을 저장하는 용도로 사용되는데, 본 절에서는 ModelAndView 객체를 생성하는 방법을 살펴보자.


(1) modelAndView 객체를 생성하는 방법은 setViewName(String viewName) 메서드를 이용하여 뷰 이름을 설정하고, addObject(String name, Object value) 메서드를 이용하여 뷰에 전달할 값을 추가한다.

@RequestmMapping("/search/game.do")

public ModelAndView search(SearchCommand command) {

ModelAndView mav = new ModelAndView();

mav.setViewName("search/game");

mav.addObject("greeting", getGreeting());

return mav;


(2) Map에 저장된 <키, 값> 쌍 전체를 뷰에 전달할 값으로 ModelAndView 객체에 추가하고 싶다면 다음과 같이 addAllObjects(Map modeMap) 메서드를 사용하면 된다.

Map referenceMap = referenceData();

mav.addAllObjects(referenceData);


(3) 생성자를 사용해서 뷰 이름과 Map을 전달할 수도 있다.

Map referenceMap = referenceData();

return new ModelAndView("search/game", referenceData);


(4) 뷰에 전달할 객체가 한 개 뿐이라면 다음과 같은 생성자를 이용해서 코드 분량을 줄일 수 있을 것이다.

return new ModelAndView("search.game", "result", referenceData");


4. @ModelAttribute 어노테이션을 이용한 모델 데이터 처리


@ModelAttribute 어노테이션을 이용하면 다음의 두 가지 작업을 수행할 수 있다.

1 @RequestMapping 어노테이션이 적용되지 않은 별도 메서드로 모델에 추가될 객체를 생성

2 커맨드 객체의 초기화 작업을 생성


cp.) @ModelAttribute의 속성은 아래와 같다.


(1) 참조 데이터 생성

웹 어플리케이션을 구현하다 보면 동일한 모델 데이터를 두 개 이상의 요청 처리 결과 화면에서 보여주어야 할 때가 있다.

예를 들어, 검색 메인 화면과 검색 결과 화면에서 검색 타입과 인기 검색어를 보여줄 수 있을 것이다. 

이 경우 다음과 같이 이들 공통 모델 데이터를 설정해주는 메서드를 구현한 뒤 요청 처리 메서드에서 호출하도록 구현 할 수 있을 것이다.

private void referenceData(Model model){

...

model.addAttribute("searchTypeList", searchTypeList);

model.addAttribute("popularQueryList", queryList);

}


@RequestMapping("/search/main.do")

public String main(Model model){

referenceData(model);

return "search/main";


@RequestMapping("search/game.do")

public ModelAndView search(

@ModelAttribute("command") SeachCommand command, Model model){

referenceData(model);

ModelAndView mav = new ModelAndView("search/game");

SearchResult result = searchService.search(Command);

mav.addObject("searchResult", result);

return mav;

}


하지만, 위와 같이 구현하려면 다음과 같은 단점이 발생한다.

1 Model이나 ModelMap과 같이 모델 정보를 설정할 때 사용할 타입이 동일해야 한다.

2 각 요청 처리 메서드에 공통으로 사용되는 모델을 설정하기 위한 코드가 중복된다.


@ModelAttribute 어노테이션을 사용하면 이런 단점 없이 두 개 이상의 요청 처리 메서드에서 공통으로 사용되는 모델을 설정할 수 있다.

@ModelAttribute 어노테이션을 메서드에 적용하면 해당 메서드가 생성한 객체가 뷰에 전달된다.

따라서, @ModelAttribute 어노테이션을 사용하면 두 개 이상의 요청 처리 메서드에서 공통으로 사용되는 모델 객체를 생성할 수 있다.

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.ModelAttribute;

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

import org.springframework.web.servlet.ModelAndView;


@Controller

public class GameSearchController {

@Autowired

private SearchService searchService;


@ModelAttribute("searchTypeList")

public List<SearchType> referenceSearchTypeList() {

List<SearchType> options = new ArrayList<SearchType>();

options.add(new SearchType(1, "전체"));

options.add(new SearchType(2, "아이템"));

options.add(new SearchType(3, "캐릭터"));

return options;

}


@ModelAttribute("popularQueryList")

public String[] getPopularQueryList() {

return new String[] { "게임", "창천2", "위메이드" };

}


@RequestMapping("/search/main.do")

public String main() {

return "search/main";

}


@RequestMapping("/search/game.do")

public ModelAndView search(@ModelAttribute("command") SearchCommand command) {

ModelAndView mav = new ModelAndView("search/game");

System.out.println("검색어 = " + command.getQuery().toUpperCase());

SearchResult result = searchService.search(command);

mav.addObject("searchResult", result);

return mav;

}

...

}

위 코드에서 main() 메서드와 search() 메서드는 둘 다 referenceSearchTypeList() 메서드와 getPopularQueryList() 메서드를 호출하고 있지 않다.

하지만 reference SearchTypeList() 메서드와 getPopularQueryList() 메서드에는 @ModelAttribute 어노테이션이 적용되어 있으므로 main() 메서드와 search() 메서드가 실행될 때 refereceSearchTypeList() 메서드와 getPopularQueryList() 메서드가 리턴한 객체가 모델로 뷰에 함께 전달된다.

이때 @ModelAttribute 어노테이션의 값이 뷰에 전달되는 객체의 이름으로 사용되므로, 뷰 코드에서는 다음과 같이 @ModelAttribute 어노테이션의 값을 이용해서 모델 객체에 접근할 수 있게 된다.


<body>

인기 키워드: <c:forEach var="popularQuery" items="${popularQueryList}">${popularQuery} </c:forEach>

<form action="game.do">

<select name="type">

<c:forEach var="searchType" items="${searchTypeList}">

<option value="${searchType.code}" <c:if test="${command.type == searchType.code}">selected</c:if>>

${searchType.text}</option>

</c:forEach>

</select>

<input type="text" name="query" value="${command.query}"/>

<input type="submit" value="검색" />

</form>


(2) 커맨트 객체 초기화

@ModelAttribute 어노테이션을 사용하면 커맨드 객체의 초기화 작업을 수행할 수도 있다.

예를 들어, GET 요청시에는 폼의 특정 값을 미리 초기화하고 POST 요청시에는 폼에 입력한 값을 커맨트 객체로 받아야 한다고 해보자.

이런 경우 @ModelAttribute 어노테이션을 사용하면 다음과 같이 GET 요청과 POST 요청에 대해 알맞게 커맨트 객체를 초기화할 수 있다.

아래 코드는 @ModelAttribute 어노테이션의 사용 예를 보여주고 있다.


@Controller

@RequestMapping("/account/create.do")

public class CreateAccountController {


@ModelAttribute("command")

public MemberInfo formBacking(HttpServletRequest request) {

if (request.getMethod().equalsIgnoreCase("GET")) {

MemberInfo mi = new MemberInfo();

Address address = new Address();

address.setZipcode(autoDetectZipcode(request.getRemoteAddr()));

mi.setAddress(address);

return mi;

} else {

return new MemberInfo();

}

}


@RequestMapping(method = RequestMethod.GET)

public String form() {

return "account/creationForm";

}


@RequestMapping(method = RequestMethod.POST)

public String submit(@ModelAttribute("command") MemberInfo memberInfo)

return "account/created";

}

}

위 코드에서 주목할 점은 formBacking() 메서드의 @ModelAttribute 어노테이션값과 submit() 메서드의 파라미터에서 사용된 @ModelAttribute 어노테이션의 값이 "command"로 동일하다는 것이다. submit() 메서드는 POST 요청을 처리하도록 되어 있는데, POST 요청이 전송될 경우 formBacking() 메서드가 생성한 MemberInfo 객체는 "command"라는 이름의 모델 객체로 저장되며, 이렇게 생성된"command" MemberInfo 객체는 submit() 메서드의 동일한 모델 이름을 갖는 커맨드 객체(첫 번째 파라미터)로 전달된다.


@ModelAttribute 어노테이션이 적용된 메서드가 @RequestMapping 어노테이션이 적용된 메서드 보다 먼저 호출되기 때문에, 

커맨드 객체에 대한 초기화 작업이 필요하다면 커맨드 객체와 동일한 이름을 갖는 @ModelAttribute 어노테이션이 적용된 메서드를 이용하면 된다.


(3) @ModelAttribute 어노테이션 적용 메서드에 전달 가능한 파라미터 타입
@ModelAttribute 어노테이션이 적용된 메서드는 @RequestMapping 어노테이션이 적용된 메서드와 동일한 타입의 파라미터를 가질 수 있다. 
앞서 코드에서는 HTTP 메서드를 확인하기 위해 HttpServletRequest를 파라미터로 전달받았다.

@ModelAttribute("command")
public MemberInfo formBacking(HttpServletRequest request) {
if (request.getMethod().equalsIgnoreCase("GET")) {
...
} else {
return new MemberInfo();
}
}
@ModelAttribute 어노테이션이 적용 메서드는 HttpServletRequest 뿐만 아니라 필요에 따라 Locale, @RequestParam 어노테이션 적용 파라미터, @PathVariable 어노테이션 적용 파라미터 등을 이용해서 모델 객체를 생성하는 데 필요한 정보를 구할 수 있다.


'Programing > Spring' 카테고리의 다른 글

[Spring] 파일업로드 처리  (2) 2014.12.18
[Spring] 요청 URI 매칭  (0) 2014.12.18
[Spring] 뷰 지정  (0) 2014.12.18
[Spring] 컨트롤러 메서드의 파라미터 타입  (2) 2014.12.18
[Spring] HTML 폼과 자바빈 객체  (0) 2014.12.18

[Spring] 뷰 지정


뷰 지정

컨트롤러 처리 메서드는 처리 결과를 보여줄 뷰 이름이나 View 객체를 리턴하고, DispatcherServlet은 뷰 이름이나 View 객체를 이용해서 뷰를 생성하게 된다. 뷰 이름은 직접 또는 자동으로 설정할 수 있다.


1. 뷰 이름 명시적 지정: ModelAndView와 String 리턴 타입

뷰 이름을 명시적으로 리턴하려면 ModelAndView나 String을 리턴해야 한다.


(1) ModelAndView 클래스의 생성자

@RequestMapping("/hello.do")

public ModelAndView index(){

ModelAndView mav = new ModelAndView("hello");

...

return mav;

}


(2) setViewName() 메서드

@RequestMapping("/hello.do")

public ModelAndView index(){

ModelAndView mav = new ModelAndView();

mav.setViewName("hello");

...

return mav;

}



(3)  리턴 타입이 String인 경우

@RequestMapping("/help/main.do")

public String helpMain(ModelMap model){

...

return "help/main";

}

리턴 타입이 ModelAndView가 아니고 String 타입인 경우


2. 뷰 이름 자동 지정

다음의 경우 RequestToViewNameTranslator를 이용해서 URL로부터 뷰 이름을 결정한다.

- 리턴 타입이 Model이나 Map인 경우

- 리턴 타입이 void 이면서 ServletResponse나 HttpServletResponse 타입의 파라미터가 없는 경우


스프링 설정 파일에 RequestToViewNameTranslator 빈이 존재하지 않을 경우 기본적으로 DefalutRequestToViewNameTranslator를 사용한다. 이 클래스는 요청 URI로부터 맨 앞의 슬래시와 확장자를 제외한 나머지 부분을 뷰 이름으로 사용한다. ( 더 정확하게는 전체 경로를 사용하지 않도록 설정한 경우 서블릿 경로를 제외한 나머지 경로가 사용된다.)

@RequestMapping("/search/game2.do")

public Map<String, Object> search(){

HashMap<String, Object> model = new HashMap<String, String>();

...

return model;

}

위 코드는 뷰에 전달된 모델 데이터를 갖고 있는 Map을 리턴하고 있다. 이 경우 RequestToViewNameTranslator를 이용하여 결과를 보여줄 뷰 이름을 결정하게 되는데,

DefaultRequestToViewNameTranslator가 사용될 경우 다음과 같이 URL로부터 뷰 이름이 결정된다.

/search/game2.do --> search/game2


3. 리다이렉트 뷰

뷰 이름에 "redirect:" 접두어를 붙이면, 지정한 페이지로 리다리렉트 된다. 리다이렉트 URL은 다음과 같이 두 가지 방식으로 입력할 수 있다.

- redirect:/bbs/list: 현재 서블릿 컨텍스트에 대한 상대적인 경로로 리다이렉트

- redirect:http://host/bbs/list: 지정한 절대 URL로 리다이렉트


ModelAndView mav = new ModelAndView();

mav.setViewName("redirect:error.do");

return mav;



[Spring] 컨트롤러 메서드의 파라미터 타입


컨트롤러 메서드의 파라미터 타입


컨트롤러 @RequestMapping 어노테이션이 적용된 메서드는 커맨드 클래스뿐만 아니라 HttpServletRequest, HttpSession, Locale 등 웹 어플리케이션과 관련된 다양한 타입의 파라미터를 가질 수 있는데, 전달 가능한 파라미터 타입은 다음과 같다.



1. @RequestParam 어노테이션을 이용한 파라미터 매핑

컨트롤러를 구현하면서 가장 많이 사용되는 어노테이션이 바로 @RequestParam 어노테이션이다. @RequestParam 어노테이션은 HTTP 요청 파라미터를 메서드의 파라미터로 전달받을 때 사용된다. @RequestParam 어노테이션과 HTTP 요청 파라미터의 관계는 다음과 같다.


첫번째 파라미터인 userName은 name 요청 파라미터의 값을 전달 받으며, 두 번째 파라미터인 userEmail 파라미터는 email 파라미터 값을 전달 받는다.

바로, JSP에서 String name = request.getParameter("name"); 과 같다.


@RequestParam은 Controller 메소드의 파라미터와 웹요청 파라미터와 맵핑하기 위한 어노테이션이다. 관련 속성은 아래와 같다.

@Controller

public class HelloController {

 

    @RequestMapping("/hello.do")

   

    public String hello(@RequestParam("name") String name,

    // required 조건이 없으면 기본값은 true, 즉 필수 파라미터이다. 파라미터 pageNo가 존재하지 않으면 Exception 발생.       

     @RequestParam(value="pageNo", required=false) String pageNo){ 

    // 파라미터 pageNo가 존재하지 않으면 String pageNo는 null.

...

    }

}

@RequestParam 어노테이션이 적용된 파라미터는 기본적으로 필수 파라미터이다. 따라서 @RequestParam 어노테이션에 명시한 HTTP 요청 파라미터가 존재하지 않을 경우

스프링 MVC는 잘못된 요청을 의미하는 400 응답 코드를 웹 브라우저에 전송한다. 예를 들어 @RequestParam 어노테이션의 별도 설정을 하지 않은 경우, 다음의 URL을 요청하면 400 에러가 발생하게 된다.


http://host/chap06/search/internal.do?query=spring&p=3


@Controller

public class SearchController{

@RequestMapping("/search/internal.do")

public ModelAndView seachInternal(

@RequestParam("query") String query, @RequestParam("p") int pageNumber){

...

}

}

첫 번째 파라미터는 query 요청 파라미터의 값을 전달받으며, 두 번째 파라미터인 pageNumber 파라미터는 p 파라미터의 값을 전달받는다.

@RequestParam 어노테이션이 적용된 파라미터가 String이 아닐 경우 실제 타입에 따라서 알맞게 타입 변환을 수행한다. 예를 들어, pageNumber 파라미터의 타입은 int인데, 이 경우 자동으로 문자열을 int 타입으로 변환해준다. 


만약 아래와 같이 int 타입으로 변환할 수 없는 값인 "a"를 pageNumber 파라미터에 매핑되는 HTTP 요청 파라미터로 전달하면, 스프링 MVC는 잘못된 요청(Bad Request)을 의미하는 400 응답 코드를 웹 브라우저에 전송한다.

http://localhost:8080/chap06/search/internal.do?query=spring&p=a

@RequestParam 어노테이션이 적용된 파라미터는 기본적으로 필수 파라미터이다. 따라서 @RequestParam 어노테이션에 명시한 HTTP 요청 파라미터가 존재하지 않을 경우 스프링 MVC는 잘못된 요청을 의미하는 400 응답코드를 웹 브라우저에 전송한다. 예를 들어 아래와 같이 @RequestParam 어노테이션의 별도 설정을 하지 않은 경우, 다음의 URL을 요청하면 400 에러가 발생하게 된다.


http://localhost:8080/chap06/search/internal.do?query=spring


필수가 아닌 파라미터인 경우 required 속성 값을 false로 지정해 주면 된다. 아래 코드는 적용 예이다. 참고로 required 속성의 기본 값은 true이다.

import orr.springframework.web.bind.annotation.RequestMapping

...

...

@RequestMapping("/search/external.do")

public ModelAndView searchExternal(

@RequestParam(value="query", required=false) String query, @RequestParam(value ="p", required=false) int pageNumber){

System.out.println("query="+query+",pageNumber="+pageNumber);

...

}

필수가 아닌 요청 파라미터의 값이 존재하지 않을 경우 null 값을 할당한다. 그런데, null을 할당할 수 없는 기본 데이터 타입인 경우에는 타입 변환 에러가 발생하게 된다. 예를 들어 , 위 코드에서 "p" 요청 파라미터를 필수가 아닌 파라미터로 설정했는데, 이 상태에서 "p" 요청 파라미터를 지정하지 않은 경우 null을 기본 데이터 타입으로 변환할 수 없다는 예외가 발생하게 된다.


기본 데이터 타입을 사용할 경우 HTTP 요청 파라미터가 존재하지 않으며 기본 값을 할당하는 경우가 많은데, 이런 경우에는 다음과 같이 defaultValue 속성을 이용해서 기본값을 지정할 수 있다.

@RequestMapping("/search/external.do")

public ModelAndView searchExternal(

@RequestParam(value="query", required=false) String query, @RequestParam(value="p", defaultValue="1") int pageNumber){

System.out.println("query="+query+",pageNumber="+pageNumber);

...

}

defaultValue 속성을 이용해서 기본 값을 지정하면 해당 요청 파라미터를 지정하지 않을 경우 defaultValue 속성에 지정한 문자열을 값으로 이용하게 된다. 예를 들어, 위와 같이 "p" 요청 파라미터의 기본 값을 "1"로 지정했다면,

http://localhost:8080.chap06/search/external.do?query=spring

"p" 파라미터가 존재하지 않으므로 기본 값으로 지정한 "1"을 "p" 요청 파라미터의 값으로 사용하게 되고, 따라서 pageNumber 파라미터의 값은 1이 된다.


2. @RequestHeader 어노테이션을 이용한 헤더 맵핑


@RequestHeader 어노테이션을 이용하면 HTTP 요청 헤더의 값을 메서드의 파라미터로 전달받을 수 있다.

import org.springframework.web.bind.annotation.RequestHeader;

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


@Controller

public class HeaderController {


@RequestMapping("/header/check.do")

public String check(@RequestHeader("Accept-Language") String languageHeader) {

System.out.println(languageHeader);

return "header/pass";

}

}


cf.) 컨트롤러 메서드의 리턴 타입

컨트롤러 메서드는 ModelAndView를 비롯한 몇 가지 리턴 타입을 가질 수 있다.


'Programing > Spring' 카테고리의 다른 글

[Spring] 모델 생성하기  (1) 2014.12.18
[Spring] 뷰 지정  (0) 2014.12.18
[Spring] HTML 폼과 자바빈 객체  (0) 2014.12.18
[Spring] 자바 코드 기반 설정  (0) 2014.12.17
[Spring] 어노테이션 기반 설정  (0) 2014.12.17

[Spring] HTML 폼과 자바빈 객체


HTML 폼과 자바빈 객체


스프링 MVC는 HTML 폼에 입력한 데이터를 자바빈 객체를 이용해서 전달 받을 수 있도록 하고 있다.

예를 들어 HTML 폼의 항목과 이름과  자바빈 클래스의 프로퍼티 이름이 일치할 경우 폼에 입력한 값을 해당 자바빈 클래스의 프로퍼티 값으로 설정해주는 기능을 제공하고 있다.


<from method="post">

<input type="hidden" name="parantId" value="0" />

제목: <input type="text" name="title" />

내용: <textarea name="content"> </textarea>

<input type="submit"/>

</from>


public class NewArticleCommand{

private String title;

private String content;

private int parentId;


// 입력 항목의 이름과 일치하는 프로퍼티에 값이 저장

public void setTitle(String tilte){

this.title;

}

public void setContent(String content){

this.content;

}

public void setParentId(int parentId){

this.parentId = parentId;

}

}


@Controller

@RequestMapping("/article/newArticle.do")

public class NewArticleController{


@RequestMapping(method = RequestMethod.POST)

public String submit(NewArticleCommand command){

//command.getTitle(): title 파라미터 값 저장

//command.getContent(): content 파라미터의 값 저장

//comman.getParentId(): parentId 파라미터의 값 저장

return "article/newArticleSubmitted";

}

}

HTML 폼에 입력한 데이터를 자바빈 객체로 전달받는 방법은 매우 간단하다. 단지 @RequestMapping 어노테이션이 적용된 메서드의 파라미터로 자바빈 타입을 추가해주기만 하면 된다.

폼에 입력한 값을 NewArticleCommand 클래스로 전달 받고 싶다면 요청 처리 메서드에 NewArticleCommand 타입의 파라미터를 추가해주기만 하면 된다.


1. 뷰에서 커맨드 객체 접근하기

뷰 코드에서는 컨트롤러의 @RequestMapping 어노테이션 메서드에서 전달받은 command 객체에 접근할 수 있다. 예를 들어, 아래와 같이 NewArticleCommand 타입의 command 객체를 전달받는다고 하자.

@RequestMapping(method =ResquestMethod.POST)

public String submit(NewArticleCommand command){

}

 

이 경우 컨트롤러의 처리 결과를 보여주는 뷰 코드에서는 command 객체의 클래스 이름을 이용해서 command 객체에 접근할 수 있다. 즉, command 객체는 자동으로 모델에 추가된다.

<body>

제목:${newArticleCommand.title}


뷰에서 사용할 모델의 이름을 변경하고 싶다면 다음과 같이 @ModelAttribute 어노테이션을 이용해서 command 객체의 모델 이름을 지정할 수 있다.

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.ModelAttribute;

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

import org.springframework.web.bind.annotation.RequestMethod;


@Controller

@RequestMapping("/article/newArticle.do")

public class NewArticleController {


@RequestMapping(method = RequestMethod.POST)

public String submit(@ModelAttribute("command") NewArticleCommand command) {

articleService.writeArticle(command);

return "article/newArticleSubmitted";

}

}

@ModelAndView 어노테이션을 이용해서 command 객체의 모델 이름을 지정했다면, 뷰 코드에서는 다음과 같이 해당 모델 이름을 사용해서 command 객체에 접근할 수 있다.

제목:${command.title} <---  [기존의 제목:${newArticleCommand.title}]


2. 커맨드 객체로 List 받기

스프링 MVC는 List 타입의 프로퍼티에 대한 바인딩도 처리해준다. 예를 들어, 아래 코드는 OrderItem 목록을 갖는 List타입의 orderItems 프로퍼티를 갖고 있다.


public class OrderCommand {


private List<OrderItem> orderItems; //OrderItem 목록을 갖는 List타입의 orderItems 프로퍼티

private Address address;


public List<OrderItem> getOrderItems() {

return orderItems;

}


public void setOrderItems(List<OrderItem> orderItems) {

this.orderItems = orderItems;

}


public Address getAddress() {

return address;

}


public void setAddress(Address address) {

this.address = address;

}

}


List 타입의 프로퍼티에 값을 전달하고 싶다면 폼에서 "프로퍼티명[index].프로퍼티" 같이 입력 폼의 이름을 구성하면 된다. 예를 들어, 위 코드의 orderItems 프로퍼티에 값을 전달하고 싶다면

아래 코드와 같이 폼을 구성하면 된다.

<form method="post">

상품1: ID - <input type="text" name="orderItems[0].itemId" /> 

개수 - <input type="text" name="orderItems[0].number" />

주의 - <input type="text" name="orderItems[0].remark" />

<br/>

상품2: ID - <input type="text" name="orderItems[1].itemId" /> 

개수 - <input type="text" name="orderItems[1].number" />

주의 - <input type="text" name="orderItems[1].remark" />

<br/>

......

<input type="submit" />

</form>



폼의 <input>이나 <select> 등의 name에 인덱스 값을 포함시키면 List 타입의 프로퍼티에 값을 전달받을 수 있다. 컨트롤러 코드에서는 다음과 같이 커맨드 객체를 @RequestMapping 메서드에 지정해주기만 하면 된다.

@Controller

@RequestMapping("/order/order.do")

public class OrderController {

@RequestMapping(method = RequestMethod.POST)

public String submit(OrderCommand orderCommand) {

return "order/orderCompletion";

}

}



[Java] 직렬화


직렬화


컴퓨터에 저장했다가 다음에 다시 꺼내 쓸 수는 없을지 또는 네트웍을 통해 컴퓨터 간에 서로 객체를 주고 받을 수는 없을까라고 고민해 본 적이 있는가? 과연 이러한 일들이 가능할까?

가능하다. 이러한 것을 직렬화가 처리해준다.


1. 직렬화란


직렬화(스트림으로)란 객체를 데이터 스트림으로 만드는 것을 뜻한다. 즉, 객체에 저장된 데이터를 스트림에 쓰기 위해 연속적인 데이터로 변환하는 것을 말한다.

반대로 스트림으로부터 데이터를 읽어서 객체를 만드는 것을 역직렬화(객체로)라고 한다.

객체 스트림은 프로그램 메모리상에 존재하는 객체를 직접 입출력해 줄 수 있는 스트림으로 현재 상태를 보존하기 위한 영속성을 지원할 수 있다.

자바에서 객체 안에 저장되어 있는 내용을 파일로 저장하거나 네트워크를 통하여 다른 곳으로 전송하려면 객체를 바이트 형태로 일일이 분해해야 한다. 

이를 위하여 객체를 직접 입출력 할 수 있도록 해주는 객체 스트림이다.


직렬화(Serialization)

 - 객체를 데이터스트림(스트림에 쓰기(write)위한 연속적인(serial) 데이터)으로 만드는 것.

 - 예) 객체를 컴퓨터에 저장했다가 꺼내 쓰기. 네트워크를 통한 컴퓨터 간의 객체 전송.  


역직렬화(Deserialization)

 - 스트림으로부터 데이터를 읽어서 객체를 만드는 것. 



사실 객체를 저장하거나 전송하려면 당연히 직렬화를 거칠수 밖에 없다.

객체를 저장한다는 것이 무엇을 의미하는지 상기시켜야 한다.

객체는 클래스에 정의된 인스턴스변수의 집합이다. 객체에는 클래스변수나 메서드가 포함되지 않는다. 객체는 오직 인스턴스변수들로만 구성되어 있다.

인스턴스변수는 인스턴스마다 다른 값을 가질 수 있어야하기 때문에 별도의 메모리공간이 필요하지만 메서드는 변하는 것이 아니라서 메모리를 낭비해 가면서 인스턴스마다 같은 내용의 코드를 포함시킬 이유가 없다.



위의 그림은 6장에 나오는 Tv클래스의 객체가 생성되었을 때 사용한 그림인데, 왼쪽 그림은 이해를 돕기 위해 인스턴스에 메서드를 포함시켜서 그렸지만, 실제로는 오른쪽 그림과 같이 인스턴스에 메서드가 포함되지 않는것이 더 정확한 그림이다. 그래서 객체를 저장한다는 것은 바로 객체의 모든 인스턴스변수의 값을 저장한다는 것과 같은 의미이다. 어떤 객체를 저장하고자 한다면, 현재 객체의 모든 인스턴스변수의 값을 저장하기만 하면된다. 그리고 저장했던 객체를 다시 생성하려면, 객체를 생성한 후에 저장했던 값을 읽어서 생성한 객체의 인스턴스변수에 저장하면 되는 것이다.

클래스에 정의된 인스턴스변수가 단순히 기본형일 때는 인스턴스변수의 값을 저장하는 일이 간단하지만. 인스턴스변수의 타입이 참조형일 때는 그리 간단하지 않다. 예를 들어 인스턴스변수의 타입이 배열이라면 배열에 저장된 값들도 모두 저장되어야 할 것이다. 그러나 우리는 객체를 어떻게 직렬화해야 하는지 전혀 고민하지 않아도 된다. 다만 객체를 직렬화/역직렬화할 수 있는 ObjectInputStream과 ObjectOutputStream을 사용하는 방법만 알면 된다.

--> 두 객체가 동일한지 판단하는 기준이 두 객체의 인스턴스변수 값들이 같고 다름이라는 것을 기억하자.


2. ObjectInputStream(직렬화) / ObjectOutputStream(역직렬화)

직렬화(스트림에 객체를 출력)에는 ObjectInputStream을 사용하고 역직렬화(스트림으로부터 객체를 입력)에는 ObjectOutputStream을 사용한다.

ObjectInputStream과 ObjectOutputStream은 각각 InputStream / OutputStream을 직접 상속받지만 기반스트림을 필요로하는 보조스트림이다. 그래서 객체를 생성할 때 입출력(직렬화/역직렬화)할 스트림을 지정해주어야 한다.


만일 파일에 객체를 저장(직렬화)하고 싶다면 다음과 같이 해야 한다.


※ UserInfo 객체를 직렬화하여 저장

FileOutputStream fos = new FileOutputStream("objectfile.ser");

ObjectOutputStream out = new ObjectOutputStream(fos);

out.writeObject(new UserInfo());

--> objectfile.ser이라는 파일에 UserInfo객체를 직렬화하여 저장한다. 출력할 스트림(FileOutputStream)을 생성해서 이를 기반 스트림으로 하는 ObjectOutputStream을 생성한다.

ObjectOutputStream의 writeObject(Object obj)를 사용해서 객체를 출력하면, 객체가 파일에 직렬화되어 저장된다. 


※ UserInfo 객체를 역직렬화

FileInputStream fis = new FileInputStream("objectfile.ser");

ObjectInputStream in = new ObjectInputStream(fis);

UserInfo Info = (UserInfo)in.readObject();

--> 직렬화할 때와는 달리 입력스트림을 사용하고 writeObject(Object obj)대신 readObject()를 사용하여 저장된 데이터를 읽기만 하면 객체로 역직렬화된다.

다만, readObject()의 반환타입이 Object이기 때문에 객체  원래의 타입으로 형변환 해주어야 한다.


(1) 객체 전송의 단계

객체를 분해하여 전송하기 위해서는 직렬화(Serialization) 되어야 한다.

객체를 전송하기 위해서는 3가지 단계를 거친다.

1) 직렬화된 객체를 바이트 단위로 분해한다. (marshalling)

2) 직렬화 되어 분해된 데이터를 순서에 따라 전송한다.

3) 전송 받은 데이터를 원래대로 복구한다. (unmarshalling)


(2) 마샬링 (marshalling)

마샬링(marshalling)은 데이터를 바이트의 덩어리로 만들어 스트림에 보낼 수 있는 형태로 바꾸는 변환 작업을 뜻한다.

자바에서 마샬링을 적용할 수 있는 데이터는 원시 자료형(boolean, char, byte, short, int, long, float, double)와 객체 중에서 Serializable 인터페이스를 구현한 클래스로 만들어진 객체이다. 객체는 원시 자료형과 달리 일정한 크기를 가지지 않고 객체 내부의 멤버 변수가 다르기 때문에 크기가 천차만별로 달라진다. 이런 문제점을 처리할 수 있는게 ObjectOutputStream 클래스이다.


(3) 직렬화 (Serializable)

마샬링으로 바이트로 분해된 객체는 스트림을 통해서 나갈 수 있는 준비가 되었다. 앞에서 언급한대로 객체를 마샬링하기 위해서는 Serializable 인터페이스를 구현한 클래스로 만들어진 객체에 한해서만 마샬링이 진행될 수 있다. Serializable 인터페이스는 아무런 메소드가 없고 단순히 자바 버추얼 머신에게 정보를 전달하는 의미만을 가진다.


* 직렬화가 가능한 객체의 조건

1) 기본형 타입(boolean, char, byte, short, int, long, float, double)은 직렬화가 가능

2) Serializable 인터페이스를 구현한 객체여야 한다. (Vector 클래스는 Serializable 인터페이스구현)

3) 해당 객체의 멤버들 중에 Serializable 인터페이스가 구현되지 않은게 존재하면 안된다.

4) transient 가 사용된 멤버는 전송되지 않는다. (보안 변수 : null 전송)


(4) 언마샬링 (unmarshalling)

언마샬링은 객체 스트림을 통해서 전달된 바이트 덩어리를 원래의 객체로 복구하는 작업이다. 이 작업을 제대로 수행하기 위해서는 반드시 어떤 객체 형태로 복구할지 형 변환을 정확하게 해주어야 한다.

Vector v = (Vector)ois.readObject(); 

// OutputInputStream의 객체를 읽어서 Vector 형으로 형변환 한다.

이때 ObjectInputStream을 사용하여 데이터를 복구한다.



import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;


public class ObjectStream {

public static void main(String[] args){

// ObjectOutputStream 을 이용한 객체 파일 저장

FileOutputStream fos = null;

ObjectOutputStream oos = null;

// UserClass 에 이름과 나이를 입력하여 객체를 3개 생성한다.

UserClass us1 = new UserClass("하이언", 30);

UserClass us2 = new UserClass("스티브", 33);

UserClass us3 = new UserClass("제이슨", 27);

try{

// object.dat 파일의 객체 아웃풋스트림을 생성한다.

fos = new FileOutputStream("object.dat");

oos = new ObjectOutputStream(fos);

// 해당 파일에 3개의 객체를 순차적으로 쓴다

oos.writeObject(us1);

oos.writeObject(us2);

oos.writeObject(us3);

// object.dat 파일에 3개의 객체 쓰기 완료.

System.out.println("객체를 저장했습니다.");

}catch(Exception e){

e.printStackTrace();

}finally{

// 스트림을 닫아준다.

if(fos != null) try{fos.close();}catch(IOException e){}

if(oos != null) try{oos.close();}catch(IOException e){}

}

// 파일로 부터 객체 데이터 읽어온다.

FileInputStream fis = null;

ObjectInputStream ois = null;

try{

// object.dat 파일로 부터 객체를 읽어오는 스트림을 생성한다.

fis = new FileInputStream("object.dat");

ois = new ObjectInputStream(fis);

// ObjectInputStream으로 부터 객체 하나씩 읽어서 출력한다.

// (UserClass) 로 형변환을 작성해야 한다.

// System.out.println 으로 객체의 구현된 toString() 함수를 호출한다.

System.out.println( (UserClass)ois.readObject());

System.out.println( (UserClass)ois.readObject());

System.out.println( (UserClass)ois.readObject());



}catch(Exception e){

e.printStackTrace();

}finally{

// 스트림을 닫아준다.

if(fis != null) try{fis.close();}catch(IOException e){}

if(ois != null) try{ois.close();}catch(IOException e){}

}

}


}

실행결과)

- UserClass 객체를 생성하여 ObjectOutputStream을 통해 object.dat 에 순차적으로 객체를 쓴다.

- UserClass 객체를 Stream에 쓰기위해서는 Serializable 인터페이스를 사용해야 직렬화 할 수 있다.

- Serializable 구현하지 않으면 NotSerializableException이 발생한다.

- ObjectInputStream을 통해 object.dat에 저장되어 있는 객체를 읽어온다.

- ObjectInputStream에서 readObject()로 읽을때는 정확한 형변환을 해주어야 정확하게 언마샬링을 할 수 있다.


3. Serializable 과 transient


(1) Serializable

데이터를 파일에 쓰거나, 네트워크를 타고 다른 곳에 전송할 때는 데이터를 바이트 단위로 분해하여 순차적으로 보내야 한다. 이것을 직렬화(Serialization)라고 한다.

기본 자료형(boolean, char, byte, short, int ,long, float, double)은 정해진 바이트의 변수이기 때문에 바이트 단위로 분해하여 전송한 후 다시 조립하는데 문제가 없다.

하지만 객체의 크기는 가변적이며, 객체를 구성하는 자료형들의 종류와 수에 따라 객체의 크기는 다양하게 바뀔 수 있다. 이런 객체를 직렬화 하기 위해서 Serializable 인터페이스를 구현하게 된다.


* 직렬화가 가능한 객체의 조건

① 기본형 타입(boolean, char, byte, short, int, long, float, double)은 직렬화가 가능

② Serializable 인터페이스를 구현한 객체여야 한다. (Vector 클래스는 Serializable 인터페이스구현)

③ 해당 객체의 멤버들 중에 Serializable 인터페이스가 구현되지 않은게 존재하면 안된다.

④ transient 가 사용된 멤버는 전송되지 않는다. (보안 변수 : null 전송)

객체 직렬화는 객체에 implements Serializable 만 선언해 주면 된다.


(2) transient

하지만, 객체의 데이터 중 일부의 데이터는(패스워드와 같은 보안) 여러가지 이유로 전송을 하고 싶지 않을 수 있다. 이러한 변수는 직렬화에서 제외해야 되며, 이를 위해서 변수에 transient를 선언한다.

또한, 직렬화 조건 중 객체의 멤버들 중에 Serializable 인터페이스 구현되지 않은 객체가 있으면, 직렬화 할 수 없다.(NonSerializableException) 직렬화 해야 되는 객체 안의 객체 중 Serializable 인터페이스가 구현되지 않으면서 전송하지 않아도 되는 객체 앞에는 transient 를 선언해준다. 그러면 직렬화 대상에서 제외되므로 해당 객체는 직렬화가 가능해진다.


public class UserInfo implements Serializable{

    String name;

    String password;

    int age;

     

    Object ob = new Object();   

    // 모든 클래스의 최고조상인 Object는 Serializable을

    // 구현하지 않았기 때문에 직렬화가 불가능하다.

     

    Object obj = new String("abc"); // String은 직렬화될 수 있다.

     

    // 직렬화 제외

    transient String weight;    

    transient Object obe = new Object();

}


cp.) 직렬화하고자 하는 객체의 클래스에 직렬화가 안 되는 객체에 대한 참조를 포함하고 있다면, 제어자 transient를 붙여서 직렬화 대상에서 제외되도록 할 수 있다.

또는 password와 같이 보안상 직렬화되면 안되는 값에 대해서 transient를 사용할 수 있다.

다르게 표현하면 transien가 붙은 인스턴스변수의 값은 그 타입의 기본값으로 직렬화된다고 볼 수 있다.

즉, UserInfo객체를 역직렬화하면 참조변수인 obj와 password의 값은 null이 된다.


※ Serializable 과 transient 사용 예제

UserClass.java

import java.io.Serializable;


// 직렬화 한다.

public class UserClass implements Serializable{

private static final long serialVersionUID = 4220461820168818967L;

String name;

// age 비 전송

transient int age;

// NonSerializable 클래스

NonSerializableClass nonSerializable;

public UserClass() {

}

public UserClass(String name, int age){

this.name = name;

this.age = age;

this.nonSerializable = new NonSerializableClass(false);

}


public String getName() {

return name;

}


public void setName(String name) {

this.name = name;

}


public int getAge() {

return age;

}


public void setAge(int age) {

this.age = age;

}


public NonSerializableClass getNonSerializable() {

return nonSerializable;

}


public void setNonSerializable(NonSerializableClass nonSerializable) {

this.nonSerializable = nonSerializable;

}


@Override

public String toString() {

return "UserClass [name=" + name + ", age=" + age

+ ", nonSerializable=" + nonSerializable + "]";

}

}


public class NonSerializableClass {

boolean serializable;

public NonSerializableClass(){

this.serializable = false;

}

public NonSerializableClass(boolean serializable){

this.serializable = serializable;

}

}


import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;


public class ObjectStream {

public static void main(String[] args){

// ObjectOutputStream 을 이용한 객체 파일 저장

FileOutputStream fos = null;

ObjectOutputStream oos = null;

// UserClass 에 이름과 나이를 입력하여 객체를 3개 생성한다.

UserClass us1 = new UserClass("하이언", 30);

UserClass us2 = new UserClass("스티브", 33);

UserClass us3 = new UserClass("제이슨", 27);

try{

// object.dat 파일의 객체 아웃풋스트림을 생성한다.

fos = new FileOutputStream("object.dat");

oos = new ObjectOutputStream(fos);

// 해당 파일에 3개의 객체를 순차적으로 쓴다

oos.writeObject(us1);

oos.writeObject(us2);

oos.writeObject(us3);

// object.dat 파일에 3개의 객체 쓰기 완료.

System.out.println("객체를 저장했습니다.");

}catch(Exception e){

e.printStackTrace();

}finally{

// 스트림을 닫아준다.

if(fos != null) try{fos.close();}catch(IOException e){}

if(oos != null) try{oos.close();}catch(IOException e){}

}

// 파일로 부터 객체 데이터 읽어온다.

FileInputStream fis = null;

ObjectInputStream ois = null;

try{

// object.dat 파일로 부터 객체를 읽어오는 스트림을 생성한다.

fis = new FileInputStream("object.dat");

ois = new ObjectInputStream(fis);

// ObjectInputStream으로 부터 객체 하나씩 읽어서 출력한다.

// (UserClass) 로 형변환을 작성해야 한다.

// System.out.println 으로 객체의 구현된 toString() 함수를 호출한다.

System.out.println( (UserClass)ois.readObject());

System.out.println( (UserClass)ois.readObject());

System.out.println( (UserClass)ois.readObject());



}catch(Exception e){

e.printStackTrace();

}finally{

// 스트림을 닫아준다.

if(fis != null) try{fis.close();}catch(IOException e){}

if(ois != null) try{ois.close();}catch(IOException e){}

}

}

}


UserClass.java 의 변수를 보면 transient int age; 로 age 변수는 직렬화에서 제외했다.

- NonSerializableClass 객체는 Serializable 인터페이스를 구현하지 않은 클래스이다.

- 따라서 UserClass.java 로 직렬화를 시도하면, 위와 같이 NonSerializableClass Exception이 발생한다.

- 위의 문제를 해결하기 위해서는 NonSerializableClass.java 에 Serializable 인터페이스를 구현하여 직렬화를 할 수 있게 하는 방법과

- NonSerializableClass 를 전송하지 않아도 되면, 또는 않아야 한다면 transient 를 앞에 붙여주는 것이다.

- 그러면 NonSerializableClass 객체는 직렬화 대상에서 제외되면서 UserClass 가 정상적으로 직렬화되어 처리될 것이다.


※ NonSerializableClass 객체 선언 앞에 transient 선언 결과

import java.io.Serializable;


// 직렬화 한다.

public class UserClass implements Serializable{

private static final long serialVersionUID = 4220461820168818967L;

String name;

// age 비 전송

transient int age;

// NonSerializable 클래스

transient NonSerializableClass nonSerializable;

public UserClass() {

}

public UserClass(String name, int age){

this.name = name;

this.age = age;

this.nonSerializable = new NonSerializableClass(false);

}


public String getName() {

return name;

}


public void setName(String name) {

this.name = name;

}


public int getAge() {

return age;

}


public void setAge(int age) {

this.age = age;

}

public NonSerializableClass getNonSerializable() {

return nonSerializable;

}


public void setNonSerializable(NonSerializableClass nonSerializable) {

this.nonSerializable = nonSerializable;

}


@Override

public String toString() {

return "UserClass [name=" + name + ", age=" + age

+ ", nonSerializable=" + nonSerializable + "]";

}

}


- 객체가 정상적으로 직렬화되어 전송되고, 가져와 출력되는 것을 볼 수 있다.

- 당연히 transient가 붙은 age 변수와 nonSerializable 은 직렬화 되지 않기에 데이터가 없다.


4. 직렬화가능한 클래스의 버전관리
직렬화된 객체를 역직렬화할 때 서로 같은 클래스를 사용해야 하는데, 클래스의 이름이 같더라도 클래스의 내용이 변경된 경우 역직렬화가 실패하며 에러가 발생한다.
static 변수나 상수 또는 trasient가 붙은 인스턴스변수의 경우 직렬화에 영향을 미치지 않는다.

 java.io.InvalidClassException: UserInfo; local class incompatible: stream classdesc
 serialVersionUID = 6953673583338942489, local class serialVersion UID = -6256164443556992367 ...
 
해결책) serialVersionUID를 정의한다.
class MyData implements java.io.Serializable{
    static final long serialVersionUID = 3518731767529258119L;
    // 이렇게 추가해주면 클래스의 내용이 바뀌어도 클래스의 버전이 고정된다.
     
    int value;
}
 
serialVersionUID 얻기 (아무 정수를 써도 상관없지만 중복될 가능성때문에 사용하는 편이 좋다.)

cp.) erialVersionUID 이란? Warning 해결하기


객체를 파일에 쓰거나 전송하기 위해서는 직렬화를 해야 하는데 그러기 위해 객체 클래스에 Serializable 인터페이스를 implements 하게 된다.
하지만 Serializable 인터페이스를 implements 하게 되면 노란색 Warning이 발생한다.
The serializable class *** does not declare a static final serialVersionUID field of type long
저렇게 Warning이 발생하지만 동작하는데는 문제가 없다.
그래도 계속 저렇게 Warning이 떠있는데 왜 생기는 것이며 serialVersionUID 는 무엇이길래 없다고 그러는 건가?
serialVersionUID 는 직렬화에 사용되는 고유 아이디인데, 선언하지 않으면 JVM에서 디폴트로 자동 생성된다.
따라서 선언하지 않아도 동작하는데 문제는 없지만, 불안하기 때문에 JAVA에서는 명시적으로 serialVersionUID를 선언할 것을 적극 권장하고 있다.

* JVM에 의한 디폴트 serialVersionUID 계산은 클래스의 세부 사항을 매우 민감하게 반영하기 때문에 컴파일러 구현체에 따라서 달라질 수 있어 deserialization 과정에서 예상하지 못한 InvalidClassException을 유발할 수 있다.

serialVersionUID는 private static final 로 선언하면 된다.

그럼 serialVersionUID는 어떻게 생성하면 될까?
이클립스에서는 serialVersionUID를 자동으로 선언해주는 플러그인 있다.
위의 파일을 다운받고 압축을 풀어서 eclipse\plugin 폴더에 넣어 놓고 이클립스를 재시작 한다.

http://hyeonstorage.tistory.com/attachment/cfile26.uf@25748E385325AEA31EC4FD.zip


serialVersionUID 를 생성하고자 하는 (Serializable을 implements 한) 클래스에 마우스 오른쪽 버튼을 누르면 
아래 그림과 같이 Add SerialVersionUID 가 있다.


Add SerialVersionUID를 클릭하면 serialVersionUID가 생성된다.

앞에 private를 붙여서 private static final long 형태가 되도록 하자.


이제 노란 Warning이 없어지는 것을 볼 수 있다.
Warning을 없애는 방법은 SerialVersionUID 선언 외에 다른 방법이 있다.
클래스 위에 @SuppressWarnings("serial") 이라고 어노테이션 처리를 해주면 없어진다.
하지만 SerialVersionUID를 선언해주는 것이 권장되는 방법이다.


'Programing > Java' 카테고리의 다른 글

[Java] 쓰레드의 우선순위  (0) 2014.12.21
[Java] 쓰레드 기본  (0) 2014.12.21
[Java] File 클래스  (0) 2014.12.16
[Java] 문자 기반 스트림  (0) 2014.12.16
[Java] 바이트 기반의 스트림  (0) 2014.12.16