[Spring] 토비의 스프링 Vol.2 4장 스프링 @MVC (2)

Shiba·2023년 11월 21일
0

🍀 스프링 정리

목록 보기
17/21
post-thumbnail

📗 스프링 @MVC (2)

📖 JSP 뷰와 form 태그

여기서는 폼을 이용한 뷰를 작성할 때 필요한 스프링의 지원 기능에 대해 알아볼 것이다.
- 스프링의 뷰 기술 중 JSP/JSTL만 다룰 것이다. 벨로시티나 프리마커와 같은 템플릿 엔진을 사용한 뷰 기술에 대한 부분은 레퍼런스 문서를 참고하도록 하자.

📝 EL과 spring 태그 라이브러리를 이용한 모델 출력

일반적인 뷰에서 모델 오브젝트를 어떻게 사용할 수 있는지 알아보자.

◼ JSP EL

스프링은 JSP 뷰에서 모델 맵에 담긴 오브젝트JSP EL을 통해 접근할 수 있게 해줌.

//컨트롤러 메소드에서 모델 오브젝트 추가
model.addAttribute("name" "Spring");
//EL을 사용하여 JSP 뷰에서 모델 오브젝트 값 출력
<div> 이름 : ${name}</div>

//자바빈의 접근자를 가진 모델 오브젝트라면 프로퍼티 값 출력 가능
${user.age}

◼ 스프링 SpEL

스프링 3.0의 SpEL을 사용모델을 출력할 수도 있음
- JSP EL보다 유연하고 강력한 표현식 지원

//JSP 뷰에서 스프링 SpEL을 사용하기 위해 spring 태그 라이브러리 추가
<%@ taglib prefix="spring" url="http://www.springframework.org/tags" %>

<spring:eval> 태그를 사용해서 모델 이름이 포함된 표현식을 작성하면 된다.
<spring:eval>의 표현식에서는 모델 오브젝트를 직접 사용할 수 있다.

<spring:eval expression="user.name" />

SpEL은 오브젝트의 메소드 호출이 가능하다.
메소드가 스트링 타입의 리턴 값을 가진 경우, ()를 붙여 메소드 호출

<spring:eval expression="user.toString()" />

SpEL은 다양한 논리, 산술연산도 지원한다. 또한, 클래스를 지정해 스태틱 메소드를 호출할 수도 있다. 심지어는 new 키워드를 이용해 오브젝트를 만들고 메소드를 실행할 수도 있다.

<spring:eval expression=
	'new java.text.DecimalFormat("###,##0.00").format(user.point)' />

SpEL이 JSP EL보다 훨씬 기능이 뛰어난 표현식 언어임에는 분명하지만 뷰에서 복잡한 표현식을 사용하면서 과용하는 것은 바람직하지 않음. 적절한 기준을 마련하고 그 안에서만 SpEL을 활용해야 한다.

SpEL의 장점 중 한 가지는 컨버전 서비스를 이용할 수 있다는 점이다.
- mvc 네임스페이스의 전용태그를 이용하면 컨버전 서비스에 등록되는 포맷터를 <spring:eval>에도 자동 적용

EX) @NumberFormat 애노테이션으로 포맷이 정의된 user 모델의 point 프로퍼티가 존재

@NumberFormat(pattern="###,##0")
Integer point;

위 모델 오브젝트를 스프링 form 태그의 지원을 받아 폼의 필드 값으로 출력할 때 컨버전 서비스의 적용을 받아서 지정된 패턴에 맞는 값이 출력됨.

<spring:eval expression="user.point" />

//출력 값
12,345

◼ 지역화 메세지 출력

화면에 출력할 일반 메세지에서도 지역정보에 따라 메세지를 가져와서 출력 가능
- spring 태그 라이브러리의 message 태그를 사용
- messageSource 아이디를 가진 MessageSource 빈이 등록되고 messages와 같은 메세지 파일의 기본 이름이 프로퍼티에 지정되어 있어야 한다.

<spring:message code="greeting" />
//LocaleResolver가 결정한 지역정보가 KOREAN으로 되어 있다면
//message.ko.properties 파일에서 greeting 키에 해당하는 메세지 찾음

메세지 프로퍼티 파일에 프로퍼티 치환자 사용 시, 파라미터에 들어갈 내용을 arguments 애트리뷰트로 지정 가능

//파라미터 치환자 사용
greeting=Hello {0}!

메세지를 찾지 못했을 경우 사용할 디폴트 메세지를 text 애트리뷰트로 설정 가능

<spring:message code="greeting" arguments="${user.name}" text="Hi"/>

📝 spring 태그 라이브러리를 이용한 폼 작성

이제 폼이 포함된 뷰를 만들어보자

◼ 단일 폼 모델

HTML의 <form> 태그로 만들어진 폼 뷰를 사용하는 데는 두 가지 시나리오가 존재


1. 등록 - 처음에는 빈 폼이 뜨고 사용자 입력을 받아서 이를 저장하는 폼
2. 수정 - 폼에는 내용이 채워서 출력되고 이를 수정해서 저장할 수 있는 폼

하지만, 스프링은 두 가지 종류의 폼을 구분하지 않는다. 모두 처음 폼을 띄울 때부터 모델의 정보를 폼에 출력하는 방식을 사용
- 등록 폼에서도 모델의 정보를 폼에 다시 출력해줄 필요가 있기 때문
(사용자 입력 값에 오류가 발생한 경우, 디폴트 값을 보여주는 경우 등)

스프링 MVC에서 폼을 처리하는 과정을 다시 살펴보자


1. 폼에 출력할 모델 오브젝트 결정
2. 폼을 그릴 폼 뷰를 JSP로 만듦.
3. 각 프로퍼티 값을 폼의 필드 값으로 출력
4. 이 폼을 처음 출력할 때는 GET 메소드의 요청을 사용
- 등록 화면이라면 빈 오브젝트가 모델로, 수정 화면이라면 DB에서 읽어온 오브젝트가 폼으로 전달

사용자가 원하는 값을 입력하거나 수정한 후, 저장 버튼을 누르면 POST 메소드로 서버에 요청이 날아가서 아래와 같은 과정을 거쳐 폼 입력 결과를 처리함.


1. 폼의 모든 필드정보가 HTTP 요청 파라미터로 전송
2. 컨트롤러가 처리할 수 있는 @ModelAttribute 오브젝트인 User에 바인딩
3. 바인딩과 함께 검증 작업 진행
3-1. 오류가 하나라도 발견되면 컨트롤러는 같은 폼 뷰를 다시 띄워서 에러 메시지를 보여줌
3-2. 아무런 오류가 없다면 컨트롤러는 다른 뷰를 출력하거나 다른 페이지로 리다이렉트

◼ <spring:bind>와 BindingStatus

