스프링 MVC에서 지원하는 @Valid를 통한 데이터 검증은 정말 놀랍다. 특히 브라우저에서 클라이언트가 입력자료를 넘겨줄 때 이 자료를 검증할 수 있는 모델을 매우 손쉽게 만들 수 있다는 점이다. @Valid는 스프링이 만든 기술은 아니며 최근 JSR-303이란 이름으로 채택된 서블릿 2.3 표준스펙 중 하나인데 매번 그렇듯 스프링은 이 새로운 표준을 확장하고 쉽게 사용할 수 있도록 스프링만의 방식으로 재편성해주었다. 


@Valid가 간소화 될 수 있었던 배경을 이해하고 응용할 수 있게끔 학습하는게 중요하겠지만 먼저 @Valid가 얼마나 대단한지 보여주기 위해 맛보기로 간단한 @Valid 예제를 살펴보고자 한다. 먼저 자바빈 객체를 하나 만들어보자.

public class User {

@Size(min=5, max=50) private String id;
@Size(min=5, max=50) private String password;
@Pattern(regexp="^[_0-9a-zA-Z-]+@[0-9a-zA-Z]+(.[_0-9a-zA-Z-]+)*$")
private String email;

… get/set 생략 …
}

그리고 이 JSP파일을 처리할 수 있는 컨트롤러를 하나 만들어 보자. 최초에 문서를 불러들이는 Get과 송신을 위해 필요한 Post, 2가지가 필요하다. 그리고 세션으로 상태유지를 할 수 있게끔 해당 요소를 @SessionAttributes로 공유해주는 것까지 해주도록 하자.

이 몇줄의 코드를 설명시키기 위해 토비의 스프링에서 무려 200쪽이 넘는 공간을 할애하여 설명하고 있으므로 여기서는 가급적 자세한 설명을 생략하도록 하겠다. (생략의 가장 중대한 이유는 필자도 잘 모른다는 사실이지만 그것은 중요치 않다.... :D) 먼저 @ModelAttribute User user와 같은 형태로 클라이언트의 요청을 커맨트 패턴으로 받는다. 그리고 이 모델에 검증을 하기 위해 위의 예제와 같이 @valid라는 검증 어노테이션을 붙인다. 그리고.... 더 해야 할 일은...... 없다.

짜잔! 이것은 기본적인 검증 모델 설계가 끝난 셈이다. 믿지 않겠지만 진짜다. 스프링 MVC는 이와같은 간단한 어노테이션만으로도 검증모델을 유지하며 검증 결과를 BindingResult 객체로 전달해주는 역할을 담당한다. 먼저 자바빈 객체를 살펴보며 @Valid가 무엇인지 부터 알아보자.

@Valid는 그동안 논란이 되왔던, 검증모델이 프리젠테이션 계층에 위치해야 되는지, 서비스 계층에 위치해야 하는지에 대한 논란을 어느 정도 통일시켜주는 표준기술이다. 그러나 역설적으로 @Valid는 그동안의 논란을 뒤집고 검증모델을 프리젠테이션 계층도, 서비스 계층도 아닌 도메인 계층에 삽입할 수 있게 하였다. 즉 @Valid는 아예 원천적으로 등록 오류를 피하기 위해 객체 자체에 검증모델 주입하는 방식을 채택하고 있다.

@Valid에는 기본적으로 14개의 검증 어노테이션을 제공한다.

