2012/03/12 04:41

@ModelAttribute와 @SessionAttributes의 이해와 한계 Spring MVC

@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가 대부분의 핵심기술을 어노테이션을 이용해 구현하고 있는 것도 문제다.

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


출처 - http://springmvc.egloos.com/535572



Posted by linuxism
,