EL을 사용해 폼의 필드를 띄우는 방법에는 두 가지 한계가 존재


1. 바인딩 오류가 있을 때 에러 메시지를 출력할 수 없음
- EL로는 간단히 에러 메시지를 출력하는 방법이 없다.

2. 바인딩 작업 중 타입 변환에서 오류가 나는 경우에 잘못 입력한 값을 출력할 수 없다는 점
- 숫자로 바뀔 수 없는 값을 Integer타입에 넣은 경우, 폼을 다시 보여줄 때 값이 null 값으로 변하여 출력됨.

스프링은 위와 같이 JSP EL을 사용했을 때의 한계를 극복하고 스프링의 모델과 바인딩 결과정보를 최대한 활용할 수 있도록 <spring:bind> 태그를 제공

<spring:bind> 태그 내부에서 사용할 수 있도록 BindStatus 타입의 오브젝트를 status라는 이름의 변수로 등록

// user.name 프로퍼티에 대한 정보를 담은 BindStatus 타입 status 변수를 사용할 수 있다.
<spring:bind path="user.name">
...
</spring:bind> 

path로 설정된 프로퍼티에 관련된 많은 정보들을 제공

BindStatus 오브젝트의 프로퍼티

프로퍼티타입내용
expressionString폼 <input> 태그의 name으로도 사용할 수 있는 프로퍼티 이름.
user 모델 오브젝트의 name 프로퍼티라면 name이 지정
valueString바인딩 오류가 없는 경우에는 모델의 프로퍼티 값을 갖고 있고,
필드에 바인딩 오류가 있는 경우에는 이전에 입력했던 값을 얻을 수 있다.
errorMessagesString[]필드에 할당된 모든 에러 메시지를 담은 스트링 배열을 돌려줌
필드 하나에 여러 개의 바인딩 오류가 등록된 경우에는 하나 이상의
에러 메시지를 가질 수 있다.
errorMessageString첫 번째 에러 메시지만 돌려줌
errorsErrors바인딩 오류 정보를 얻을 수 있는 Errors 타입 오브젝트를 돌려줌.
Errors 인터페이스를 통해 다시 errorCount 같은 정보를 얻을 수 있다.

이제 <spring:bind>를 이용해서 name 필드의 뷰 코드를 작성해보자.

<spring:bind path="user.name">
	<label for="name">Name : </label>
    <!--status.value - 기존에 잘못 입력한 값이 그대로 출력되도록 해줌-->
    <input type="text" id="${status.expression}" name="${status.expression}"
    		value="${status.value}" />
</spring:bind>

이렇게 만들면 EL의 한계였던 바인딩 오류 시 잘못 입력한 값을 보여주는 기능은 해결됨. 이제 에러 메시지를 출력하게 만들어보자.

에러 메시지는 status 변수의 errorMessages를 사용.
- 하나 이상의 에러 메시지를 가진 배열이므로 JSTL의 <c:foreach> 태그를 이용해 루프를 돌면서 모든 메시지를 출력해주어야 한다.

<spring:bind path="user.name">
	<label for="name">Name : </label>
    <input type="text" id="${status.expression}" name="${status.expression}"
    		value="${status.value}" />
  	<!--에러 메시지 강조를 위해 span 태그로 감싸서 다른 색이나 모양으로 표시하기-->
  	<span class="errorMessage">
      <c:forEach var="errorMessage" items="${status.errorMessages}">
        	${errorMessage}
      </c:forEach>
  	</span>
</spring:bind>

여기서 한 가지 작업을 더 해보자. 이번엔 <label> 태그로 출력한 'Name'도 필드에 에러가 있는 경우 errorMessages라는 CSS클래스를 적용하고 싶다.
EL을 이용한 식의 status.errorMessage 값을 확인해보면 현재 필드에 에러가 있는지를 알 수 있으므로, 있는 경우에만 <label> 태그에 에러 메시지용 CSS 클래스 선언을 넣도록 만들 수 있다.

아래는 최종적으로 완성된 코드이다.

<spring:bind path="user.name">
	<label for="name"
    	<c:if test="${status.errorMessage != ''}">class="errorMessage"</c:if>
    >Name : </label>
    <input type="text" id="${status.expression}" name="${status.expression}"
    		value="${status.value}" />
  	<span class="errorMessage">
      <c:forEach var="errorMessage" items="${status.errorMessages}">
        	${errorMessage}
      </c:forEach>
  	</span>
</spring:bind>

📝 form 태그 라이브러리

스프링 form 태그 라이브러리를 이용하면 <spring:bind> 보다 훨씬 간결한 코드로 동일한 기능을 하는 코드를 만들 수 있다. 위의 코드를 아래와 같이 세 줄만으로 구현할 수 있다.

<form:label path="name" cssErrorClass="errorMessage">Name</form:label> :
<form:input path="name" size="30" />
<form:errors path="name" cssClass="errorMessage" />

form 태그를 사용하면 다음과 같이 기능은 동일하지만 훨씬 간결한 코드를 만들 수 있다.
하지만 HTML 코드를 직접 사용하지 못한다는 단점이 존재한다. 대신, form 태그에 익숙해지기만 하면 HTML 코드를 사용하지 않아도 HTML의 모든 애트리뷰트를 지정할 수 있음

지금부터는 자주 사용되는 form 태그 라이브러리의 태그를 살펴보겠다.

◼ <form:form>

HTML <form> 태그를 만들어준다. 또한 <form:form> 태그 내부의 입력 필드에 적용할 모델의 이름을 지정할 수 있다.

commandName, modelAttribute

폼에 적용할 모델의 이름을 지정. 스프링 MVC에서 말하는 커맨드는 모델 애트리뷰트와 같은 의미.
user 모델에 대한 폼이라면 commandName="user" 또는 modelAttribute="user"라고 정의한다. 디폴트 값은 command 이다.
모델 이름이 command가 아니라면 직접 지정해줘야한다. <form>태그의 id 값으로도 사용된다.

method

HTML <form> 태그의 method="post"에 들어가는 메소드 이름을 지정. 디폴트 값은 post다. 따라서 대개 method는 생략해도 좋다.

action

HTML <form> 태그의 action 부분을 설정해서 URL을 지정할 수 있게 해준다. 일반적으로는 생략한다.
- 폼을 띄우는 요청과 submit하는 요청의 URL을 동일하게 쓰고, HTTP 메소드만 GET/POST로 구분하는 것이 간결하기 때문

◼ <form:input>

HTML의 <input type="text"> 태그를 생성. 필수 애트리뷰트는 path뿐이다.
그외의 <input> 태그에 적용가능한 표준 HTML 애트리뷰트와 이벤트 애트리뷰트 등을 지원

path