@AssertFalse : false 값만 통과 가능
@AssertTrue : true 값만 통과 가능
@DecimalMax(value=) : 지정된 값 이하의 실수만 통과 가능
@DecimalMin(value=) : 지정된 값 이상의 실수만 통과 가능
@Digits(integer=,fraction=) : 대상 수가 지정된 정수와 소수 자리수보다 적을 경우 통과 가능
@Future : 대상 날짜가 현재보다 미래일 경우만 통과 가능
@Past : 대상 날짜가 현재보다 과거일 경우만 통과 가능
@Max(value) : 지정된 값보다 아래일 경우만 통과 가능
@Min(value) : 지정된 값보다 이상일 경우만 통과 가능
@NotNull : null 값이 아닐 경우만 통과 가능
@Null : null일 겨우만 통과 가능
@Pattern(regex=, flag=) : 해당 정규식을 만족할 경우만 통과 가능
@Size(min=, max=) : 문자열 또는 배열이 지정된 값 사이일 경우 통과 가능
@Valid : 대상 객체의 확인 조건을 만족할 경우 통과 가능

기본 객체는 대부분의 경우 위의 14가지 어노테이션으로도 통과가 가능하겠지만 생성객체나 좀 더 엄격한 규칙을 위해서는 직접 검증 어노테이션을 구현할 수도 있다. 직접 어노테이션을 구현하는 부분은 다음에 다뤄보기로 하고 지금은 위의 예제가 어떤 방식으로 동작하는 지에 좀 더 집중하도록 하겠다.

이제 위와 같은 어노테이션을 도메인 모델에 적용시켰다면 마지막으로 스프링은 @Valid를 컨트롤러에 적용시킴으로써 깜찍한 마법을 부려준다. 위의 예제에서 @ModelAttributes @Valid User user는 위의 어노테이션 설명에서도 보았듯이 대상 객체에 정의 되있는 확인 조건을 만족시키는 역할을 담당한다. 스프링 MVC는 이런 조건을 만족시키지 못할 경우 내부 컨트롤러에 의해 자동적으로 bindingResult 객체에 담겨저 컨트롤러로 돌아오게 된다.

즉 위의 예제는 /join URL을 Post방식으로 호출할 경우 자연스럽게 @ModelAttribute가 해당 파라미터들을 User객체로 캡슐화 시킴과 동시에 내부에 규약되있는 객체 검을을 @Valid가 발동되면서 오류가 있을 경우 오류 정보를 함께 전송한다. 그리고 마지막으로 스프링 컨테이너에 의해 오류정보는 bindingResult 객체로 분류되면서 끝이 난다.

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<form:form modelAttribute="user" autocomplete="off">
<h4><strong>회원가입</strong></h4>
<div class="clearfix">
<div class="left">
<form:label path="id">아이디</form:label>
</div>
<div class="left">
<form:input path="id"/>
<form:errors path="id" />
</div>
</div>
<div class="clearfix">
<div class="left">
<form:label path="password">비밀번호</form:label>
</div>
<div class="left">
<form:input path="password"/>
<form:errors path="password" />
</div>
</div> 
<div class="clearfix">
<div class="left">
<form:label path="email">이메일</form:label>
</div>
<div class="left">
<form:input path="email"/>
<form:errors path="email" />
</div>
</div>
<div class="clearfix">
<div class="left"></div>
<div class="left"></div>
</div>
<input type="submit" value="업로드" />
</form:form>


아래는 위의 JSP코드와 컨트롤러를 이용하여 만든 예제 회원가입 폼이다.

위의 코드를 적용시키기 위해서는 Spring에서 제공하는 form 태그를 이해해야 하는데 스프링 폼은 나중에 깊게 다루기로 하고 일단 기본적으로 이 스프링 폼을 통해 에러객체와 폼객체를 완벽하게 분리시켰다는 사실만 기억하자.

<form:errors>는 컨트롤러에서 전달받은 에러 객체를 컨트롤하는 역할을 담당하는데 현재 별도의 에러 메시지 설정이 없으므로 기본 에러가 표시된다. 만약 에러메시지를 바꾸고 싶다면 도메인 객체에 삽입한 검증 모델에 message라는 값을 더해주면 된다.

@Size(min=5, max=50, message="5자에서 50자 사이의 값만 가능합니다") private String id;

