In this tutorial you will learn about the @SessionAttributes annotation
@SessionAttributes example
Some time you need to maintain model objects by adding attributes to the Model, Map or ModelMap. in each handeler methods. The @SessionAttributes annotation removes this burden of adding attribute several time, the only you have to do is "declare @SessionAttributes with the model attribute you want to maintain before the class declaration". This attribute will be maintained for all the handler methods of this class and please note that this session attributes can not be access by other controller classes, to do so you have to add this attribute in HttpSession. Suppose you want to maintain user attribute on the session then you should first declare this attribute on the session as,
@SessionAttributes( {"user"}) and add the attribute at the model as
model.addAttribute("user", user);
And you can access this attribute from JSP page as
<%=session.getAttribute("user")%>
Please consider the controller class given below
ApplicationController.java
package roseindia.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import roseindia.domain.User;
import roseindia.form.LoginForm;
@Controller
/*
* Here @SessionAttributes annotation is used for storing model attribute on the
* session and this will be maintained for all the handler methods of this
* class.
*/
@SessionAttributes( { "id", "user", "password" })
public class ApplicationController {
@RequestMapping(value = "/index")
public String loadIndex(Model model, LoginForm loginForm) {
model.addAttribute("loginForm", loginForm);
return "index";
}
@RequestMapping(value = "/process-form")
public String processLogin(Model model, LoginForm loginForm) {
User user = new User();
user.setPassword(loginForm.getPassword());
user.setUserId(loginForm.getLoginId());
model.addAttribute("id", loginForm.getLoginId());
model.addAttribute("user", user);
model.addAttribute("password", loginForm.getPassword());
return "user-home";
}
@RequestMapping(value = "/process-test")
public String textHandler() {
return "user-home";
}
}
출처 - http://www.roseindia.net/tutorial/spring/session-attributes-annotation.html
===================================================================================
HTTP 요청에 의해 동작하는 서블릿은 기본적으로 상태를 유지하지 않는다. 따라서 매 요청이 독립적으로 처리된다. 하나의 HTTP 요청을 처리한 후에는 사용했던 모든 리소스를 정리해 버린다. 하지만 애플리케이션은 기본적으로 상태를 유지할 필요가 있다. 사용자가 로그인하면 그 로그인 정보는 계속 유지돼야할 것이고, 여러 페이지에 걸쳐 단계적으로 정보를 입력하는 위저드 폼 같은 경우에도 폼 정보가 하나의 요청을 넘어서 유지돼야 한다. 간단하게는 다순한 폼 처리 중에도 유지해줘야 하는 정보가 있다.
도메인 중심 프로그래밍 모델과 상태 유지를 위한 세션 도입의 필요성
사용자 정보의 수정 기능을 생각해보자. 수정 기능을 위해서는 최소한 두 번의 요청이 서버로 전달돼야 한다. 첫 번째는 수정 폼을 띄워달라는 요청이다. 수정할 사용자 ID를 요청에 함께 전달한다. 서버는 주어진 ID에 해당하는 사용자 정보를 읽어서 수정 가능한 폼을 출력해 준다. 사용자가 폼의 정보를 수정하고 수정 완료 버튼을 누르면 두 번째 요청이 서버로 전달된다. 이때는 수정한 폼의 내용이 서버로 전달된다. 서버는 사용자가 수정한 정보를 받아서 DB에 저장하고 수정 완료 메시지가 담긴 페이지를 보여준다..
어떻게 보면 수정 작업은 두 번의 요청과 두 개의 뷰 페이지가 있으면 되는 간단한 기능이다. 하지만 좀 더 생각해보면 수정 작업은 생각보다 복잡해질 수 있다. 사용자가 수정한 폼의 필드 값에 오류가 있는 경우에 에러 메시지와 함께 수정 화면을 다시 보여줘야 하기 때문이다. 또, 상태를 유지하지 않고 폼 수정 기능을 만들려면 도메인 오브젝트 중심 방법보다는 계층 간의 결함도가 높은 데이터 중심 방식을 사용하기 쉬워진다.
폼의 검증과 에러 메시지 문제는 이후에 다룰 것이고, 일단 여기서는 폼 처리시 상태유지 문제만 살펴보자. 서버에서 하나의 요청 범위를 넘어서 오브젝트를 유지시키지 않으면 왜 데이터 중심의 코드가 만들어지고 계층간의 결함도가 올라가는지 생각해보자.
사용자 정보의 수정 폼을 띄우는 컨트롤러는 아마도 다음과 같이 만들어질 것이다.
1 2 3 4 5 | @RequestMapping (value= "/user/edit" , method=RequestMethod.GET)
public String form( @RequestParam int id, Model model) {
model.addAttribute( "user" , userService.getUser(id));
return "user/edit" ;
}
|
사용자 수정 폼에는 사용자 테이블에 담긴 대부분의 정보를 출려해줄 필요가 있다. 그래서 도메인 오브젝트 중심 방식의 DAO 를 사용해서 주어진 id에 대한 사용자 정보를 User 오브젝트에 담아 모두 가져오는 것이 자연스럽다. 서비스 계층에서 사용자 정보를 가져오면서 일부 정보를 활용하거나 가공하는 등의 작업을 해도 상관없다. User 라는 의미 있는 도메인 오브젝트를 사용하기 때문이다.
그런데 문제는 여기서부터다. 사용자 정보 수정 화면이라고 모든 정보를 다 수정하는 것이 아니다. 사용자 스스로 자신의 정보를 수정하는 경우라면 수정할 수 있는 필드는 제한되어 있다. 로그인 아이디나 중요한 가입정보는 특별한 권한이 없으면 수정할 수 없게 해야 한다. 사용자 정보에 포함된 레벨이나 포인트 등도 수정할 수 없다. 마지막 로그인 시간이나, 기타 사용자의 활동정보에 해당하는 내용도 수정할 수 없다. 이런 정보는 아예 수정 폼에 나타나지 않거나, 나타난다고 하더라도 읽기 전용으로 출력만 될 뿐이다. 좀 더 권한이 많은 관리자 모드의 사용자 수정 화면이라고 하더라도 여전히 폼에서 모든 정보를 다 수정할 필요는 없다. 따라서 User 도메인 오브젝트에 담겨서 전달된 내용 중에서 수정 가능한 일부 정보만 폼의 <input> 이나 <select> 등을 이용한 수정용 필드로 출력된다.
그렇기 때문에 폼을 수정하고 저장 버튼을 눌렀을 때는 원래 User에 담겨 있던 내용 중에서 수정 가능한 필드에 출력했던 일부만 서버로 전송된다는 문제가 발생한다. 폼의 서브밋을 받는 컨트롤러 메소드에서 만약 다음과 같이 User 오브젝트로 폼의 내용을 바인딩하게 했다면 어떻게 될까?
1 2 3 4 5 | @RequestMapping (value= "/user/edit" , method=RequestMethod.POST)
public String submit( @ModelAttribute User user) {
userService.updateUser(User);
return "user/editsuccess" ;
}
|
@ModelAttribute 가 붙은 User 타입 파라미터를 선언했으니 폼에서 POST 를 통해 전달되는 정보는 User 오브젝트의 프로퍼티에 바인딩돼서 들어갈 것이다. 문제는 폼에서 <input> 이나 <select> 로 정의한 필드의 정보만 들어간다는 점이다. 단순히 화면에 읽기 전용으로 출력했던 loginId 나 가입일자라든가, 아예 출력도 안 했던 포인트나 내부 관리 정보 등은 폼에서 전달되지 않으므로 submit() 메소드의 파라미터로 전달되는 user 오브젝트에는 이런 프로퍼티 정보가 모두 비어 있을 것이다.
일부 필드의 정보가 빠진 이 반쪽짜리 user 오브젝트를 User 도메인 오브젝트를 사용해서 비즈니스 로직을 처리하는 서비스 계층의 UserService 빈을 사용해서 User 도메인 오브젝트를 사용해서 DB에 결과를 업데이트해주는 UserDao 빈에 전달해준다고 해보자. 어떤 일이 일어날까? UserService 에 사용자 정보를 수정할 때마다 사용자 정보를 일부 참조하거나 변경해주는 로직이 포함되어 있거나, 수정이 일어날때 이를 로깅하거나 관리자에게 통보하거나 통계를 내기 위해 다른 서비스 오브젝트를 호출하도록 만들어질 수 있다. 폼에 얼마나 많은 정보를 띄우고 이를 다시 돌려받는지에 따라 다르겠지만, 아마도 서비스 계층의 로직을 처리하는 중에 치명적인 오류가 발생할지도 모른다. 도메인 오브젝트를 이용해 비즈니스 로직을 처리하도록 만든 서비스 계층의 코드는 폼에서 어떤 내용만 다시 돌려지는지 알지 못하기 때문에, User 오브젝트를 받았다면 모든 프로퍼티 값을 다 사용하려고 할 것이다. 따라서 일부 비즈니스 로직에선 잘못된 값이 사용될 수도 있고, 아예 예외가 발생할 수도 있다.
DAO 경우는 더 심각하다. 도메인 오브젝트를 사용하도록 만든 DAO 의 업데이트 메소드는 도메인 오브젝트의 수정 가능한 모든 필드를 항상 업데이트한다. 따라서 일부 프로퍼티 값이 null 이거나 0인 상태로 전달돼도 그대로 DB에 반영해버린다. 결국 중요한 데이터를 날려버리는 심각한 결과를 초래할 것이다. 사용자가 자기 정보를 수정하고나면 0이 되는 버그 같은게 만들어 질 수 있다는 뜻이다.
도메인 오브젝트를 사용해 수정 폼을 처리하는 방식에는 이런 심각한 문제점이 있다. 그렇다면 이 문제를 해결할 수 있는 방법을 한번 생각해 보자.
히든필드
수정을 위한 폼에 User 의 모든 프로퍼티가 다 들어가지 않기 때문에 이런 문제가 발생했으니 모든 User 오브젝트의 프로퍼티를 폼에 다 넣어주는 방법을 생각해 볼 수 있다. 물론 사용자가 수정하면 안되는 정보가 있으니 이런 정보는 히든 필드에 넣어줘야 한다. 히든 필드를 사용하면 화면에서는 보이지 않지만 폼을 서브밋하면 다시 서버로 전송된다. 결국 컨트롤러가 받는 User 타입의 오브젝트에는 모든 프로퍼티의 값이 채워져 있을 것이다. 사용자가 수정하도록 노출한 필드는 바뀔 수 있지만, 히든 필드로 감춰둔 것은 원래 상태 그대로 돌아올 것이다.
이 방법은 간단히 문제를 해결한 듯 보이지만 사실 두 가지 심각한 문제가 있다. 첫째, 데이터 보안에 심각한 문제를 일으킨다. 폼의 히든 필드는 브라우저 화면에는 보이지 않지만 HTML 소스를 열어보면 그 내용과 필드 이름까지 쉽게 알아낼 수 있다. 폼을 통해 다시 서버로 전송되는 정보는 간단히 조작될 수 있다. 즉, 사용자가 나쁜 마음을 먹으면 HTML 을 통해 히든 필드 정보를 확인하고 그 값을 임의로 조작해서 서버로 보내버릴 수 있다. 이렇게 히든 필드에 중요 정보를 노출하고, 이를 다시 서버에서 받아서 업데이트 하는 방법은 보안에 매우 취약하다는 단점이 있다. 두 번째 문제는 사용자 정보에 새로운 필드가 추가됐다고 해보자. 그런데 깜빡하고 폼에 이 정보에 대한 히든 필드를 추가해주지 않으면, 추가된 필드의 값은 수정을 거치고 난 후 null 로 바뀌는 현상이 발생할 것이다. 이런 것은 테스트에서도 쉽게 발견되지 않으므로 실수하기 쉽다. 이런 버그 때문에 많은 사용자의 중요한 정보를 다 날린 뒤에 문제를 발견하면 큰일이다.
따라서 히든 필드 방식은 매우 유치한 해결 방법이고 공개된 서비스에서는 사용을 권장할 수 없다.
DB 재조회
두 번째 해결책은 기능적으로 보자면 완벽하다. 폼으로부터 수정된 정보를 받아 User 오브젝트에 넣어줄 때 빈 User 오브젝트 대신 DB에서 다시 읽어온 User 오브젝트를 이용하는 것이다. 이 방식으로 만든 컨트롤러 메소드를 살펴보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 | @RequestMapping (value= "/user/edit" , method=RequestMethod.POST)
public String submit( @ModelAttribute User formUser, @RequestParam int id) {
User user = userService.getUser(id);
user.setName(formUser.getName());
user.setPassword(formUser.getPassword());
user.setEmail(formUser.getEmail());
...
userService.updateUser(user);
return "user/editsuccess" ;
}
|
이 방법은 업데이트를 위해 서비스 계층으로 전달할 User 오브젝트를 DB에서 새로 읽어온 것으로 사용한다. DB에서 새로 읽어왔으므로 User 오브젝트에는 모든 프로퍼티의 내용이 다 들어 있다. 그리고 폼에서 전달되는 정보를 담을 User 타입의 formUser 파라미터도 메소드에 추기해 준다. formUser 에는 폼에 사용자가 수정할 수 있도록 출력해둔 필드의 내용만 들어 있을 것이다. 그리고 DB에서 가져온 모든 필드 정보가 담겨 있는 User 오브젝트에, 폼에서 전달 받은 formUser 중 폼에서 전달된 필드 값을 복사해 준다. 그리고 복사가 끝난 User 오브젝트를 서비스 계층으로 전달하는 것이다. 이렇게 전달된 User 오브젝트에는 모든 필드가 다 채워져 있고 폼에서 수정한 내용도 모두 반영되어 있다. 따라서 서비스 계층에서 User 도메인 오브젝트를 어떻게 활용하든 아무런 문제가 없다. 또 DAO 에서 모든 필드를 다 업데이트해도 상관없다.
완벽해 보이긴 하지만 이 방법에는 몇 가지 단점이 있다. 일단 폼을 서브밋할 때마다 DB에서 사용자 정보를 다시 읽는 부담이 있다. 성능에 큰 영향을 줄 가능성이 높진 않더라도 분명 DB의 부담을 증가시키는 것은 사실이다. 성능은 차치하더라도 폼에서 전달되는 필드가 어떤 것인지 정확히 알고 이를 복제해줘야 한다. 복사할 필드를 잘 못 선택하거나 빼먹으면 문제가 발생한다.
얼핏 보면 간단히 문제를 해결한 듯 보이지만 여전히 불편하며 새로운 문제를 초래하는 방법이다.
계층 사이의 강한 결합
세 번째로 생각해볼수 있는 방법은 계층 사이에 강한 결합을 주는 것이다. 강한 결합이라는 의미는 각 계층의 코드가 다른 계층에서 어떤 일이 일어나고 어떤 작업을 하는지를 자세히 알고 그에 대응해서 동작하도록 만든다는 뜻이다.
이 방식은 앞에서 지적한 폼 수정 문제의 전제를 바꿔서 문제 자체를 제거한다. 기본 전제는 서비스 계층의 updateUser() 메소드가 User 라는 파라미터를 받으면 그 User 는 getUser() 로 가져오는 User와 동등하다고 본다는 것이다. User 라는 오브젝트는 한 사용자 정보를 완전히 담고 있고, 그것을 전달받으면 원하는 모든 필드를 참조하고 조작할 수 있다고 여긴다는 말이다. 이렇게 각 계층이 도메인 모델을 따라 만든 도메인 오브젝트에만 의존하도록 만들면 각 계층 사이에 의존성과 결합도를 대폭 줄일 수 있다. 결합도를 줄인다는 의미는 한 계층의 구현 코드를 수정해도 기본적인 전제인 도메인 오브젝트가 바뀌지 않으면 다른 계층의 코드에 영향을 주지 않는다는 뜻이다. UserService 의 updateUser() 메소드는 사용자 레벨을 업그레이드하는 로직에서 결과를 반영하기 위해 호출되든, 관리자의 수정화면을 처리하는 AdminUserController 에서 호출되든, 사용자가 스스로 정보를 수정하는 화면을 처리하는 UserController 에서 호출되든 상관없이 동일하게 만들 수 있다. UserDao 의 update() 메소드도 마찬가지다. 전달되는 User 오브젝트가 폼을 통해 수정된 User인지 상관하지 않고 자신의 기능에만 충실하게 만들 수 있다. 결국 모든 계층의 코드가 서로 영향을 주지 않고 독립적으로 확장하거나 변경할 수 있고, 여러 개의 로직에서 공유할 수 있다.
반면에 계층 사이에 강한 결합을 준다는 건 각 계층의 코드가 특정 작업을 중심으로 긴밀하게 연결되어 있고, 자신을 사용하는 다른 계층의 코드가 어떤 작업을 하는지 구체적으로 알고 있다는 뜻이다. 예를 들어 사용자 자신의 정보를 수정하는 폼을 통해 전달되는 User 오브젝트에는 name, password, email 세 개의 필드만 들어온다는 사실을 UserService 의 updateUserForm() 메소드가 알고 있게 해주면 문제는 간단해진다. 폼에서 세 개의 필드만 수정할 수 있음을 알고 있으니 그 세 개의 필드만 담긴 User 오브젝트가 컨트롤러로 전달되는 것을 알 수 있다. 그에 따라 세 개의 필드 외에는 참조하지 않고 무시하도록 코드를 만들면 된다. DAO 도 마찬가지다, User의 모든 필드를 업데이트 하는 대신 updateForm() 이라는 메소드를 하나 따로 만들어서 name, password, email 세 개의 필드만 수정하도록 SQL을 작성하게 해주면 된다.
관리자 메뉴의 사용자 정보 수정 기능이 있고 거기에는 더 많은 정보를 수정하도록 만들어진 폼이 있다면, 이 폼을 처리하는 컨트롤러는 관리자 사용자 정보 수정을 전담하는 UserService 의 updateAdminUserForm() 을 호출하게 한다. updateAdminUserForm() 은 폼에 어떤 필드가 수정 가능하도록 출력되는지를 알고 있는 메소드다. 따라서 해당 필드만 참조해서 비즈니스 로직을 처리하고, 역시 해당 필드만 DB에 반영해주는 UserDao 의 updateAdminForm() 메소드를 사용한다. 아예 컨트롤러에서 폼의 정보를 받는 모델 오브젝트를 폼에서 수정 가능한 파라미터만 가진 UserForm, UserAdminForm 등을 독립적으로 만들어서 사용하는 방법도 있다. 결국 개별 작업을 전담하는 코드를 각 계층마다 만들어야 한다.
이런식으로 계층 간에 강한 의존성을 주고 결합도를 높이면 처음에는 만들기 쉽다. 대부분의 코드는 화면을 기준으로 해서 각 계층별로 하나씩 독립적으로 만들어질 것이다. DAO 의 메소드와 서비스 계층의 메소드는 각각 하나의 화면을 위해서만 사용된다. 결국 화면을 중심으로 거기에 사용되는 SQL 을 정의하고 하나씩의 메소드를 새롭게 추가하는 것을 선호하는 개발자가 애용하는 방식이다. 이런식의 개발은 코드 생성기를 적용하기도 좋다.
하지만 이렇게 결합도가 높은 코드를 만들 경우, 애플리케이션이 복잡해지기 시작하면 단점이 드러난다. 일단 코드의 중복이 늘어난다. 코드를 재사용하기가 힘들기 때문이다. 수정할 필드가 조금 달라도 다른 메소드를 만들어야 한다. 코드는 자꾸 중복되고 그 때문에 기능을 변경할 때 수정해야 할 곳도 늘어난다. 또한 계층 사이에 강한 결합이 있기 때문에 한쪽을 수정하면 연관된 코드도 함께 수정해줘야 한다. 코드의 중복은 많아지고 결합이 강하므로 그만큼 테스트하기도 힘들다. 테스트 코드에도 중복이 일어난다.
하지만 여전히 이런 방식을 선호하는 경우가 많다. 이런 방식이라면 서비스 계층, DAO 계층을 구분하는 것도 별 의미가 없을 지 모른다. 차라리 2계층이나, 컨트롤러 - 뷰만의 1계층 구조로 가능게 나을 수 있다.
아무튼, 이렇게 계층 사이에 강한 결합을 만들고 수정할 필요가 있는 필드가 어떤 것인지 모든 계층의 메소드가 다 알고 있게 하면 문제는 해결할 수 있다. 이럴 땐 User 와 같은 도메인 오브젝트보다는 차라리 파라미터 맵을 쓰는 것이 편리할 수 있다. 다음과 같이 컨트롤러 메소드의 @RequestParam 이 붙는 Map<String, String> 타입 파라미터를 사용하면 모든 요청 파라미터를 맵에 담은 것을 전달 받을 수 있다. 맵을 이용하면 필드의 정보를 문자로 된 키로 가져와야 하고, 값도 모두 스트링 타입이기 때문에 필드의 정보를 담을 클래스를 정의하는 수고는 덜지만 한편으로는 코드의 부담이 늘어날 수도 있다.
1 2 3 4 5 | @RequestMapping (value= "/user/edit" , method=RequestMethod.POST)
public String submit( @RequestParam Map<String, String> userMap) {
userService.updateUserByUserMap(user);
return "user/editsuccess" ;
}
|
어쩔 수 없이 이런 접근 방법을 사용해야 하는 경우가 있다. 폼에서 받은 정보가 특정 도메인 오브젝트에 매핑되지 않는 특별한 정보인 경우도 있다. 하지만 도메인 오브젝트에 연관된 정보라면 가능한 도메인 오브젝트 접근 방법을 사용하는 것을 권장한다. 그래야 스프링이 제공해주는 편리한 기능을 활용할 수 있으며, 좀 더 객체지향적인 코드를 만들 수 있기 때문이다.
마지막으로 이 문제에 대한 스프링의 해결책을 알아보자.
@SessionAttribute
수정 폼을 다루는 컨트롤러 작성 시 스프링의 접근 방법은 바로 세션을 이용하는 것이다. 스프링의 세션 지원 기능은 기본적으로 HTTP 세션을 사용한다. 하지만 세션 정보를 저장해두는 방법은 얼마든지 변경할 수 있다.
스프링이 지원하는 세션 기능을 활용해서 만든 수정용 컨트롤러인 다음 코드를 살펴보자. 스프링의 해결책을 적용하는 방법은 아주 간단하다. 다음과 같이 @SessionAttributes 애노테이션을 클래스 레벨에 부여하고 폼의 정보를 담을 모델 이름을 넣어주는 것이 전부다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
@Controller
@SessionAttributes ( "user" )
public class UserController {
@RequestMapping (value = "/user/edit" , method = RequestMethod.GET)
public String form( @RequestParam int id, Model model) {
model.addAttribute( "user" , userService.getUser(id));
return "user/edit" ;
}
@RequestMapping (value = "/user/edit" , method = RequestMethod.POST)
public String submit( @ModelAttribute User user) { ..user. }
}
|
기존에 만든 form() 과 submit() 메소드는 전혀 손댈 것 없이 단지 @SessionAttributes 에 "user" 라는 이름을 넣어서 클래스에 부여해주는 것만으로 앞에서 살펴봤던 모든 문제가 해결된다. @SessionAttributes 가 해주는 기능은 두 가지다. 첫째, 컨트롤러 메소드가 생성하는 모델정보 중에서 @SessionAttributes 에 지정한 이름과 동일한 것이 있다면 이를 세션에 저장해 준다. 수정 폼을 출력해주는 form() 메소드는 user 라는 이름으로 DB에서 가져온 User 오브젝트를 모델에 추가한다. 모델에 User 오브젝트를 저정하는 목적은 뷰가 이 모델을 참조해서 기존 사용자 정보를 폼에 뿌려줄 수 있게 하기 위해서다. @SessionAttributes 는 이렇게 모델정보에 담긴 오브젝트 중에서 세션 애트리뷰트로 지정한 모델이 있으면 이를 자동으로 세션에 저장해준다. 폼의 출력을 마친 후에 세션을 보면 form() 메소드에서 모델에 추가한 User 오브젝트가 저장되어 있을 것이다.
두 번째로 @SessionAttributes 가 해 주는 일은 @ModelAttribute 가 지정된 파라미터가 있을 때 이 파라미터에 전달해줄 오브젝트를 세션에서 가져오는 것이다. 원래 파라미터에 @ModelAttribute 가 있으면 해당 타입의 새 오브젝트를 생성한 후에 요청 파라미터 값을 프로퍼티에 바인딩해준다. 그런데 @SessionAttributes 에 선언된 이름과 @ModelAttribute 의 모델 이름이 동일하면, 그 때는 먼저 세션에 같은 이름의 오브젝트가 존재하는지 확인한다. 만약 존재한다면 모델 오브젝트를 새로 만드는 대신 세션에 있는 오브젝트를 가져와 @ModelAttribute 파라미터로 전달해줄 오브젝트로 사용한다. @ModelAttribute 는 폼에서 전달된 필드정보를 모델 오브젝트의 프로퍼티에 넣어준다. 폼을 출력하기 위한 컨트롤러인 form() 메소드를 먼저 거쳤다면 DB에서 가져온 User 오브젝트를 가져와 폼에서 전송해준 파라미터만 바인딩한 뒤에 컨트롤러의 user 파라미터로 넘겨준다. DB에서 처음 가져왔던 오브젝트를 그대로 사용해서 변경된 프로퍼티만 바인딩해줫으니 폼에 출력하지 않았던 프로퍼티 값도 그대로 유지된다. 이제 업데이트를 위해 user 오브젝트를 서비스 계층에 전달하기만 하면 된다. 이로써 처음 고민했던 모든 문제가 해결된다.
이 방법을 이용하면 도메인 오브젝트의 모든 정보를 그대로 유지한 채로 수정 폼에 출력한 필드만 업데이트해서 다시 서비스 계층으로 보낼 수 있다. 불필요하게 수정하지도 않을 필드를 폼에 노출한다거나 DB에 User 를 반복해서 읽거나, 서비스 계층 오브젝트와 DAO에게 폼에서 수정되는 필드가 무엇인지 알고 있도록 강요할 필요가 없다. 가장 명쾌하고 단순한 해결책이다.
@SessionAttributes 는 이름에서 알 수 있듯이 하나 이상의 모델을 세션에 저장하도록 지정할 수 있다. @SessionAttributes 의 설정은 클래스의 모든 메소드에 적용된다. 컨트롤러 메소드에 의해 생성되는 모든 종류의 모델 오브젝트는 @SessionAttributes 에 저장될 이름을 갖고 있는지 확인된다. 따라서 Model 파라미터를 이용해 저장한 모델이든, 단일 모델 오브젝트의 리턴을 통해 만들어지는 모델이든, @ModelAttribute 로 정의된 모델이든 상관없이 모두 @SessionAttributes 의 적용 후보가 된다. 마찬가지로 @ModelAttribute 로 지정된 모든 파라미터와 @ModelAttribute 가 생략됐지만 스프링이 모델로 인식하는 빈 오브젝트 등은 모두 @SessionAttributes 에 의해 저정된 세션의 값을 가져와 사용할지 확인하는 대상이다.
단, @SessionAttributes 의 기본 구현인 HTTP 세션을 이용한 세션 저장소는 모델 이름을 세션에 저장할 애트리뷰트 이름으로 사용한다는 점을 주의하자. 위의 코드에서 user 라는 세션 애트리뷰트에 User 오브젝트가 저장된다. 따라서 @SessionAttributes 에 사용하는 모델 이름에 충돌이 발생하지 않도록 주의해야 한다.
SessionStatus
@SessionAttributes 를 사용할 때는 더 이상 필요 없는 세션 애트리뷰트를 코드로 제거해 줘야 한다는 점을 잊지 말자. submit() 메소드가 정상적으로 실행돼서 세션에 저장해뒀던 User 오브젝트를 DB에 반영하고 수정 작업을 완료했다고 하자, 그런데 수정 작업이 다 끝난 뒤에도 세션에는 User 오브젝트가 여전히 남아 있다. 스프링은 @ModelAttribute 에 의해 세션에서 user 오브젝트를 꺼내오기는 하지만 이를 세션에서 제거하지는 않는다. 이를 제거하는 책임은 컨트롤러 코드에게 있다. 왜 세션을 한 번 꺼내오고서 바로 제거해 주지 않고 남겨두는 것일까? 그 이유는 폼을 한 번 서브밋했다고 해서 항상 작업이 완료되는 것이 아니기 때문이다. 세션에서 user 를 꺼내와 바인딩을 시도했는데, 사용자가 부적절한 값을 입력해서 바인딩 오류 중에 오류가 발생했다고 해보자. 이때는 수정폼을 다시 띄워주고 폼을 전송하기를 기다려야 한다. 따라서 세션에는 모델 오브젝트가 계속 남아 있어야 한다. 위저드처럼 여러 페이지에 걸쳐 진행되는 폼에서도 마찬가지다. 따라서 스프링은 언제 세션에 저장해둔 오브젝트를 제거할지 알 수 없다. 물론 세션 타임아웃에 걸려서 해당 사용자의 모든 세션이 초기화된다면 제거되긴 할 것이다.
그래서 submi() 과 같이 폼의 작업을 마무리하는 코드에서는 작업을 성공적으로 마쳤다면 다음과 같이 SessionStatus 오브젝트의 setComplete() 메소드를 호출해서 세션에 저장해뒀던 오브젝트를 제거해줘야 한다. SessionStatus 는 컨트롤러 메소드의 파라미터로 사용할 수 있는 스프링 내장 타입니다. 이 오브젝트를 이용하면 현재 컨트롤러의 @SessionAttributes에 의해 저장된 오브젝트를 제거할 수 있다.
아래 코드는 필요 없어진 세션을 깔끔하게 제거해주는 코드가 포함된 submit() 메소드의 예다.
1 2 3 4 5 6 | @RequestMapping (value = "/user/edit" , method = RequestMethod.POST)
public String submit( @ModelAttribute User user, SessionStatus sessionStatus) {
userService.updateUser(user);
sessionStatus.setComplete();
return "user/editsuccess" ;
}
|
SessionStatus 로 세션을 정리해주는 작업은 빼먹으면 안된다. 물론 빼먹는다고 기능에 문제가 될 건 없겠지만, 주로 서버의 메모리를 사용하는 HTTP 세션에 불필요한 오브젝트가 쌓여가는 것은 위험하기 때문이다. 따라서 @SessionAttributes 를 사용할 때는 SessionStatus 를 이용해 세션을 정리해주는 코드가 항상 같이 따라다녀야 한다는 사실을 기억해두자.
등록 폼을 위한 @SessionAttributes 사용
지금까지 살펴본 것처럼 수정 폼을 다루는 기능에서는 @SessionAttributes 를 반드시 사용해야 한다. 수정 화면은 폼의 내용을 출력하기 위해 먼저 DB에서 오브젝트를 가져오고, 폼을 통해 일부만 수정하기 때문에 DB에서 가져온 오브젝트를 유지해줘야 한다. 보통 수정 폼에선 필드의 대부분을 다 사용하지 않기 때문에라도 @SessionAttributes 를 이용해 DB에서 가져온 도메인 오브젝트를 유지해줘야 한다.
반면에 등록 폼의 경우는 좀 다르다. 등록 폼은 미리 DB에서 가져온 정보를 출력할 필요가 없다. 따라서 초기 모델 오브젝트를 세션에 저장해뒀다가 폼 서브밋 시 이를 가져와 프로퍼티를 넣어주는 식으로 만들지 않아도 된다. 그렇다면 등록 작업을 위한 폼 컨트롤러에서는 @SessionAttributes 를 사용할 필요가 없을까? 그냥 addSubmit() 과 같은 메소드에서 @ModelAttribute 파라미터를 선언해서 새로운 도메인 오브젝트가 만들어 지도록 하면 될까?
아주 간단한 도메인 오브젝트이고, 모든 정보는 폼을 통해 받으며, 등록과정에서 자동으로 넣어줘야 하는 정보, 예를 들어 등록시간이라든가 사용자 IP 등은 서비스 계층의 코드에서 넣어주면 된다. 서비스 계층의 addUser() 메소드는 그 자체로 모든 정보가 처음부터 들어 있는 User 를 요구하진 않을 수도 있다.
하지만 등록화면을 위한 컨트롤러에서도 @SessionAttributes 가 유용하게 쓰일 수 있다. 복잡한 도메인 오브젝트의 경우 미리 초기화를 해둘 수도 있다. 사용자의 입력을 돕기 위해 디폴트 값을 보여주는 경우도 있다. 이렇게 등록 폼을 위한 도메인 오브젝트를 미리 초기화해놓고 사용한다면, 초기화된 오브젝트를 세션에 저장해두고 사용해야 한다. 등록폼의 입력값에서도 잘못된 것이 있으면 다시 폼을 띄워서 재입력을 요구해야한다. 초기화해둔 값 중에서 폼에 출력되지 않은게 있다면 이를 유지할 수 있도록 세션을 이용해야 한다.
가장 중요한 이유는 스프링의 폼 태그를 사용하기 위해서다. 스프링의 폼 태그는 등록과 수정화면에 상관없이 폼을 출력할 때 폼의 내용을 담은 모델 오브젝트를 필요로한다. 디폴트 값을 출력하는 경우를 제외하면 등록 폼에는 초기에 아무런 값을 보여주지 않아도 좋다. 하지만 폼의 입력 값에 오류가 있어서 다시 폼을 띄울 때는 기존에 입력한 값을 보여줘야 한다. 입력 값에 오류가 하나 있다고 폼을 처음부터 다시 입력하라고 하는 건 3류 웹 사이트에서나 있을 법한 일이다. 그렇다면 등록을 담당하는 폼을 출력하는 뷰는 초기에 빈 폼을 보여주는 것과 입력에 문제가 있을 때 기존 입력값을 보여주는 두 개의 폼을 각각 만들어야 할까? 이건 너무 번거롭다. 그래서 이보다는 아예 모든 폼을 출력할 때 항상 모델의 정보를 보여주는 편이 낫다. 이렇게 하면 초기 등록폼과 등록 중에 오류가 났을 때 보여줄 폼을 구분해서 만들지 않아도 된다. 결국 처음부터 모델 오브젝트를 만들어서 폼에 그 내용을 보여줄 생각이라면 아예 최초에 폼을 출력하는 컨트롤러에게 빈 모델 오브젝트를 만들어서 리턴하는 편이 낫다. 그리고 불필요하게 오브젝트가 다시 생성되지 않도록 @SessionAttributes 를 이용해 모델 오브젝트를 저장했다가 다시 사용하는게 좋다.
결국 신규 등록을 위한 폼에서도 @SessionAttributes 를 적용하고 폼을 띄우는 컨트롤러 메소드에서 빈 오브젝트라도 모델을 만들어서 돌려주는 것이 전형적인 스프링 MVC 의 처리 방식이다.
초기 등록 화면에 미리 할당된 신규 일려번호를 보여주는 기능을 가진 등록 폼 처리 컨트롤러 메소드를 살펴보자. 아래의 addForm() 은 초기 등록 폼을 띄우는 컨트롤러다. 여기서도 수정 폼을 띄우는 메소드와 비슷하게 User 오브젝트를 만들어 모델에 넣어 준다. DB에서 가져오는 대신 직접 오브젝트를 생성해서 디폴트 값을 넣어주는 것이 다를 뿐이다. 클래스에 @SessionAttributes 를 지정했으므로 이 모델 오브젝트는 세션에 저장됐다가 폼의 서브밋을 담당하는 addSubmit() 메소드의 @ModelAttribute 파라미터를 준비하는 중에 세션에서 꺼내와서 사용하게 될 것이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @Controller
@SessionAttributes ( "user" )
public class UserController {
@RequestMapping (value = "/user/add" , method = RequestMethod.GET)
public String addForm(Model model) {
User user = new User();
user.setSerialNo(createNewSerialNo());
....
model.addAttribute( "user" , user);
return "user/edit" ;
}
@RequestMapping (value = "/user/add" , method = RequestMethod.POST)
public String submit( @ModelAttribute User user, SessionStatus sessionStatus) { ... }
}
|
지금까지 살펴본 것처럼 스프링의 @SessionAttribute 애노테이션은 스프링 MVC 의 모델 기능과 자연스럽게 결합돼서 간단한 방법으로 상태를 유지하는 애플리케이션을 만들게 해준다. 이를 잘 활용하면 여러 페이지에 걸쳐 폼의 정보를 등록해가는 위저드 페이지 형태의 애플리케이션을 만드는 것도 어렵지 않다.
하지만 상태유지 개념을 본격적으로 사용하려면 @SessionAttributes 만으로는 충분하지 않다. 본격적인 상태유지 방식의 애플리케이션을 만들려면 스프링의 포트폴리오 프로젝트인 Spring Web Flow(SWF) 를 검토해 보자. SWF는 스프링 MVC 를 기반으로 해서 매우 강력하고 편리한 상태유지 기능을 가진 애플리케이션을 손쉽게 제작할 수 있도록 지원하는 프레임워크다.
출처 - http://springsource.tistory.com/14