id를 따로 지정하지 않았다면 <input> 태그의 id, name에 할당되며 value 애트리뷰트에 지정할 모델의 프로퍼티 이름으로 사용
이전 폼 submit에 오류가 있어서 폼이 다시 뜬 경우에는 모델에 바인딩 값 대신 직전에 잘못 입력한 값이 value에 들어감.
따라서, 모델의 프로퍼티에서는 지원하지 않는 타입의 정보도 출력할 수 있다.

cssClass, cssErrorClass

cssClass는 <input> 태그의 class 애트리뷰트 값을 지정할 때 사용
path에 해당하는 프로퍼티에 바인딩 오류가 있다면, cssClass 대신 cssErrorClass에 지정한 값이 Css class로 지정된다. 이를 이용해 바인딩 오류 시 스타일을 다르게 만들 수 있다.

◼ <form:label>

폼의 레이블 출력에 사용되는 <label> 태그를 만들어준다. 필수 애트리뷰트는 path다. <form:input>과 마찬가지로 cssClass, cssErrorClass 애트리뷰트를 이용해서 바인딩 오류가 없을 때와 있을 때의 CSS 클래스를 각각 지정할 수 있다.

◼ <form:errors>

바인딩 에러 메세지를 출력할 때 사용. 기본적으로 <span> 태그를 사용에러 메세지를 감싼다.
필수 애트리뷰트는 없지만 path 설정에 따라 에러 메시지의 종류를 선택할 수 있다.

path

기본적으로 생략 가능하다. 생략할 시 Errors의 reject() 메소드에 의해 등록된 모델 레벨의 글로벌 에러 메시지가 출력.
path="*"라고 설정하면 글로벌 에러 메시지와 필드의 모든 에러 메시지가 함께 출력.

delimiter

에러 메시지가 하나 이상일 때 사용할 구분자를 지정하기 위해 사용.
디폴트는 <br/>태그 이다.

cssClass

에러 메시지는 항상 바인딩 오류가 있을 때만 출력된다. 따라서 CSS 클래스는 하나만 지정하면 된다.
에러가 없으면 출력이 되지 않음

입력 필드와 함께 출력

가장 자주 사용되고 사용자가 보기에 편리한 방법. 입력받는 필드 바로 뒤나 아래에 메시지를 출력.
❗ <form:input>에 이어서 같은 path의 <form:errors>가 붙은 경우
- 이때는 다음과 같이 path를 이용해서 구체적인 필드를 지정해줘야 한다.

<form:input path="name" /><form:errors path="name" cssClass="errorMessage"/>

상단 또는 하단에 일괄 출력

때로 모든 에러 메시지를 폼의 상단 또는 하단에 일괄적으로 출력하는 방식을 선호하는 경우가 존재.
path="*"를 이용해 모든 에러 메시지를 한 번에 출력 가능

<form:form commandName="user">
	<div class="errors"><form:errors path="*" /></div>
    <fieldset>
    	<form:input path="name" />
        ...

◼ <form:hidden>

<input type="hidden"> 태그를 작성. HTML에 히든 필드를 직접 넣어야 할 경우가 있을 때 사용. 기본적으로 다음과 같이 path만 지정해주면 된다.

<form:hidden path="loginCount" />

◼ <form:password>, <form:textarea>

각각 HTML <input type="text">와 <textarea> 태그를 생성한다. 기본적인 사용 방법은 <form:input>과 동일하다.
path만 디폴트 애트리뷰트이며, 대응되는 HTML 태그의 표준 애트리뷰트를 사용할 수 있다. cssClass, cssErrorClass 두 가지를 이용해서 바인딩 에러가 있을 때와 없을 때의 CSS 클래스를 각각 지정할 수 있다.

◼ <form:checkbox>, <form:checkboxes>

HTML의 <input type="checkbox"> 태그를 만들어준다. 자동으로 필드마커가 붙은 히든 필드를 등록해주어 편리함.
필수 애트리뷰트는 path뿐이다. 하지만 체크박스에 대한 설명을 담은 label을 함께 사용하는 경우가 일반적이다.

◼ <form:radiobutton>, <form:radiobuttons>

HTML <input type="radio"> 태그를 생성. 기본적인 사용방법은 <form:checkbox>와 비슷하다. 다만 라디오버튼은 단일 선택이므로, 각각을 구분할 수 있는 값을 명확히 지정해줄 필요가 있다.

◼ <form:select>, <form:option>, <form:options>

HTML의 <select>와 <option>을 생성해준다. <form:select>와 <form:option>은 <form:radio>와 사용 방법이 비슷하다. 라디오버튼 대신 드롭다운 박스나 선택창이 나타나고, path를 <form:select>에 따로 지정한다는 정도가 다를 뿐이다.
목록을 수동으로 지정하려면 <form:option>을 이용하면 된다.
<option>목록을 뷰에서 지정하지 않고 맵이나 리스트를 이용해 자동생성하려면 <form:options>를 사용하면 된다.

◼ 커스텀 UI 태그 만들기

스프링의 spring 태그 라이브러리든 form 태그 라이브러리든 그 자체로 사용하기 보다는 다시 애플리케이션의 UI 스타일에 따라 커스텀 태그를 만들어 사용하면 편리
form 태그는 상대적으로 간결하지만 일정한 사용 패턴이 있다면 다시 태그화하는 것이 편리

EX)
텍스트 입력을 받는 필드의 전형적인 form 태그 사용 방법

<p>
	<form:label path="name" >Name</form:label>:
    <form:input path="name" size="20"/>
    <form:errors path="name" cssClass="errorMessage"/>
</p>

동일한 스타일의 텍스트 입력 창이라면 위 코드에서 바뀌는 부분만 애트리뷰트로 지정해서 다음과 같이 간결한 커스텀 태그 하나로 만들 수 있음

<ui:text path="name" label="Name" size="20" />

📖 메시지 컨버터와 AJAX

메시지 컨버터XML이나 JSON을 이용한 AJAX 기능이나 웹 서비스를 개발할 때 사용 가능
- HTTP 요청 메시지 본문HTTP 응답 메시지 본문통째로 메시지로 다루는 방식
- 파라미터의 @RequestBody메소드에 부여한 @ResponseBody를 이용해서 사용 가능

메시지 방식의 컨트롤러를 사용하는 방법은 두 가지

  • GET 사용 : 요청정보가 URL과 쿼리스트링으로 제한되므로 @RequestParam이나 @ModelAttribute로 요청 파라미터를 전달 받음.
  • POST 사용 : HTTP 요청 본문이 제공되므로 @RequestBody 사용 가능

📝 메시지 컨버터의 종류

사용할 메시지 컨버터는 AnnotationMethodHandlerAdapter를 통해 등록. 일반적으로 하나 이상의 메시지 컨버터를 등록해두고 요청 타입이나 오브젝트 타입에 따라 선택되게 한다.