또 하나의 방법은 messages.properties를 이용하여 이런 에러메시지를 관리하는 방법인데 이 방법은 좀 더 스프링 검증 모델을 완벽히 습득한 뒤에 작성하도록 하겠다. 생각같아선 가급적 @Valid에 대해 완벽하게 다루고 싶었지만 생각보다 복잡한 방식으로 이루어지고 있는데다 확장포인트를 잡기가 어려워 여기서 대략적인 설명만 달아놓을 수 밖에 없었다.

이 문서는 기본적인 설명만을 다루고 있으므로 예제나 방식을 실제 서비스에 적용할 때는 좀 더 많은 관련 문서를 참고해 보고 자신만의 설계 모델을 구축해야 할 것이다.


'SPRING > Basic' 카테고리의 다른 글

@ModelAttribute와 @SessionAttributes의 이해와 한계  (0) 2016.10.05

@MVC에는 개발자들에게 프로그래밍을 예술의 경지까지 승화시켜주는 다양한 기술들이 존재하지만 그 중에서도 가장 아름다운 것을 꼽으라면 어노테이션을 통한 자동 객체변환을 꼽을 수 있겠다. 그리고 그 자동 객체변환 기술 중에서도 가장 아름다운 것은 @SessionAttributes와 @ModelAttribute… 개인적인 느낌으론 그야말로 객체변환의 결정체라고 할 수 있겠다.



@ModelAttribute

먼저 @ModelAttribute를 살펴보자. 필자가 @MVC를 처음 접했을 때는 어노테이션이라는 것 마저도 굉장히 생소했고 어노테이션만으로 이런 말도 안되는 기술이 구현가능하다는 사실에 깜짝 놀랐었다. 몸만 안 자빠졌을 뿐이지 정신은 안드로메다로 내달리는 정도라고나 할까? 굳이 예를 들자면… 당신이 90년대 편지 밖에 없는 세상에서 살다가 다음날 갑자기 아이폰4S를 만지작 거리고 있는 수준이다.

@ModelAttribute는 다른 말로 커맨드 오브젝트라고도 불리는데 그 이유는 클라이언트가 전달하는 파라미터를 1:1로 전담 프로퍼티에 담아내는 방식이 커맨드 패턴 그 자체이기 때문이다. 위의 이미지와 같이 @ModelAttribute는 클라이언트의 파라미터를 완벽하게 이해하고 있는 자바빈 객체를 요구하며 이 객체에 @ModelAttribute란 어노테이션를 붙이면 자동으로 바인딩해주고 중간 과정은 모두 자동으로 생략 해준다.

@Controllerpublic class HomeController {

@RequestMapping(value="/", method=RequestMethod.GET)
public String home(@ModelAttribute Command command) {
...
}
}

위의 예를 보면 바인딩 과정이 코드에 전혀 나타나있지 않고 전달인자에 @ModelAttribute를 넣는 것 만으로 모두 생략이 가능해 졌다. 만약 위의 코드를 서블릿으로 대체하자면 아래와 같이 작성할 수 있을 것이다.

public class Controller extends HttpServlet {
private static final long serialVersionUID = 1L;

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

String parameter1 = request.getParameter("parameter1");
String parameter2 = request.getParameter("parameter2");
String parameter3 = request.getParameter("parameter3");
String parameter4 = request.getParameter("parameter4");

...

}
}

사실 위의 작성한 서블릿 코드도 @ModelAttribute의 기능 중 고작 파라미터를 바인딩하는 정도만 구사해낸 저급 수준의 코드이다. 과거에는 이렇듯 파라미터로 전송받는 데이터를 처리하고 DB에 넣는 과정이 상당히 까다로왔다. 간단한 회원가입 조차도 데이터를 처리하는데 복잡한 로직이 필요했으니 말이다. 특히 핸드폰이나 집전화 같은 경우는 하나의 값에 3개의 <input>을 사용하는데다 생년월일 같은 값은 년, 월, 일을 다 각자 받아내 Date형태로 변화하려고 했으니 꽤나 애를 먹을 수 밖에 없었다. (여기다 에러처리까지 포함하면 코드가 순식간에 산더미가 되곤 한다.) 헌데 이런 모든 과정을 스프링이 이해하고 있고 자동으로 모든 작업 대신 해주니 얼마나 감사한 일인가.