AnnotationMethodHandlerAdapter에 등록되는 디폴트 메시지 컨버터를 알아보자.

ByteArrayHttpMessageConverter

지원하는 오브젝트 타입은 byte[] 다. 미디어 타입은 모든 것을 다 지원한다. 따라서 @RequestBody로 전달받을 때는 모든 종류의 HTTP 요청 메시지 본문을 byte 배열로 가져올 수 있다. 반대로 @ResponsBody로 보낼 때는 콘텐트 타입이 application/octet-stream으로 설정된다.

StringHttpMessageConverter

지원하는 오브젝트 타입은 String이다. 미디어 타입은 모든 종류를 허용한다. 따라서 TTP 요청의 본문을 그대로 스트링으로 가져올 수 있다.
HTTP가 기본적으로 텍스트 기반의 포맷이므로 가공하지 않은 본문을 직접 받아서 사용하고 싶은 경우 유용

FormHttpMessageConverter

미디어 타입이 application/x-www-form-urlencoded로 정의된 폼 데이터를 주고받을 때 사용할 수 있다. 오브젝트 타입은 MultiValueMap<String, String>을 지원한다. HTTP 요청의 폼 정보는 @ModelAttribute를 이용해 바인딩 하는 것이 훨씬 편리하고 유용하므로 @RequestBody에는 잘 사용하지 않음.
MultiValueMap : 맵의 값이 List 타입인 맵.

SourceHttpMessageConverter