@ModelAttribute를 사용하는 방법은 다음과 같다. 우선 클라이언트가 전송할 파라미터를 하나씩 바인딩 할 수 있는 커맨드형 자바빈 클래스를 하나 만든 뒤 파라미터의 이름과 커맨드 오브젝트의 프로퍼티 명을 맞춰준다. 그리고 해당 리소스를 처리할 컨트롤러를 만든 뒤 파라미터를 @ModelAttribute를 이용해 커맨드 오브젝트에 바인딩 시켜주면 된다. 이게 지금은 이해가 잘 가지 않아도 알고나면 굉장히 쉽다. 그래도 처음 접하는 사람에겐 전혀 상식적이지 않은 기술이므로 @ModelAttribute가 이해하는 바인딩 규칙의 예를 좀 더 꼼꼼히 들어보도록 하겠다.

public class Command {
String id;
String pass;
String name;

--* <input name="id" /> = *-- public void setId(String id) { this.id = id }
--* <input name="pass" /> = *-- public void setPass(String pass) { this.pass = pass }
--* <input name="name" /> = *-- public void setName(String name) { this.name = name }

(get~ 생략)

}

위의 예제를 보면 조금 @ModelAttribute의 동작원리를 이해할 수 있을 것이다. name="id"는 setId() 메서드와 관계를 맺고 있고 name="pass"는 setPass() 메서드와 밀접한 관계를 맺고 있다. 스프링은 이런 명명규칙만으로도 해당 폼에서 전송되는 파라미터와 커맨드 오브젝트의 관계를 이해해 낼 수 있다. 결론은 이런 식으로 일일이 대입되는 커맨드 오브젝트만 있으면 나머지는 스프링 컨테이너가 알아서 바인딩 과정을 처리해준다는 뜻이다. 게다가 이게 끝이 아니다. @ModelAttribute는 BindingResult 전달 인자와 @Valid와 합쳐서 더욱 멋진 기술을 구현해 낼 수 있다.

@Controllerpublic class Controller {
@RequestMapping(value="/", method=RequestMethod.GET)
public String home(@ModelAttribute @Valid Command command, BindingResult result) { if(result.hasErrors()) ...
else ...
}
}

BindingResult 전달인자는 자동으로 커맨드 오브젝트에 내장되 있는 검증 어노테이션을 기준으로 에러를 판독해 그 결과값을 가져와 주고 오류가 있을 경우 손쉬운 후처리 기능을 제공해 해준다. 그 뿐인가. 바인딩의 에러 내용을 다시 뷰에 전송해주고 <form:form> 태그와 같은 SpringEL을 이용해 바인딩 에러 결과를 브라우져를 통해 클라이언트에게 보여주는 작업 또한 간편하게 작성할 수 있게 해준다. 게다가 정말 신기한 것은 enum 객체같은 경우라도 파라미터의 이름만 알고 있다면 클래스명.파라미터명과 같이 매핑해준다는 사실이다.

여기까지 설명을 들었다면 이제 @ModelAttribute가 얼마나 활용도 높은 기술인지 대략 짐작할 수 있을 것이다. 토비의 스프링에서도 딱 이정도 수준까지 설명됬던지라 (물론 이것보다 자세하게…) 여기까지 이해가 됬다면 @ModelAttribute가 세상에서 제일 좋은 킹 오브 킹 어노테이션으로 생각할 수도 있겠다. 헌데 아쉽게도 @ModelAttribute에는 분명한 한계가 존재한다. 물론 단점보다 장점이 많은 기술이므로 이정도 단점에 사용이 꺼려질 정도가 되는 것은 아니지만 @ModelAttribute도 모든 바인딩 과정에서 완벽할 수는 없다는 결론 정도로 생각할 수 있다.


배열 방식 객체의 까다로움

필자는 어느날 @ModelAttribute를 사용해 개발을 하다가 일순 멈칫하고 말았다. 이유는 @ModelAttribute를 활용하면 배열로 전달되는 파라미터의 처리가 상당히 까다로워 진다는 인식을 받았기 때문이다. 솔직히 기존의 서블릿과 자바 애플릿으로 했던 노가다에 비하면 세발의 피같은 어려움이긴 하지만 그래도 이런 기술을 좀 더 깔끔하게 사용하고 싶다는 욕구 때문인지 많은 아쉬움이 남았다. 내용은 바로 아래와 같다.

@RequestMapping(value="/", method=RequestMethod.POST)
public String home(@ModelAttribute List<Command> command) {
...
}

왜 위와 같은 형태로 전달인자를 받고 싶었냐면 (아직 @SessionAttributes로 넘어가지 않아 자세히 설명할 수는 없지만…) 만약 동일한 커맨드 오브젝트가 2개 이상 전달될 것으로 예상되는 폼 구성이라면 넘어오는 커맨드 오브젝트를 하나씩 DB에 저장시키기엔 낭비와 후처리가 상당히 까다로웠기 때문이다. 만약 입력 중간에 사용자가 변심한다면 DB의 자료를 롤백시켜주는 기능도 필요했고 추가/변경 기능까지 구성하려다 보면 그 과정이 상당히 번거로워지기 마련이었다. 그렇기 때문에 이런 처리는 가급적 세션으로 사용자의 파라미터를 List 형태로 저장시키고 사용자의 입력이 완료되면 한번에 DB에 입력하고 싶었다. 그러면 불필요한 입출력을 줄일 수 있을테고 불량 데이터로 인한 시스템 리소스의 낭비도 덜할테니 말이다.

헌데 @ModelAttribute는 위와같은 형태의 데이터를 허용하지 않았다. 사실 @ModelAttribute 뿐만이 아니라 @SessionAttributes도 그렇고 @MVC의 어노테이션 대부분이 위와 같은 컬렉션프레임워크 형태의 데이터를 처리하지 못한다. 어찌보면 당연한 일이지만 사뭇 당연히 될 것이라 기대했던 나로서는 한풀 꺽인 경우가 됬다.

@ModelAttribute는 매우 뛰어난 바인딩 기술로 정평이 나있지만 해당 값을 복수의 형태로 받고 싶다면 오로지 자바빈 객체를 배열로 선언하는 방법 밖에 없다.

public class Command {
private String[] id;
private String[] pass;
private String[] name;
...
}

물론 이렇게 처리하는  방법도 결코 나쁜 방식은 아니다. 문제는 기본 배열로 객체를 다루다 보니 처리방식이 너무 정형화되어 유동적으로 변하는 <form>에는 대응하기가 조금 까다로워진데다 우리가 이용하려던 @Valid를 통한 검증기능이 많이 축소되었다는 것이다. 왜냐하면 @Valid는 단일 객체에 대한 검증기능은 풍부해도 배열형태의 자료를 검증하는 기능은 고작 길이 정도를 검증하는 기능 밖에 지원되지 않기 때문이다.

그러나 아무리 생각해도 파라미터 배열 바인딩은 포기할 수 없는 필수 요소 중 하나이다. 예를 들어 한 사람의 경력을 폼으로 입력받는다 할 때 모든 사람이 평생 하나의 직업과 직장을 가지는 것은 아니므로 유동적으로 배열 파라미터를 처리할 수 있는 폼 설계가 필요하다. 그리고 해당 파라미터 값이 문제가 있을 경우 다른 단일 객체와 똑같이 배열이 아닌 배열 속의 객체에 바인딩 에러를 처리해주는 기술이 필요하다. (@Valid의 기능만으론 이 조건을 충족시킬 수 없었다. 물론 확장한다면 상황이 달라질 수도 있겠지만…)