미디어 타입은 application/xml, application/*+xml, text/xml 세가지를 지원
오브젝트 타입은 javax.xml.transform.Source 타입인 DOMSource,SAXSource, StreamSource 세 가지를 지원
XML 문서를 Source 타입의 오브젝트로 전환하고 싶을 때 유용하게 쓸 수 있음.

기본적으로 위의 네 가지 HttpMessageConverter가 디폴트로 등록되지만, 이보다는 디폴트로 등록되지 않은 다음 세 가지가 실제로 더 유용함. 사용하고 싶다면 직접 AnnotationMethodHandlerAdapter 빈의 messageConverters 프로퍼티에 등록하고 사용해야 한다.

Jaxb2RootElementHttpMessageConverter

JAXB2의 @XmlRootElement와 @XmlType이 붙은 클래스를 이용해서 XML과 오브젝트 사이의 메시지 변환을 지원.
기본적으로 SourcehttpMessageConverter와 동일한 XML 미디어 타입을 지원. 오브젝트는 두 가지 애노테이션 중 하나가 적용됐다면 어떤 타입이든 사용 가능.

MarshallingHttpMessageConverter

스프링 OXM 추상화의 Marshaller와 Unmarshaller를 이용해서 XML 문서와 자바 오브젝트 사이의 변환을 지원해주는 컨버터다. 빈으로 등록 시에 프로퍼티에 marshaller와 unmarshaller를 설정해줘야 한다. 미디어 타입은 다른 XML 기반 메시지 컨버터와 동일하며, 지원 오브젝트는 unmarshaller의 supports() 메소드를 호출해서 판단.
단, Marshaller의 개수만큼 MarshallingHttpMessageConverter를 등록해줘야 하는 것이 조금 번거로울 수 있다.

MappingJacksonHttpMessageConverter

Jackson ObjectMapper를 이용해서 자바오브젝트와 JSON 문서를 자동 변환해주는 메시지 컨버터다. 지원 미디어 타입은 application/json이다. 자바오브젝트 타입에 제한은 없지만 프로퍼티를 가진 자바빈 스타일이거나 HashMap을 이용해야 정확한 변환 결과를 얻을 수 있다.
Jackson 프로젝트의 ObjectMapper가 대부분의 자바 타입을 무난히 JSON으로 변환해주지만 날짜나 숫자 등에서 포맷을 적용하는 등의 부가적인 변환 기능이 필요하다면 ObjectMapper를 확장해서 적용할 수 있다.

여타 전략과 마찬가지로 전략 프로퍼티를 직접 등록하면 디폴트 전략은 자동으로 추가되지 않음을 유의하자.

◼ JSON을 이용한 AJAX 컨트롤러 : GET + JSON

메시지 컨버터를 이용해 간단한 JSON 애플리케이션을 만들어보자
AJAX는 비동기 자바스크립트와 XML의 약자(Asynchronous JavaScript and XML)자바 스크립트를 이용해 서버와 비동기 방식의 통신을 해서 웹 페이지를 갱신하지 않은 채로 여러 가지 작업을 수행하는 프로그래밍 모델을 말한다. 최근에는 서버 측 라이브러리 지원에 힘입어 JSON(JavaScript Object Notation, 자바 스크립트의 오브젝트를 표현하는 표기법)이 AJAX의 인기 메시지 포맷으로 자리 잡았다.
JSON 기반의 AJAX를 지원하려면 컨트롤러는 결과를 JSON 포맷으로 만들어서 돌려줘야 한다.

스프링 MVC라면 두 가지 방법이 있다.

  • JSON 지원 뷰를 사용
    - 모델에 JSON으로 변환할 오브젝트를 넣고 MappingJacksonJsonView 뷰를 선택
    - JSON과 다른 포맷의 뷰를 동시에 사용할 경우, .json 확장자를 자동인식하는 ContentNegotiatingViewResolver를 사용하면 편리

    * @ResponseBody를 이용
    - 콘텐트교섭을 통해 JSON 뷰를 결정하는 것이 아니고, 항상 JSON으로 고정됐다면 @ResponseBody를 이용하는 편이 훨씬 간결.

AJAX 요청을 보내는 방법은 단순 GET 방식과 POST 방식으로 구분할 수 있고, POST는 다시 일반 폼을 보내는 것과 JSON 메시지를 보내는 것으로 구분 가능.

먼저 GET 방식의 단순 요청을 받아서 JSON으로 결과를 보내는 기능을 구현해보자.
사용자 등록 화면에서 폼을 submit하기 전에 입력한 로그인 아이디가 이미 등록된 것인지를 확인하는 기능을 만들어보자. 이때, AJAX를 사용해서 화면 갱신 없이 로그인 아이디 중복 여부만 먼저 확인하도록 만들면 좋다.

아이디 폼 필드

<label>로그인 아이디 : </label>
<input id="loginid" name="loginid" type="text" />
<input id="loginidcheck" type="button" value="아이디 중복검사" />

아이디 중복검사 버튼을 누르면 AJAX 방식으로 서버에 요청을 보내서 입력된 아이디가 이미 존재하는지 확인한다. 서버에 보낼 정보는 로그인 아이디 하나 뿐이다.
로그인 아이디 중복을 검사하는 URL이 /user/checkloginid/라면 다음과 같은 URL이 만들어진다.

/user/checkloginid/ceoahn

URL에서 로그인 아이디를 파라미터로 받을 수 있도록 컨트롤러 메소드를 정의한다.

// {loginId}에 해당하는 부분을 loginId 파라미터로 받을 수 있음!
@RequestMapping(value="chekloginid/{loginId}", method=RequestMethod.GET)
public Result checklogin(@PathVariable String loginId) {

LoginService나 UserService 등의 서비스 계층 오브젝트에 요청해서 로그인 아이디의 중복 여부를 확인함.
간단히 중복 여부만 알려줄 수도 있지만, 중복된 아이디인 경우 사용가능한 아이디를 함께 제공해주면 좋을 것 같다. 따라서 중복 여부와 사용가능 아이디 정보를 담은 클래스를 정의하자.

public class Result {
	boolean duplicated;
    String availableId;
    // getter, setter 생략
}

Result 오브젝트에 결과를 담은 뒤 이를 다시 JSON으로 변환해서 클라이언트로 보내야 한다. 가장 간단한 메시지 컨버터를 이용해보자.

<bean class=
"org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
	<property name="messageConverters">
    	<list>
        	<bean class=
"org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" />
        </list>
    </property>
</bean>

컨트롤러 메소드에 @ResponseBody를 붙여준다

@RequestMapping(value="chekloginid/{loginId}", method=RequestMethod.GET)
@ResponseBody
public Result checklogin(@PathVariable String loginId) {

이렇게 해주면 컨트롤러가 리턴하는 Result 오브젝트는 JSON 포맷의 메시지로 변환될 것이다.
아이디 중복 확인과 그 결과에 따라 Result 오브젝트를 만들고 결과를 리턴하는 checklogin() 메소드의 나머지 코드를 작성한다. 추천 아이디를 생성하는 방식은 다양할 것이지만, 여기서는 난수를 요청한 아이디에 추가하는 방식을 사용하겠다.

//checkLogin() 메소드 후반부
Result result = new Result();

	if(userService.isRegisteredLoginId(loginId)) { //등록된 아이디이면
    	result.setDuplicated(true);
        result.setAvailableId(loginId + (int)(Math.random() * 1000)); //난수 추가
    }
    else { // 사용가능한 아이디이면
    	result.setDuplicated(false);
    }
    return result; 
}

이미 등록된 아이디라면 Result 오브젝트는 true와 아이디+난수로 이루어진 프로퍼티 값을 갖게 될 것이다. 이 오브젝트는 @ResponseBody 설정에 따라 MappingJacksonHttpMessageConverter에 넘겨지고, JSON 메시지로 만들어져 HTTP 응답 메시지 본문으로 설정되어 클라이언트로 보내짐

JSON 메시지
로그인 아이디로 ceoahn을 입력했지만, 중복이여서 난수를 추가하여 프로퍼티로 받음
{"duplicated":true, "availableId":"ceoahn930"}

이제 남은 것은 클라이언트에서 자바스크립트를 사용해 위의 URL을 호출해주고 그 결과를 받아서 사용자에게 알려주는 코드를 만드는 일이다. AJAX의 클라이언트 코드를 만드는 방법은 다양하다. 자바 스크립트로만 만들면 코드가 복잡해지지만, 자바 스크립트 라이브러리를 활용하면 훨씬 간결하게 AJAX 코드 작성 가능

<script>
$(document).ready(function() {
	$('#loginIdcheck').click(function() {
    	$.getJSON('checkloginId/' + $('#loginid').val(), function(result) {
          	if(result.duplicated == true) {
            	alert('이미 등록된 로그인 ID 입니다.' + result.availableId +
                      '는 사용할 수 있습니다');
            }
          	else {
            	alert('사용할 수 있는 로그인 ID 입니다.');
            }
        });
    });
});
</script>

◼ JSON을 이용한 AJAX 컨트롤러 : POST(JSON) + JSON

다음으로 POST 메소드를 사용하고 본문에 JSON으로 된 정보를 보내는 방법을 살펴보자.
JSON으로 전달되는 요청은 MappingJacksonHttpMessageConverter를 통해 @RequestBody가 붙은 파라미터로 변환되어 받을 수 있음
간단한 입력 폼이라면 AJAX를 이용해 등록 기능이 동작하도록 만들 수 있다.

먼저 사용자 정보를 입력받는 HTML 폼을 작성한다

<form id="user">
	<fieldset>
  		<label>로그인 아이디 : </label><input id="loginid" name="loginid"
           type="text" />
       	<input id="loginidcheck" type="button" value="아이디 중복검사" /> <br/>
       	<label>비밀번호 : </label><input id="password" name="password"
           type="password" /><br/>
      	<label>이름 : </label><input id="name" name="name" type="text" /> <br/>
      	<input type="submit" value=" 등록 " />
  	</fieldset>
</form>

등록 버튼을 누르면 폼의 모든 필드정보를 JSON 메시지로 만들어서 POST로 전송하도록 만든다. 폼의 submit 버튼을 이용하지만 실제 폼이 submit 되지는 않도록 이벤트에서 false를 리턴

$('#user').submit(function() {
  	//폼의 모든 입력 필드를 JSON 포맷의 메시지로 만듦
	var user = $(this).serializeObject(); 
  	$.postJSON("register", user, function(user) {
    	//등록완료 안내 또는 에러 메시지 출력
      	...
    });
    return false;
});

이 자바스크립트 코드에 의해 서버로 전송되는 HTTP 요청의 메시지 본문은 JSON 포맷으로 만들어짐

{"loginid":"ceoahn","password":"helloworld","name":"Ahn Young Hoe"}

이제 서버 코드를 살펴보자. URL은 /user/register이고, POST 메소드를 처리하는 컨트롤러 메소드를 추가한다. 메소드에는 @RequestBody가 붙은 User타입 파라미터를 넣는다.
application/json 콘텐트 타입으로 전달되는 요청이므로 MappingJacksonHttpMessageConverter에 의해 User 타입 오브젝트로 변환된다.
JSON의 각 요소와 일치하는 프로퍼티에 자동으로 바인딩될 것이다.

@RequestMapping(value="/register", method=RequestMethod.POST)
@ResponseBody
public User registerpost(@RequestBody User user) {
	// user 검증과 등록 작업
    ...
    return user;
}

📖 MVC 네임스페이스

스프링 3.0에 대거 도입된 @MVC 관련 기능을 제대로 활용하려면 디폴트로 등록되는 AnnotationMethodHandlerAdapter의 설정으로는 부족하다. 애노테이션 방식의 포맷터를 사용하는 바인딩 기능이나, JSR-303의 빈 검증기능, 메시지 컨버터 등을 제대로 활용하려면 AnnotationMethodHandlerAdapter를 빈으로 등록하고 여러가지 프로퍼티 설정을 해줄 필요가 있다.

스프링은 최신 @MVC기능을 손쉽게 등록할 수 있게 해주는 mvc스키마의 전용 태그를 제공한다. 스프링의 기본 @MVC 설정을 적용할 경우, mvc 스키마의 태그를 활용하면 설정이 간결해지고 편리할 것이다.

◼ <mvc:annotation-driven>

DispatcherServlet 전략 빈을 자동으로 등록해준다. 또한, 최신 @MVC 지원 기능을 제공하는 빈도 함께 등록하고 전략 빈의 프로퍼티로 설정해준다. 라이브러리의 존재 여부를 파악해서 자동으로 관련 빈을 추가해주는 기능도 제공된다.

DefaultAnnotationHandlerMapping

가장 우선으로 적용되도록 @RequestMapping을 이용한 핸들러 매핑 전략을 등록
다른 디폴트 핸들러 매핑 전략은 자동 등록되지 않는다는 점을 기억해두자.

AnnotationMethodHandlerAdapter

DispatcherServlet이 자동으로 등록해주는 디폴트 핸들러 어댑터. 디폴트 설정을 변경하려면 빈으로 등록해야 한다. <mvc:annotation-driven>은 기본적으로 이 AnnotationMethodHandlerAdapter를 빈으로 등록한다.
❗ 따라서 <mvc:annotation-driven>을 사용했을 때는 직접 빈으로 등록해서는 안된다.

ConfigurableWebBindingInitializer

모든 컨트롤러 메소드에 자동으로 적용되는 WebDataBinder 초기화용 빈을 등록하고 AnnotationMethodHandlerAdapter의 프로퍼티로 연결해준다.
기본적으로 컨버전 서비스는 @NumberFormat과 같은 애노테이션 방식의 포맷터를 지원하는 FormattingConversionServiceFactoryBean이 등록됨.
글로벌 검증기는 LocalValidatorFactoryBean으로 설정된다. 따라서 JSR-303의 검증용 애노테이션 기능이 자동으로 제공된다.
❗ 단, 이 기능이 적용되려면 JSR-303 지원 라이브러리가 클래스패스에 등록되어 있어야한다.

메시지 컨버터

AnnotationMethodHandlerAdapter의 messageConverters 프로퍼티로 메시지 컨버터들이 등록됨.
네 개의 디폴트 메시지 컨버터와 함께 Jaxb2RootElementHttpMessageConverter와 MappingJacksonHttpMessageConverter가 추가로 등록된다.
❗ 단, 각각 JAXB2와 Jackson 라이브러리가 클래스패스에 존재하는 경우에만 등록

<spring:eval>을 위한 컨버전 서비스 노출용 인터셉터

기본적으로 표준 컨버터를 이용해서 모델의 프로퍼티 값을 JSP에 출력할 문자열로 변환한다. 하지만, <mvc:annotation-driven>을 등록해주면 <spring:eval>에서 컨버전 서비스를 이용할 수 있음
ConfigurableWebBindingInitializer에 등록되는 것과 동일한 컨버전 서비스를 인터셉터를 이용해서 <spring:eval>에서 사용할 수 있도록 제공해주는 기능이 자동으로 추가

<mvc:annotation-driven> 이 자동으로 등록해주는 몇 가지 설정정보는 애트리뷰트를 통해 바꿀 수 있음. 다음과 같은 애트리뷰트를 사용해 등록해주면 된다.

validator

자동등록되는 ConfigurableWebBindingInitializer의 validator 프로퍼티에 적용할 Vaildator 타입의 빈을 지정할 수 있다.
모든 컨테이너에 일괄 적용하는 검증기는 디폴트로 추가되는 LocalValidatorFactoryBean으로 충분하다. 하지만 확장이나 재구성을 위해서는 빈으로 등록해줄 필요가 있는데, 이때 validator 애트리뷰트를 이용해 등록해준다.

<mvc:annotation-driven validator="myValidator" />
<bean id="myValidator" class="MyLocalValidatorFactoryBean">
	//프로퍼티 설정
</bean>

conversion-service

ConfigurableWebBindingInitializer의 conversionService 프로퍼티에 설정될 빈을 직접 지정할 수 있다. 직접 개발한 컨버터나 포맷터를 적용하기 위해서 ForattingConversionServiceFactoryBean을 빈으로 직접 등록하고 재구성해줘야 하는데 이때 conversion-service 애트리뷰트를 이용해 다음과 같이 설정한다.

<mvc:annotation-driven conversion-service="myConversionService" />
<bean id="myConversionService" class="FormattingConversionServiceFactoryBean">
	<property name="converters">
    ...
    </property>
</bean>

<mvc:annotation-driven>은 매우 빠르고 간편하게 @MVC의 주요 빈을 설정하고 최신 기능을 사용하게 해줌. 하지만 검증기와 컨버전 서비스를 제외하면 기본적으로 등록되는 빈의 설정을 변경할 수 없다. AnnotationMethodHandlerAdapter와 DefaultAnnotationHandlerMapping 등의 설정을 변경해야 할 때는
<mvc:annotation-driven>을 사용할 수는 없다.

◼ <mvc:interceptors>

HandlerInterceptor의 적용 방법은 두 가지가 존재

  • 핸들러 매핑 빈의 interceptors 프로퍼티를 이용해 등록하는 방법
    - 인터셉터 등록을 위해 핸들러 매핑 빈을 명시적으로 빈으로 선언해줘야 한다
    - 핸들러 매핑이 여러 개라면 인터셉터를 핸들러 매핑마다 인터셉터를 반복해서 설정해줘야 한다.
  • <mvc:interceptors>를 이용
    - 모든 핸들러 매핑에 일괄 적용되는 인터셉터를 한 번에 설정 가능. 인터셉터를 등록하려고 디폴트 핸들러 매핑 빈을 설정파일에 등록해주지 않아도 됨.
    - URL 패턴을 지정할 수 있음. 경로가 일치하는 요청에만 자동으로 인터셉터가 적용되도록 할 수 있음.

<mvc:interceptors>를 빈으로 등록

<mvc:interceptors>
	<bean class="...MyInterceptor" />
</mvc:interceptors>

특정 패턴의 URL에만 인터셉터 적용하기

<mvc:interceptors>
	<mvc:interceptor>
    	<mvc:mapping path="/admin/*" />
        <bean class="...AdminInterceptor" />
    </mvc:interceptor>
</mvc:interceptors>

◼ <mvc:view-controller>

컨트롤러가 뷰를 지정하는 것만 하는 경우, 굳이 이런 기능을 위해 컨트롤러를 만드는건 번거로운 일이다. 그렇다고 JSP와 같은 뷰를 직접 사용자가 접근할 수 있도록 노출하는 것은 바람직하지 않다.
바로 이럴 때 <mvc:view-controller> 태그를 사용한다. 이 태그에 매핑할 URL 패턴과 뷰 이름을 넣어주면 해당 URL을 매핑하고 뷰 이름을 리턴하는 컨트롤러가 자동으로 등록된다.

// /URL의 요청을 받았을 때, 뷰 이름으로 /index를 돌려주는 컨트롤러 등록
<mvc:view-controller path="/" view-name="/index" />

<mvc:view-controller>를 하나라도 등록하면 SimpleUrlHandlerMapping과 SimpleControllerHandlerAdapter를 자동으로 등록한다. 두 개의 전략을 빈으로 직접 등록하더라도 등록된 빈을 활용하므로, 중복 등록의 문제는 없다.

📖 @MVC 확장 포인트

스프링이 제공하는 세부적인 기능을 확장하고 싶다면 스프링이 제공하는 확장 포인트를 이용할 수 있다. 여기서는 애노테이션 방식의 컨트롤러를 만들 때 적용되는 기능을 확장할 수 있도록 준비된 확장 포인트 중에서 유용한 몇 가지만을 소개하겠다.

📝 AnnotationMethodHandlerAdapter

확장 포인트가 가장 많은 전략. 이미 WebBindingInitializer와 HttpMessageConverter를 활용하는 방법은 설명했다. 그 외에도 다음과 같은 인터페이스를 구현한 확장 기능을 적용할 수 있다.

◼ SessionAttributeStore

@SessionAttribute에 의해 지정된 모델은 자동으로 HTTP 세션에 저장됐다가 다음 요청에서 사용 가능. 정확히는 SessionAttributeStore 인터페이스의 구현 클래스에 의해 저장됐다가 다시 참조할 수 있는 것이다.
SessionAttributeStore의 구현 클래스가 HTTP 세션을 이용하기 때문에 기본적으로 HTTP 세션에 저장된다.

HTTP 세션은 잘 다루지 않으면 웹 애플리케이션 메모리 누수의 원인이 됨. 적절하게 SessionStatus의 setComplete()을 호출해주지 않으면, 필요없는 모델 오브젝트가 메모리에 계속 남게되어 메모리를 잠식하게 된다.
또 다른 문제는 클러스터링을 통해 한 대 이상의 서버를 동시에 적용한 경우이다. 서버 다운 등으로 사용자가 다른 서버로 연결돼도 HTTP 세션정보를 유지하기 위해 HTTP 세션정보를 서버 사이에 복제하는 작업이 필요 - 상당한 부담.

따라서 고성능을 요구하면서 대규모의 사용자를 처리해야 하는 서버라면 아예 @SessionAttribute를 적용하지 않아서 HTTP 세션의 사용을 피하는 방법을 선택할 수 밖에 없음. 또는 더 효율적이고 빠른 방식으로 세션정보를 저장하도록 만들어야 한다.

이런 경우 SessionAttributeStore 인터페이스를 구현해서 세션정보를 저장하는 방법을 바꿀 수 있다. 또는 HTTP 세션에 저장하지만 세션 타임아웃이 되기 전에라도 일정 시간 저장된 모델 오브젝트를 자동으로 삭제하게 할 수도 있다.

◼ WebArgumentResolver

컨트롤러 메소드의 파라미터로 사용할 수 있는 타입과 애노테이션의 종류는 20여 가지나 된다. 하지만 필요하다면 이를 더 확장할 수 있는데, 이때 사용하는 것이 바로 WebArgumentResolver다. 이 인터페이스를 구현하면 애플리케이션에 특화된 컨트롤러 파라미터 타입을 추가할 수 있다.

EX) 로그인 사용자에 대한 정보가 암호화되어 쿠키에 encodedUserInfo라는 이름으로 저장되어있는 경우

로그인 사용자를 가져오려면 쿠키에서 값을 꺼내서 사용자 정보로 다시 변환해주는 라이브러리를 호출해야 함.

@Autowired UserService userService;
...
public void add(@CookieValue String encodedUserInfo) {
	User currentUser = userService.decodedUserInfo(encodedUserInfo);
}

매번 코드를 통해 사용자 정보를 가져오는 작업이 번거롭게 느껴진다면, WebArgumentResolver를 이용쿠키 값으로부터 사용자 정보를 가져와 메소드 파라미터로 전달받도록 만들 수 있다. WebArgumentResolver는 다음과 같은 매우 단순한 메소드 하나만 구현하면 된다.

package org.springframework.web.bind.support;
...
public interface WebArgumentResolver {
	Object UNRESOLVED = new Object();
    Object resolveArgument(MethodParameter methodParameter,
    	NativeWebRequest webRequest) throws Exception;
}

◼ ModelAndViewResolver

컨트롤러 메소드의 리턴 타입과 메소드 정보, 애노테이션 정보 등을 참고해서 ModelAndView를 생성해주는 기능을 만들 수 있음.
스프링은 7가지 정도의 리턴 방식을 지원함. 여기에 더해서 특별한 타입의 리턴 값 또는 메소드 레벨의 애노테이션 등을 이용해 ModelAndView를 생성하는 방법 추가 가능

package org.springframework.web.servlet.mvc.annotation;
...
public interface ModelAndViewResolver {
	ModelAndView UNRESOLVED = new ModelAndView();
    ModelAndView resolveModelAndView(Method handlerMethod, Class
    	handlerType, Object returnValue, ExtendedModelMap implicitModel,
        NativeWebRequest webRequest);
}

//implictModel : @ModelAttribute 파라미터처럼 
//						스프링이 자동으로 추가해둔 모델 오브젝트가 담겨 있는 맵

📖 URL과 리소스 관리

이 절에서 소개할 기능은 스프링 3.0.4 또는 그 이후 버전에서만 지원

📝 <mvc:default-servlet-handler/>를 이용한 URL 관리

◼ 디폴트 서블릿과 URL 매핑 문제

최근에는 확장자 없이 웹페이지 URL을 작성하는 경우가 많음.
- 확장자가 없는 URL을 사용하는 경우에는 다음과 같이 특정 경로 아래의 내용을 모두 매핑하는 방식 사용

<servlet-mapping>
	<servlet-name>spring</servlet-name>
  	<servlet-name>/app/*</servlet-name>
</servlet-mapping>

파라미터조차 URL에 포함시켜 깔끔한 URL을 만들고 싶다면 www.myweb.com/app/user/1보다 www.myweb.com/user/1이 낫다.

<servlet-mapping>
	<servlet-name>spring</servlet-name>
  	<servlet-name>/</servlet-name>
</servlet-mapping>

하지만 위의 URL은 다른 문제가 발생한다.

서블릿 컨테이너는 /로 시작하는 모든 URL을 모두 DispatcherServlet이 처리하는 것으로 기대하고 DispatcherServlet에 전달한다. 서블릿 컨테이너는 확장자에 특별한 의미를 두지 않으므로 /를 매핑한 DispatcherServlet에는 /index.html이나 /js/jquery.js, /css/theme/default.css와 같은 URL 요청도 모두 전달된다.
DispatcherServlet에는 이러한 URL을 처리하는 핸들러가 없기 때문에 404에러가 발생한다.

서블릿 컨테이너는 개별 웹 애플리케이션의 web.xml에 앞서 서블릿 컨테이너의 디폴트 web.xml을 적용한다. 서블릿 컨테이너의 디폴트 web.xm에는 jsp 확장자로 끝나는 JSP 페이지를 처리하는 JSP 서블릿과 정적 리소스를 처리하는 디폴트 서블릿이 정의되어 있음.

JSP 서블릿은 jsp나 jspx 확장자를 가진 모든 요청을 JspServlet으로 보내주고, 다른 서블릿 매핑에서 처리되지 않은 모든 나머지 URL은 가장 우선순위가 낮은 /에 매핑된 DefaultServlet이 처리한다. 그래서 웹 애플리케이션에 별다른 설정을 하지 않더라도 이미지나 js파일 같은 정적인 내용도 서블릿 컨테이너가 처리 가능
그런데, 위와 같이 서블릿을 /에 매핑해버리면 DispactherServlet이 DefaultServlet보다 우선적으로 처리함.

이 문제를 해결하는 대표적인 방법으로 UrlRewriteFilter가 존재.
패턴에 따라 URL을 변경할 수 있는 기능을 가진 서블릿 필터.

ex) /user/1 이라는 요청 -> /app/user/1로 변경해서 서블릿에 전달
대신 패턴을 사용하지 않을 나머지 정적 리소스는 /resource 같은 특정 폴더에 몰아두기

◼ <mvc:default-servlet-handler/>

스프링 3.0.4 부터는 위와 같이 UrlRewriteFilter를 사용하지 않고, 스프링이 제공하는 디폴트 서블릿 핸들러를 이용해 문제를 해결할 수 있다.

<servlet-mapping>
	<servlet-name>spring</servlet-name>
  	<servlet-name>/</servlet-name>
</servlet-mapping>

<mvc:default-servlet-handler/> <!--이 한 줄만 추가하면 된다-->

위와 같이 한 줄만 추가하면 @MVC 컨트롤러가 /로 시작하는 URL을 자유롭게 사용할 수 있음. 동시에 정적인 리소스는 서블릿 컨테이너가 제공하는 디폴트 서블릿이 처리

<mvc:default-servlet-handler/>을 넣더라도 DispatcherServlet으로 모든 URL이 매핑되는 것은 바뀌지 않지만, DispatcherServlet에 정적 리소스 파일에 대한 요청을 디폴트 서블릿으로 포워딩하는 기능이 추가됨.

  1. DispatcherServlet이 요청을 받으면 @RequestMapping의 요청조건이 맞는지 확인
  2. 요청을 처리할 핸들러를 찾지 못하면 정적 리소스라고 판단 후 디폴트 서블릿으로 위임

◼ <url:resource>를 이용한 리소스 관리

js나 css파일 또는 UI 관련 공통 리소스는 별도로 패키징하고 여러 프로젝트에서 이를 가져다 쓰도록 하기가 어렵다.
- 스프링 3.0.4에서 추가된 <url:resource>를 사용하면 정적 파일로 구성된 웹 리소스도 쉽게 모듈화해서 사용 가능

UI 프레임워크의 리소스가 URL을 기준으로 /ui 폴더 아래 있다고 가정하고, 이 폴더는 프로젝트의 웹 콘텐트 아래 있어야 한다. 이클립스 WTP의 Dynamic Project 메뉴를 이용해 웹 프로젝트를 만들었다면 프로젝트 폴더의 /WebContent가 웹 콘텐트의 기준이 된다. Maven의 표준 디렉토리 관례를 따라 만들었다면 /src/main/webapp이 기준 폴더이다.

이때, 프로젝트 안에서 위치가 일정하게 유지돼야 하는 정적 파일의 일부를 모듈화해서 가져오려면 어떻게 해야 할까?

디폴트 서블릿 대신 모듈화된 리소스를 다루는 별도의 서블릿을 만들어서 처리하게 하면 된다. 이런 서블릿을 이용해 /ui 폴더의 내용을 웹 콘텐트 폴더 아래가 아닌 클래스 패스에서 가져오게 한다거나, 서버의 로컬 폴더에서 읽게 한다거나, 별도의 원격 서버에서 가져오도록 구성하면 된다.
즉, 리소스의 위치나 패키징 방법에 상관없이 항상 일정한 URL에 매핑해주는 기능을 제공해주면 된다.

모듈화하기 가장 좋은 방법은 jar 파일로 패키징하는 것이다. jar 파일에는 자바 클래스가 담기는 것이 원칙이지만, 만약 클래스 파일이 아닌 메타정보나 리소스가 있다면 /META-INF에 다음과 같은 구조로 넣어주면 된다.

META-INF
	+--webresources
    		+--ui
            	+--js
                +--css
                +--theme

jar파일로 패키징한 뒤에 이를 사용할 웹 프로젝트의 라이브러리 폴더에 넣고, 서블릿 컨텍스트용 XML안에 다음 한 줄만 넣어주면 된다.

<mvc:resources mapping="/ui/**" location="classpath:META-INF/webresources/" />

/ui라는 요청이 들어왔을 때, 이를 클래스패스/META-INF/webresources 아래의 파일로 매핑해주는 핸들러가 등록됨.
location 애트리뷰트에는 스프링의 리소스를 기술하는 어떤내용이 나와도 좋다.
http:를 쓰면 네트워크를 통해 다른 서버의 내용을 가져올 수 있고, 접두어를 붙이지 않으면 서블릿 컨텍스트 리소스가 되므로 웹 콘텐트 폴더의 내용을 그대로 매핑하게 해준다.

<mvc:resources>는 304 응답 기능이 존재
- 정적 리소스에 대한 요청이 반복될 때 리소스 내용이 바뀌지 않았다는 응답 코드

<mvc:resources>가 모든 정적 리소스를 담당하게 한다면 굳이 디폴트 서블릿이 사용될 필요가 없으니 <mvc:default-servlet-handler>를 사용할 필요가 없다.
하지만, 일부 모듈화가 필요한 리소스만 <mvc:resources>로 매핑하고 해당 애플리케이션에만 사용되는 웹 리소스는 디폴트 서블릿이 담당하게 한다면 <mvc:default-servlet-handler>를 함께 사용하는 편이 편리
- <mvc:resources>가 <mvc:default-servlet-handler>보다 우선순위가 높으므로 <mvc:resources>로 매핑이 안된 요청만 디폴트 서블릿으로 포워딩될 것이다.

3편에서 계속...

profile
모르는 것 정리하기

0개의 댓글