그렇다고 이런 제약 때문에 @ModelAttribute를 포기할 수도 없는 노릇이고, 안되는 걸 어거지로 끼워맞추자니 뭔가 앞뒤가 안 맞는 것 같은 기분이 들었다. 이런 모든 상황에 맞는 처리 방법은 @ModelAttribute가 List<Command> command와 같은 컬렉션프레임워크 형태를 지원하는 것 뿐이었으니 아쉬움은 더 클 수밖에 없었다. 그래도 포기하는 것은 이르고 아직 이 문제에 대한 해결법을 찾지 못했지만 추후에 좀 더 심층적으로 연구한 뒤에 해결법을 공개해보도록 하겠다.


@SessionAttributes과 SessionStatus

@SessionAttributes를 이해하기 위해선 아래의 참고 UML을 이해하는 과정이 필요하다. 물론 아직 아래의 UML만으로는 이해가 쉽지 않다는 것을 잘 알고 있다. 왜냐하면 우리는 모델 오브젝트 준비가 뭔지도… 프로퍼티 바인딩이 뭔지도, WebBindingInitializer라는게 뭔지도 제대로 이해하지 않고 있기 때문이다. 아직 아래의 UML을 이해하지 못하는 것은 당연한 일이며 억지로 이해하려하기 보다는 당장 우리가 이해하려는 @SessionAttribute 부분만 살펴보고 대략적으로 이해할 수 있도록 해보자.

클라이언트 -> 스프링 컨테이너 -> 서버의 과정
클라이언트 <- 스프링 컨테이너 <- 서버의 과정
위의 UML은 필자가 만든 것은 아니며 토비의 블로그에서 퍼온 것인데 @Controller가 동작하는 과정을 너무 자세히 설명해주고 있어서 참고자료로 삽입하게 되었다. 많은 사람들이 이 UML을 보기 전 만하더라도 기존의 @MVC가 어떤 방식으로 동작하는지 감잡았다고 생각했다가 UML을 잠깐 훑어보고는 그동안의 이해가 싹 사라지고 눈앞이 껌껌해지는 느낌을 받을 수도 있을 것이다. 필자도 이 UML을 처음 본 순간… 이제 좀 알았다는게 정말 알긴 개뿔이라는 수준 밖에 못된다는 사실을 깨달았다.

여하튼 차츰차츰 알아가는 것이 공부이므로 위의 @Controller의 동작과정은 나중의 확장할 때에 좀 더 자세히 공부하도록 하고 일단은 우리의 과제인 @SessionAttributes에 대해 이해해보도록 하자. 이 어노테이션은 스프링에서 상태유지를 위해 제공되는 어노테이션인데 대충 객체의 위치가 뷰와 컨트롤러의 사이에 존재한다고 생각하면 좋다.

우선 @SessionAttributes는 항상 클래스 상단에 위치하며 해당 어노테이션이 붙은 컨트롤러는 @SessionAttributes("세션명")에서 지정하고 있는 "세션명"을 @RequestMapping으로 설정한 모든 뷰에서 공유하고 있어야 한다는 규칙을 갖고 있다. 예를 들어 위와 같이 @SessionAttributes("command") 라는 어노테이션이 붙은 클래스라면 하위의 종속되있는 모든 뷰가 "command"라는 모델 값을 공유하고 있어야 한다는 것이다. 만약 이 조건을 충족하지 못하면 다음과 같은 에러가 발생하게 된다.

org.springframework.web.HttpSessionRequiredException: Expected session attribute 'command'

RequestMapping(value="/", method=RequestMethod.GET)
public String home(Model model) {
model.addAttribute("command", new Command());
}

// 이런 방식으로 SessionAttributes를 이용하는 것은 옳지 않다.

이 문제를 해결하기 위해 사용할 수 있는 방법은 2가지 인데 첫째는 해당 컨트롤러에서 맨 처음 읽어들일 것으로 예상되는 뷰의 Model 객체를 통해 수동적으로 "command"란 파라미터를 보내주는 것이다. 이 방식은 클라이언트가 해당 클래스로 뷰어에 접근할 때 반드시 첫번째로 해당 뷰를 통해야만 한다는 제약조건을 갖게 되며 그렇지 않을 경우 또다시 위의 에러가 발생할 수 있으므로 결코 추천할 수 없는 방식이다.

@SessionAttributes("command")
@Controllerpublic class Controller {

@ModelAttribute("command")
public Command command() {
return new Command();
}

@RequestMapping(value="/", method=RequsetMethod.POST)
public String home(@ModelAttribute Command command) {
...
}
}

이런 불필요한 에러를 보고 싶지 않다면 @ModelAttributes를 붙인 메서드를 이용할 것을 적극 권장한다. 위의 예제를 보면 @ModelAttribute가 붙은 command()메서드를 볼 수 있는데 이 메서드는 해당 컨트롤러로 접근하려는 모든 요청에 @ModelAttribute가 붙은 메서드의 리턴 값을 설정된 모델명으로 자동 포함해주는 역할을 담당해준다. 물론 이미 동일한 이름의 모델이 생성되었있다면 위의 메서드 값은 포함되지 않으며 오로지 설정한 모델명과 일치하는 객체가 존재하지 않는 경우에만 메서드의 리턴 값을 서버의 응답과 함께 클라이언트에게 전송하는 역할을 담당한다.

말이 조금 어렵긴 한데 단순하게 요약하자면 해당 컨트롤러로 클라이언트가 접근할 때 반드시 @ModelAttribute가 붙은 메서드의 리턴 값을 보장받는 다는 소리다. 지금은 단순하게 return new Command(); 정도로 마무리 지었지만 원한다면 해당 객체에 기본 값을 포함할 수도 있다.

@SessionAttributes의 기본 충족조건을 이해했으므로 이제 사용 용도에 대해 조금 생각해보자. 필자가 생각하는 @SessionAttribute의 사용 용도는 다음과 같다.

1. 스프링에서 제공하는 form 태그라이브러리를 이용하고 싶을 때.
2. 몇 단계에 걸쳐 완성되는 폼을 구성하고 싶을 때
3. 지속적으로 사용자의 입력 값을 유지하고 싶을 때

아마 첫번째 이유가 가장 절실할 것 같다. 필자도 스프링에서 제공하는 폼태그를 자주 활용하는데 이 태그라이브러리를 활용하면 폼 작성이 정말 쉬워지는데다 검증 바인딩 기술은 한번 쓰면 헤어나올 수 없는 마약과도 같아서 쉽게 떨쳐버리기가 힘들다 :(
@SessionAttributes는 해당 어노테이션에 설정한 값과 동일한 이름의 모델객체를 발견하면 이를 캐치하여 세션값으로 자동 변경시켜준다. 그리고 해당 모델객체가 세션값으로 대체되면 앞으로 세션값을 지우기 전까지 해당 이름의 모델명 호출시 세션에 저장된 값을 불러오게 된다.

@Controller
@SessionAttributes("command")
public class Controller {

@ModelAttribute("command")
public Command command() {
return new Command();
}

@RequestMapping(value="/", method=RequestMethod.GET)
public String home() {
return "home";
}

@RequestMapping(value="/", method=RequestMethod.POST)
public String home(Model model, @ModelAttribute Command command) { model.addAttribute("command", command);
return "home";
}
}
위와 같은 소스를 예로 들어 설명한다면 "/"란 경로로 POST 방식을 통해 클라이언트가 파라미터를 보낼 경우 서버는 해당 값을 세션에 저장되어 있는 "command"객체에 저장시켜 해당 세션을 종료하기 전까지 값을 유지해준다. 물론 이 예제만으로 @SessionAttributes는 동작과정을 판단하기가 매우 어렵기 때문에 @SessionAttributes를 사용하기 전에 미리 필요한 학습테스트를 거친 후에 사용할 것을 권장한다.

이제 세션 값이 더이상 필요 없어질 경우 이를 지우는 방법도 알아야 하겠다. 세션값을 제때 지우지 않고 계속 쌓아둔다면 메모리에 무리가 생길 수 있으므로 불필요해질 경우 제거해주는 것도 중요하다. 제거법은 매우 간단한데

@RequestMapping(value="/", method=RequestMethod.POST)
public String home(Model model, @ModelAttribute Command command, SessionStatus session) {
model.addAttribute("command", command);
session.setComplete();
return "home";
}

위와 같이 종료가 필요한 URL매핑 메서드에 SessionStatus란 세션관리 인자를 전달받아 종료시켜주면 된다.


SessionAttributes의 한계

이 어노테이션도 위의 ModelAttribute와 마찬가지고 컬렉션 프레임워크를 지원하지 않는다는 단점이 있다. 만약 이 값이 List형태의 데이터를 지원했다면 정말 최고였겠지만 아쉽게도 그렇지가 못하다. SessionAttributes와 ModelAttribute는 밀접한 관계를 가진 어노테이션이며 어느 한쪽만 List 형태를 지원한다고 해결될 일이 아니므로 이 해결법에 대해서는 좀 더 심층적으로 연구하고 일반화된 해결법이 필요하겠다.

스프링 컨트롤러를 이용하면서 얻게된 습관이 가급적 코드를 짧게 쓰려는 습성이 생겼다는 것이다. 스프링 프레임워크를 활용하면 본래 엄청나게 길어질 코드들도 단 몇줄의 코드로 똑같은 적용이 가능하다보니 코딩을 하면서 괜히 더 짧게 할 수 있는 방법은 없나… 꼼수를 부리게 되곤 한다. 게다가 괜히 작성하는 코드가 좀 길어지다보면 문득 내가 무언가 잘못하고 있다는 압박감을 받기도 한다.

위의 @ModelAttribute가 List 형태를 지원하지 않는 것도 이런 압박감의 일종일지 모른다. 이렇게나 저렇게나 해결할 방법은 분명 존재하지만 뭔가 깔끔하게 떨어지지 않는다는 것에 미련에 베스트 프렉티스가 존재하지는 않나 인터넷을 뒤져보는 것들 말이다.

더욱이 스프링을 이용하면서 이런 어노테이션을 자주 활용하다보니 많은 기술을 함축시켜 사용할 수 있다는 장점 때문에 개발자를 게으르게 하고 손수 기능을 구현하는데 선뜻 나서지 못하는 상황을 만들어 낸다는 것이다. 더욱이 어노테이션 기술은 한가지 엄청나게 치명적인 단점을 가지고 있는데 그것은 바로 스프링의 뚜렷한 장점이었던 확장이 굉장히 힘들어진다는 것이다.

만약에 위와 같은 문제로 @SessionAttributes를 조금 손보고 싶다고 하자. 가히 만만치 않은 작업인데다 어쩌면 @MVC에 해당하는 클래스 대부분을 손봐야 할지도 모른다. 그렇다고 이제와서 어노테이션을 사용 안할 수도 없는 노릇인데다 이미 스프링 @MVC가 대부분의 핵심기술을 어노테이션을 이용해 구현하고 있는 것도 문제다.

물론 스프링 자체에서도 이러한 문제점에 대해 잘 인식하고 있으며 지속적인 해결책을 강구하고 있긴 하다. 뭐 이런 확장이니 뭐니해도 어찌됬든 개발자가 원하는 기술을 구현하기만 하면 장땡이다. 필자가 원하는 기술이 제대로 구현할 수가 없으니 이런 못된 심보의 글도 나오는 것 아니겠는가.


'SPRING > Basic' 카테고리의 다른 글

객체 검증의 종결자 @Valid  (0) 2016.10.05