도메인 객체란


Domain Object == Transfer Object == Value Object 

이게 다 같은 의미 인가요? 

저도 헷갈리네요 
보통 도메인의 의미은 업무적의미를 갖는데 Business Domian Object 
이런거입니다. facade object, value object 이 모두를 광의의미로 도메인 
오브젝트를 칭하지 않나 하는 생각이 드네요 즉 스프링프레임워크의 클라이언트 
입장의 업무클래스 전부를 ...



도메인 객체는 비즈니스 엔티티를 표현하는 녀속이고, TO는 계층간 데이터 전송, VO는 값이 한번 설정되면 변경될 수 없는 객체로 보는 게 맞는 것 같습니다. 

도메인 객체가, TO가 되고, TO가 VO로 표현되기도 하고 TO랑 VO를 따로 용어구분하지 않고 섞어 쓰기도 하지만 구분해줘야 하지 싶네요.

도메인 객체, 비즈니스 엔티티 같은 단어 바꿔쓴 말장난 같아서 덧붙이면, 비즈니스에서 표현될 수 있는 어떤 성질을 객체로 표현한거라고(예: 계좌) 봐야겠지요

사실 Core J2EE Patterns, 2nd에 보면 VO랑 TO랑 구별하지 않고 사용합니다. 

다만, 테스트 주도 개발(TDDBE) 책을 보면 뒷 부분에 Value Object Pattern을 설명하고 있고, 의도한 바는 한 가지 값을 나타내는 객체의 표현이라 값을 변경할 수 없습니다. 

TO가 패턴 맞을거에요. 가물가물. 뷰랑 컨트롤러 단에 왔다리 갔다리 하는 녀석도 TO라고 하니 퍼시스턴스단에만 해당하지는 않는 듯 합니다.



Domain Object = Data + Control Logic 
입니다.. 
Value Object에 handling할 수 있는 것이 필요하죠(VO=TO는 대략 일반적으로 같이 쓰죠) 

Kent beck 같은 냥반(OOP를 매우 강조하는 사람들)은 
데이터 있는 곳에 로직 있다고 하지만.. 
DB에 대한 제약 사항이 있는 웹 프로그래밍에서는 
진정하게 데이터 있는 곳에 로직 있게 작성하는 게 힘들죠.. 
OR mapping에서 시도하고 있으나 아직은 완벽하지 못하고요.. 

그래서 VO + Transaction Script(이건 마틴 파울러 아저씨 용어고, 우리는 보통 DAO + SQL)이 일반적인 모델이라고 할 수 있습니다.. 

VO는 immutable하냐 안하냐의 문제도 이견이 있습니다만.. 
저 개인적으로는 도메인 모델 같이 logic이 같이 포함되는게 아니라면 
immutable 한게 맞다고 생각합니다..


출처 - http://www.okjsp.pe.kr/seq/122225


===================================================================================


도메인 객체의 발전사

먼저 도메인 객체에 대해 잘 모르는 사람들을 위해 이 오브젝트를 조금 자세히 설명할 필요가 있다.과거 자바가 웹의 영역에 드러서기 시작하면서 다른 플랫폼들과 소통하기 위한 장치가 필요했는데 그것이 바로 JSR-000220, 자바빈즈였다. 자바빈은 본래 다양한 목적으로 설계되었으나 지금은 대부분이 외부 리소스를 담는 그릇같은 용도로 사용되고 있으며, 빈 생성자를 가지며 내부에 private로 설정된 프로퍼티에 get…, set…과 같은 명명규칙을 갖고 있는 클래스를 일컫는다.


이렇듯 도메인 오브젝트의 첫 시작이었던 자바빈즈는 클래스 내부에 연산이나 다양한 기능을 넣기엔 기술적 한계가 많았으므로 대부분 해당 데이터를 처리하기 전에 임시로 담아두는 정도로 밖에 사용하지 않았다. 임시적으로 발생하는 데이터를 처리하기 전까지 담아두는 도메인 객체를 일컫어 빈약한 도메인 오브젝트 방식이라도 한다.

빈약한 도메인 오브젝트를 보다 잘 이해할 수 있도록 한가지 예를 들어보자. @Controller에서 파라미터로 넘어온 값을 DB에 넣기 위해 다음과 비슷한 도메인 오브젝트를 만들어 본 기억이 있을 것이다.

public class User {
private String id;
private String password;
private String name;

public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}

…중략…

}

이런 단순한 도메인 오브젝트는 테이블의 컬럼 또는 뷰에서 넘어오는 파라미터를 기본적으로 완벽하게 이해하게끔 설계하고 있으므로 해당 요청을 처리하는 계층에서 로직이 시작되기 전에 미리 User란 그릇에 테이블의 컬럼값과 파라미터 값을 저장시켜놓고 사용할 수 있었다.

근데 이런 도메인 오브젝트를 계속 사용하다보니 조금 헷갈리기 시작한다. 왜냐하면 따지고 보면 도메인 오브젝트가 가장 객체의 본질을 잘 표현해주는 주된 형태인데 이걸 User 오브젝트 자신보다 연관성이 떨어지는 UserService나 UserDao, UserController같은 곳에서 대신 컨트롤 하고 있으니 말이다.

혹시나 이해가 잘 안된 사람을 위해서 좀 더 자세히 설명하고자 한다. 객체지향적인 관점에서 말해보자면 도메인 오브젝트는 객체 자신에 그 무엇보다 가깝다. 현재의 자바기술로 도메인 오브젝트는 바로 객체 자신이라고까지 할 수 있으며 비록 잠시 태어나 소멸할지언정 가장 명확하게 객체 자신을 표현하고 있다는 오브젝트였다는 말이다.

너무 중요한 단락이기에 더욱 쉽게 설명해보겠다. 예를 들어 나는 초밥을 좋아하고 키가 178cm에 단정한 머리 스타일이고 …블라블라… 여하튼 나 자신을 너무 잘 알고 있다. 근데 나를 만든 개발자 때문에 나는 어디든 내 마음대로 움직일 수 없다. 이 개발자는 내가 스스로 못 움직이게끔 바닥에 나를 고정시켜놓고는 대신 나와 비슷한 녀석들을 대량으로 처리하는 이상한 기계에 맡겨 나를 조종하고 있다. 나는 저 기계보다 내 자신이 누군지 잘 이해하고 행동할 자신이 있는데 개발자 때문에 내가 누군지도 잘 모르는 기계가 나를 다스리도록 하고 있는 셈이다.

이제 이해가 조금 됬는지 모르겠다. 우리가 객체지향 프로그래밍이라 부르는 것은 바로 사물을 객체지향적인 관점으로 바라보고 그대로 해석할 수 있는 언어이기 때문에 객체지향적이라고 일컫는 것이다. 근데 빈약한 오브젝트 방식은 이런 객체지향적인 관점을 어느 정도 훼손하고 있다. 물론 보다 능률적인 대량처리를 위해 빈약한 오브젝트 방식이 결코 나쁘다고는 할 수는 없지만 한편으로는 결코 좋은 방식이라고도 할 수 없는 것이다.

그렇다면 이 빈약한 오브젝트를 조금 풍성하게 해보면 어떨까? 다음과 같은 소스처럼 말이다.

public class User {
private String id;
private String password;
private String name;

public int getToken() {
return getId() + getPassword();
}

… 중략…
}

다시 말하지만 도메인 오브젝트는 자신을 가장 잘 이해하고 있으므로 getToken() 메서드를 효율적으로 처리할 수 있는 로직을 가장 직관적으로 구현할 수 있다. 바로 아래와 같이 말이다.

public class User {
private String id;
private String password;
private String name;

private UserDao userDao;

@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public int getToken() {
return getId() + getPassword();
}
public String getId() {
Assert.hasText(id);
return id;
}
public String getPassword() {
if(password==null) return userDao.getPassword(id);
else return password;
}

… 중략 …
}

위의 코드는 여러모로 부족한 점이 많지만 중요한 것은 getToken()의 무결성을 보장해준다는 것이다. 만약 이 로직을 도메인 오브젝트 외의 Service 계층이나 Dao계층에서 처리 해야했다면 굉장히 이해가 불편함과 동시에 무결성을 보장하기가 까다로웠을 것이다. 하지만 우리는 위와 같이 도메인 오브젝트가 자기 자신과 관련한 비지니스 로직은 스스로 처리하게 하고 그 무결성을 스스로 보장하게끔 한 덕분에 매우 직관적이고 깔끔한 코드를 얻을 수 있게 되었다.

결론적으로 DDD는 도메인 자체에 더 많은 권한과 기능을 부여함으로써 도메인이 가질 수 있는 능력을 극대화 시키고 주도해 나가는 개발방식이며 더 나아가 도메인을 하나의 계층으로 인정하고 다른 영역과 분리해내는 4Tier 아키텍쳐인 것이다.

물론 위의 코드는 여러모로 문제점이 많다. 우선 도메인 객체에 적용될 수 없는 @Autowired와 같은 DI기술이 적용됬다는 점이며 지금은 보이지 않지만 해당 도메인이 DAO 계층을 흡수한 만큼 DB에 접근할 수 있게된 권한으로 자칫 엄청난 불상사를 초래할 수 있다는 점이다.

하지만 이런 문제점은 스프링의 @Configurable과 AOP 기술, 그리고 DTO 모델을 도입함으로써 해결할 수 있는 문제점들이기 때문에 DDD 주도의 개발을 막지는 못한다. 되려 DDD는 개발을 보다 쉽고 빠르게 도와주며 코드 자체를 직관적으로 바꿔주어 이용자의 부담과 불필요한 이해를 막아준다.


글을 마치며...

도메인 오브젝트는 이렇듯 단순한 자바빈에서 시작하여 조금씩 기능을 더해오더니 이제 개발의 주도적인 역할까지 수행하는 단계에 이르게 되었다. 이렇듯 도메인 오브젝트를 적극 활용하여 개발을 하게 되면 소스의 테스트가 더욱 쉬워지고 활용도 또한 극대화 된다. 그렇다고 무작정 도메인 오브젝트에 모든 기능을 씌우려고 해서는 절대로 안된다. 왜냐하면 DDD를 적용하는 것은 어디까지나 직관적이고 객체지향적인 설계를 위한 것이지 무분별한 남용을 위한 것은 아니기 때문이다.

만약 도메인 오브젝트에 존재하지 않는 외부값의 첨가가 필요하다면 이런 로직은 필히 해당 도메인의 Service 계층, 또는 Support 계층에서 전담하여 처리하고 도메인 오브젝트는 오로지 본인 내부의 리소스만을 조합하여 처리할 수 있는 로직만을 담당하여야 한다.

이렇게 도메인 오브젝트의 발전사를 마쳤다. 필자는 멋도 모르고 코딩을 해대던 과거 시절, 이미 자바빈 객체를 내멋대로 확장하여 사용한 적이 있었는데 그 당시 MVC를 알게 되고 부터는 자바빈에 기능을 부여하는 것이 잘못된 것으로만 알고 있었다. 헌데 이젠 DDD란 이름으로 예전의 나의 코딩방식이 또다시 새로운 패러다임이라고 조명받고 있으니 조금 우습기도 했다.

사람이란게 말을 함부로 바꿔서는 안되는 모양인가 보다.


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





'Framework & Platform > Spring' 카테고리의 다른 글

Spring Security 이해  (1) 2012.05.23
Spring - @mvc -@Pattern  (0) 2012.05.23
spring - @SessionAttributes 와 SessionStatus  (0) 2012.05.21
Spring - Validation  (0) 2012.05.21
spring - Validator  (0) 2012.05.21
Posted by linuxism
,



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








'Framework & Platform > Spring' 카테고리의 다른 글

Spring - @mvc -@Pattern  (0) 2012.05.23
도메인 객체(오브젝트)  (0) 2012.05.21
Spring - Validation  (0) 2012.05.21
spring - Validator  (0) 2012.05.21
Spring - @ModelAttribute  (0) 2012.05.21
Posted by linuxism
,

13.Validation

Spring에서는 사용자가 입력한 값에 대한 유효성을 체크하기 위해 Spring Validator 또는 JSR-303 Validator를 사용할 수 있도록 지원하고 있다.

13.1.Spring Validator

Spring MVC에서는 Spring Validator를 이용하여 입력 필드의 값에 대해 Validation Check를 수행하고 Errors 객체를 통해 에러 메시지를 출력해 줄 수 있도록 지원한다. 또한 Errors 객체에 담겨진 에러 메시지는 jsp 페이지에서 form:errors 태그를 통해 출력될 수 있다.

13.1.1.Validator 생성

  • ValidatorUtils 사용

    필수 입력 필드에 대해 Validation Check를 수행하고 에러 메시지를 출력할 수 있도록 지원한다. 이것은 ValidatorUtils를 사용하여 간단히 구현할 수 있다. 다음은 Validator 인터페이스를 구현한 UserValidator.java 클래스의 일부이다.

    public class UserValidator implements Validator {
        public boolean supports(Class clazz) {
            return UserVO.class.isAssignableFrom(clazz);
        }
    			
        public void validate(Object object, Errors errors) {
            // validationUtils를 이용하여 입력값이 비었는지 체크
              ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName",
                "required", new Object[] { "userName" }, "Enter your name");
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password",
                "required", new Object[] { "password" }, "Enter your password");
  • Errors 사용

    Validation Check 결과 발생된 Error를 Errors 객체를 사용하여 저장함으로써 해당 필드에 대해 정의된 에러 메시지를 출력할 수 있도록 지원하며 그 예는 다음과 같다.

    public class UserValidator implements Validator {
        public boolean supports(Class clazz) {
            return HelloVO.class.isAssignableFrom(clazz);
        }
    
        public void validate(Object object, Errors errors) {
            
            HelloVO helloVO = (HelloVO) object;
            if (helloVO.getPassword().length() < 6)
                errors.rejectValue("password", "error.password.tooshort");
    
            if (!helloVO.getPassword().equals(helloVO.getConfirmPassword()))
                errors.rejectValue("confirmPassword", "error.confirm");
        }
    }

    Validation Error가 있는 경우 메시지 리소스 파일에 미리 정의된 error.password.tooshort, error.confirm 등의 메시지가 출력될 것이다.

13.1.2.Validator 활용

생성한 Validator를 활용하기 위해서는 해당 Validator를 Inject하여 사용하거나 Controller 클래스 내에 @InitBinder 메소드를 정의하고 해당 메소드의 입력 인자로 전달된 Binder에 해당 Validator를 셋팅하여 활용할 수 있다.

13.1.3.<form:errors> 태그 사용

Validation Error를 JSP 페이지에서 쉽게 출력하기 위해 Spring MVC에서 제공하는 form 태그 중 <form:errors> 태그를 사용할 수 있다. 이 태그를 사용하기 위해서는 다음과 같은 절차를 따르도록 한다.

  • 태그 라이브러리 등록

    Spring form 태그 라이브러리를 사용하기 위해서는 spring-form.tld 파일이 필요하며 이는 spring-webmvc-x.x.x.jar 파일에 포함되어 있다. 이 form 태그를 사용하기 위해서는 JSP 페이지에 다음과 같이 taglib 정의가 추가되어야 한다.

    <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
  • <form:form> 태그 사용

    form 태그를 사용하려면 commandName 속성을 지정해야 하는데 이 이름은 JSP 페이지에서 사용되는 commandName과 일치해야 하며 commandClass와 같은 타입의 객체이어야 한다. commandName에 특정 이름을 부여하지 않으면 기본 값은 command로 셋팅된다. form 태그는 여러가지 폼 입력 태그들을 갖는다. 그 중, Validation Error 표현을 위한 태그는 <form:errors>이며 이 태그는 속성으로 path를 가진다. path 값으로 "*" 값을 주게 되면 commandClass가 가지는 모든 속성에 대한 Error 메시지를 출력하게 된다. 다음은 <form:errors> 태그가 정의되어 있는 getUser.jsp 파일의 일부이다.

    <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
    <tr>
        <td> Name :</td>
        <td><form:input path="userName" />(required)</td>
        <td><form:errors path="userName" /></td>
    </tr>
    <tr>
        <td>password :</td>
        <td><form:password path="password" />(required, 6자이상입력)</td>
        <td><form:errors path="password" /></td>
    </tr>

13.2.Spring 3 Validation

Spring 3 이후부터는 Bean Validation에 대한 표준을 정의한 JSR-303 Spec.을 지원하고 있다. Validation은 선언적인 형태와 프로그램적인 형태로 구분할 수 있으며 Hibernate Validator와 같은 JSR-303 Spec.을 구현한 구현체를 연계하여 처리된다.

13.2.1.JSR-303 (Bean Validation) Basic

JSR-303은 Bean Validation을 위한 표준을 정의하고 있으며 특정 어플리케이션을 구성하는 도메인 클래스에 대해 JSR-303 Annotation을 활용하여 Validation Constraints를 부여하게 되면 런타임시에 이를 기준으로 Validation Check가 이루어지게 된다. 다음은 JSR-303 Spec.에서 제시한 Annotation 목록이다.

AnnotationSupported Type

Description

@AssertFalseboolean, Boolean해당 속성의 값이 false인지 체크한다.
@AssertTrueboolean, Boolean해당 속성의 값이 true인지 체크한다.
@DecimalMax

BigDecimal, BigInteger, String, byte, short, 

int, long and primitive type에 대한 wrappers

해당 속성이 가질 수 있는 최대값을 체크한다.
@DecimalMinBigDecimal, BigInteger, String, byte, short,
int, long and primitive type에 대한 wrappers
해당 속성이 가질 수 있는 최소값을 체크한다.
@DigitsBigDecimal, BigInteger, String, byte, short,
int, long and primitive type에 대한 wrappers
해당 속성이 가질 수 있는 정수부의 자리수와
소수부의 자리수를 체크한다.
@Futurejava.util.Date, java.util.Calendar해당 속성의 값이 현재일 이후인지 체크한다.
@MaxBigDecimal, BigInteger, String, byte, short,
int, long and primitive type에 대한 wrappers
해당 속성이 가질 수 있는 최대 Length를 체크한다.
@MinBigDecimal, BigInteger, String, byte, short,
 int, long and primitive type에 대한 wrappers
해당 속성이 가질 수 있는 최소 Length를 체크한다.
@NotNullany type해당 속성의 값이 Null이 아닌지 체크한다.
@Nullany type해당 속성의 값이 Null인지 체크한다.
@Pastjava.util.Date, java.util.Calendar해당 속성의 값이 현재일 이전인지 체크한다.
@PatternString해당 속성의 값이 정의된 Regular Expression에
부합하는지 체크한다. Regular Expression은
Java Regular Expression Convention
(see java.util.regex.Pattern)에 맞게 정의해야 한다.
@SizeString, Collection, Map and arrays해당 속성이 가질 수 있는 최대, 최소 Length를 체크한다.
@Validany non primitive type해당 객체에 대해 Validation Check가 이루어진다.


이 외에도 JSR-303 구현체별로 Validation Constraint 정의를 위한 Custom Annotation을 추가로 제공하기도 한다.

다음은 Foundation Plugin 설치로 추가된 도메인 클래스 ~/domain/Movie.java의 일부로써 앞서 언급한 JSR-303 Annotation을 활용하여 Validation Constraint를 정의하고 있다. 예를 들어, title 속성은 Null 값을 가질 수 없으며 최소 1자리, 최대 50자리까지만 허용하며 runtime 속성값은 최대 180을 초과할 수 없고 정수부 3자리 소수부는 0자리를 허용하고 있음을 알 수 있다.

public class Movie implements Serializable {

    private static final long serialVersionUID = 1L;
	
    private String movieId;

    @NotNull
    @Size(min = 1, max = 50)
    private String title = "";
	
    @NotNull
    @Size(min = 1, max = 50)
    private String director;

    private Genre genre;

    @NotNull
    @Size(min = 5, max = 100)
    private String actors;

    @DecimalMax(value = "180")
    @Digits(integer=3, fraction=0)
    private int runtime;

    @DateTimeFormat(iso = ISO.DATE)
    @Future
    private Date releaseDate;

    @NumberFormat(pattern = "#,###")
    @Digits(integer=5, fraction=0)
    private int ticketPrice;

    private String posterFile;

    private String nowPlaying = "Y";

    // getter, setter ...
}

13.2.2.JSR-303 (Bean Validation) Optional

JSR-303 Spec.을 준수하는 모든 Annotation은 Annotation별 속성 외에 payload, groups, message라는 속성을 공통적으로 가진다. 각 속성이 가지는 의미에 대해 살펴보도록 하자.

  • payload (Programmatic Validating의 경우 활용 가능) : 사용된 Validation Constraint와 관련된 메타 정보를 정의하는데 사용된다. 특정 Constraint에 대해 payload 속성의 값으로 심각도를 정의해두면 Validation Error가 발생하였을 경우 심각도 정보를 추출할 수 있게 된다. 다음은 payload 정보가 추가 정의된 도메인 클래스의 일부로 title, director의 Validaion Constraint에 대해 Severity라는 클래스의 Error와 Warning 클래스로써 payload 값을 부여하고 있음을 알 수 있다.

    public class Movie implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        private String movieId;
    
        @NotNull(payload = Severity.Error.class)
        @Size(min = 1, max = 50, payload = Severity.Warning.class)
        private String title = "";
    
        @NotNull(payload = Severity.Error.class)
        @Size(min = 1, max = 50, payload = Severity.Warning.class)
        private String director;
    
        // ...
    }

    다음은 위에서 언급한 Severity 클래스의 모습이다. 내부에 Warning, Error라는 클래스 정의를 포함하고 있으며 이들 각각은 javax.validation.Payload를 상속받고 있음에 유의해야 한다.

    public class Severity {
        public static interface Warning extends Payload {
        };
    
        public static interface Error extends Payload {
        };
    }

    위와 같이 코드가 구성된 경우 Validation Error를 담고 있는 ConstraintViolation 객체의 getConstraintDescriptor().getPayload() 메소드를 호출함으로써 Payload 정보를 추출할 수 있다.

    Set<ConstraintViolation<Movie>> constraintViolations = validator.validate(movie);
    System.out.println("the number of constraint violation is "	+ constraintViolations.size());
    
    Iterator<ConstraintViolation<Movie>> iterator = constraintViolations.iterator();
    
    while (iterator.hasNext()) {
        ConstraintViolation<Movie> constraintViolation = iterator.next();
        Set payloads = constraintViolation.getConstraintDescriptor().getPayload();		
        // ...
    }

    위에서 언급한 payload 샘플 코드는 본 섹션 내의 다운로드 - anyframe.sample.validation. payload를 통해 다운로드받을 수 있다.

  • groups (Programmatic Validating의 경우 활용 가능) : 사용된 Validation Constraint의 그룹 정보를 정의하는데 사용된다. 일부 Constraint에 대해 동일한 그룹을 부여하게 되면 특정 그룹에 대해서만 Validation 작업을 수행할 수 있게 된다. 예를 들어, 특정 도메인 객체가 생성되는 시점의 Validation Check 대상 속성들과 해당 도메인 객체가 수정되는 시점의 Validation Check 대상 속성들이 다를 수 있기 때문에 이들에 대해 그룹을 부여하고 그룹별로 Validation을 수행하고자 하는 경우 활용할 수 있다. 다음은 groups 정보가 추가 정의된 도메인 클래스의 일부로 title, director, actors에 대해서는 Draft, Playing이라는 그룹을 부여하고 runtime, releaseDate, ticketPrice에 대해서는 Playing이라는 그룹만 부여하고 있음을 알 수 있다.

    public class Movie implements Serializable {
        private static final long serialVersionUID = 1L;
    
        private String movieId;
    
        @NotNull(groups = { Draft.class, Playing.class })
        @Size(min = 1, max = 50, groups = { Draft.class, Playing.class })
        private String title = "";
    
        @NotNull(groups = { Draft.class, Playing.class })
        @Size(min = 1, max = 50, groups = { Draft.class, Playing.class })
        private String director;
    
        @NotNull(groups = { Draft.class, Playing.class })
        @Size(min = 5, max = 100, groups = { Draft.class, Playing.class })
        private String actors;
    
        @DecimalMax(value = "180", groups = Playing.class)
        @Digits(integer = 3, fraction = 0, groups = Playing.class)
        private int runtime;
    
        @Future(groups = Playing.class)
        private Date releaseDate;
    
        @Digits(integer = 5, fraction = 0, groups = Playing.class)
        private int ticketPrice;
    	
        // ...
    }

    즉, 영화가 등록될 당시(Draft Group)에는 title, director, actors에 대해서만 Validation Check가 이루어지고 영화 상영이 결정된 이후(Playing Group)부터는 runtime, releaseDate, ticketPrice에 대해서도 추가적으로 Validation Check가 이루어질 수 있도록 하기 위함이다.

    다음은 위에서 groups 정의시 활용한 Draft.java 클래스의 모습이다. group 클래스는 javax.validation.groups.Default 유형이어야 하며, group 클래스 사이에서 계층 관계를 가질 수 있다. 그리고 하위 계층 그룹에 대해 Validation Check 요청이 있을 경우 상위 계층에 대한 Validation Check도 함께 이루어지게 된다. groups 속성값이 정의되지 않은 경우 Default group으로 간주된다.

    public interface Draft extends Default {
    }

    위와 같이 코드가 구성된 경우 Validator의 validate() 메소드 호출시 group 정보를 인자로 전달하면 해당 group에 속한 속성 정보에 대해서만 Validation Check가 수행된다.

    Set<ConstraintViolation<Movie>> constraintViolations = validator.validate(movie, Draft.class);
    System.out.println("the number of constraint violation is "	+ constraintViolations.size());
    
    Iterator<ConstraintViolation<Movie>> iterator = constraintViolations.iterator();
    
    while (iterator.hasNext()) {
        ConstraintViolation<Movie> constraintViolation = iterator.next();	
        // ...
    }

    위에서 언급한 groups 샘플 코드는 본 섹션 내의 다운로드 - anyframe.sample.validation. groups를 통해 다운로드받을 수 있다.

  • message : Validation Error가 발생하였을 경우 표현되는 메시지를 정의하는데 사용된다. 기본적으로 사용중인 Validator를 포함하는 라이브러리 내에 포함된 메시지 리소스 파일로부터 해당 Annotation의 {fully-qualified class name}.message에 해당하는 메시지 값을 추출하게 된다. 예를 들어, @NotNull Check시 에러가 발생하면 javax.validation.constraints.NotNull.message에 해당하는 메시지가 표현될 것이다. 기본적으로 제공되는 메시지가 아닌 다른 메시지를 표현해주고 싶을 경우에는 message의 속성값으로 신규 메시지 key를 정의하면 된다. 그리고 클래스패스 상위에 해당 key와 이에 대한 메시지를 포함하고 있는 메시지 리소스 파일을 정의한다.

    메시지 리소스 파일에 대해서는 기본적으로 국제화가 지원되며, Hibernate Validator의 경우 기본적으로 영어,불어,독일어 형태의 메시지 리소스 번들을 제공하고 있는데 만일 다른 언어로 구성된 메시지 리소스 파일을 추가하고자 원한다면 클래스패스 내에 org/hibernate/validator/ValidationMessages_{locale}.properties 파일을 추가하고 JSR-303 Annotation 각각에 대한 메시지를 정의하도록 한다.

13.2.3.Custom Constraints

JSR-303에서 기본적으로 제공하는 Annotation만으로 특정 도메인 클래스의 속성값에 대한 Validation Check가 수행되기 어려운 경우 프로젝트에 적합한 Custom Constraints를 정의할 수 있다. Custom Constraints를 활용하기 위해서는 Custom Annotation과 Custom Validator 구현이 이루어져야 한다. 다음은 전화번호 속성에 대한 Validation Check를 위해 신규 정의한 Telephone.java 클래스의 일부이다.

@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = TelephoneValidator.class)
@Size(min = 12, max = 13)
public @interface Telephone {
    String message() default "{anyframe.sample.validation.constraint.Telephone.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

위 코드에 의하면 @Telephone은 Method, Field에 대해 정의 가능하며 런타임시에 적용된다. 그리고 도메인 클래스 내에 @Telephone이 부여된 속성을 만나면 TelephoneValidator가 초기화되어 Validation Check를 수행할 것이다. 또한 @Size Annotation 정의가 추가되어 있어서 @Telephone은 기본적으로 Size에 대해서도 제약하게 된다. @Telephone은 JSR-303 Spec.에서 정의한 기본 속성(message, groups, payload) 외에 추가 속성을 포함하고 있지는 않다.

message 속성의 경우 기본값을 anyframe.sample.validation.constraint.Telephone.message으로 정의하고 있으므로 @Telephone에 대한 Validation Check 관련 Error가 발생한 경우 클래스패스 최상위의 ValidationMessages.properties 파일로부터 anyframe.sample.validation.constraint.Telephone.message을 key로 하는 메시지가 출력될 것이다. 다음은 ValidationMessages.properties 파일의 내용이다.

anyframe.sample.validation.constraint.Telephone.message=must match "0000-000(or 0000)-0000" (max 13)

다음은 @Telephone Annotation에 대해 Validation Check를 수행할 TelephoneValidator.java 파일의 일부이다. 다음 코드에서와 같이 Custom Validator는 javax.validation.ConstraintValidator 인터페이스를 implements해야 하며 Validation Check 로직을 수행할 isValid()라는 메소드를 구현해주어야 한다.

public class TelephoneValidator implements ConstraintValidator<Telephone, String> {
    private java.util.regex.Pattern pattern = java.util.regex.Pattern
        .compile("^[0-9]\\d{2}-(\\d{3}|\\d{4})-\\d{4}$");

    public void initialize(Telephone annotation) {
    }

    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null || value.length() == 0) {
            return true;
        }
        Matcher m = pattern.matcher(value);
        return m.matches();
    }
}

TelephoneValidator는 Regular Expression을 이용하여 전화번호에 대한 패턴을 정의해 두고 이 패턴과 동일하지 않을 경우 Validation Error를 발생하게 된다.

다음은 앞서 정의한 @Telephone 정의를 포함하고 있는 도메인 클래스 Movie.java 파일의 일부이다.

public class Movie implements Serializable {

    private static final long serialVersionUID = 1L;
    
    private String movieId;

    @Telephone
    private String telephone;			

    // ...
}

위에서 언급한 Custom Constraint 샘플 코드는 본 섹션 내의 다운로드 - anyframe.sample.validation. custom를 통해 다운로드받을 수 있다.

13.2.4.Declarative Validating

Spring MVC 2.5 이전에서는 앞서 언급한 바와 같이 Spring Validator를 구현하고 이를 특정 Controller의 Validator로 직접 지정해 주어야만 Validation Check가 이루어졌었다. 그러나 Spring 3 이후부터는 Controller 메소드의 입력 인자에 대해 @Valid라는 Annotation을 부여함으로써 해당 메소드 호출 전에 자동적으로 Validation Check가 이루어질 수 있도록 지원한다. 다음은 Foundation Plugin 설치로 추가된 ~/foundation/moviefinder/web/MovieController.java 클래스 내에 정의된 create() 메소드의 일부이다.

@RequestMapping(params = "method=create")
public String create(..., @Valid Movie movie, BindingResult results, ...) throws Exception {

    if (results.hasErrors()) {
        return "foundationViewMovie";
    }
    
    // ...
}

위 메소드의 경우, 사용자의 입력값을 Movie 객체로 매핑할 때 Validation Check가 이루어지게 되고 결과값은 BindingResult 객체에 담겨지게 된다. 따라서 입력 인자로 전달된 BindingResult 객체 내에 Validation Error가 존재하는 경우 입력 화면으로 되돌아가도록 로직을 구성하면 된다.

또한 Spring에서 제공하는 <form:errors>를 활용하면 Validation Error를 입력 화면에 표현해 줄 수 있게 된다. 다음은 Foundation Plugin 설치로 추가된 webapp/WEB-INF/jsp/foundation/moviefinder/movie/form.jsp 파일의 일부로 title, director 필드에 입력된 값이 유효하지 않을 경우 <form:errors>를 이용하여 표현해 줄 수 있도록 정의하고 있음을 알 수 있다.

<tr>
    <td width="150" class="ct_td"><spring:message code="movie.title" />&nbsp;*</td>
    <td bgcolor="D6D6D6" width="1"></td>
    <td class="ct_write01">
        <form:input path="title" cssClass="ct_input_g" cssErrorClass="text medium error" size="40" maxlength="50" />
        <form:errors path="title" cssClass="errors" />
    </td>
</tr>
<tr>
    <td height="1" colspan="3" bgcolor="D6D6D6"></td>
</tr>
<tr>
    <td width="150" class="ct_td"><spring:message code="movie.director" />&nbsp;*</td>
    <td bgcolor="D6D6D6" width="1"></td>
    <td class="ct_write01">
        <form:input path="director" cssClass="ct_input_g" cssErrorClass="text medium error" size="40" maxlength="50" />
        <form:errors path="director" cssClass="errors" />
    </td>
</tr>

끝으로 선언적인 Validation Check를 위해서는 Validator 지정을 위한 속성 정의가 필요하다. Spring에서는 이를 위해 3가지 방법을 제공한다.

  • Spring 3에서 새롭게 선보이는 mvc namespace를 활용하는 것으로 다음과 같이 정의된 경우 클래스패스로부터 Hibernate Validator와 같은 JSR-303 Validator 구현체가 자동으로 검색되어 모든 @Controller에 적용된다.

    <mvc:annotation-driven />
  • Spring 3에서 새롭게 선보이는 mvc namespace를 활용하되 특정 Validator를 지정하는 것으로 지정된 Validator가 모든 @Controller에 적용된다.

    <mvc:annotation-driven validator="..."/>
  • Controller 클래스 내에 @InitBinder 메소드를 정의하고 해당 메소드의 입력 인자로 전달된 Binder에 특정 Validator를 셋팅하는 것으로, 이 경우 셋팅된 Validator가 특정 Controller에만 적용된다.

    @Controller
    public class MovieController {
    
        @InitBinder
        protected void initBinder(WebDataBinder binder) {
            binder.setValidator(new CustomValidator());
        }
        
        // ...
    }

13.2.5.Programmatic Validating

Spring에서는 Validation Check가 필요한 경우에 Hibernate Validator와 같은 JSR-303 Validator 구현체를 실행시킬 수 있도록 하기 위해 LocalValidatorFactoryBean 클래스를 제공한다. LocalValidatorFactoryBean은 클래스패스 내에 JSR-303 구현체와 관련된 라이브러리를 검색하여 Validator를 자동으로 검색해주는 역할을 수행한다. 따라서 LocalValidatorFactoryBean을 Bean으로 정의하고 특정 클래스에서 이 Bean을 참조하여 Validation Check를 수행하면 된다.

<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
@Service
public class MovieServiceImpl implements MovieService {
    /**Inject a reference to javax.validation.Validator if you prefer to work with the JSR-303 API directly.
     * Inject a reference to org.springframework.validation.Validator if your bean requires the Spring Validation API
     */
    @Inject
    private Validator validator;

    public void create(Movie movie){
        validator.validate(movie);
        // ...
    }
}

13.3.Resources

  • 다운로드

    다음에서 sample 코드를 포함하고 있는 Eclipse 프로젝트 파일을 다운받은 후, 압축을 해제한다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후, mvn compile exec:java -Dexec.mainClass=...이라는 명령어를 실행시켜 결과를 확인한다. 각 Eclipse 프로젝트 내에 포함된 Main 클래스의 JavaDoc을 참고하도록 한다.

    • Eclipse 기반 실행

      Eclipse에서 압축 해제 프로젝트를 import한 후, src/main/java 폴더 하위의 Main.java를 선택하고 마우스 오른쪽 버튼 클릭하여 컨텍스트 메뉴에서 Run As > Java Application을 클릭한다. 그리고 실행 결과를 확인한다.

    표 13.1. Download List

    NameDownload
    anyframe.sample.validation.payload.zipDownload
    anyframe.sample.validation.groups.zipDownload
    anyframe.sample.validation.custom.zipDownload


출처 - http://dev.anyframejava.org/docs/anyframe/plugin/foundation/4.6.1/reference/html/ch13.html








스프링 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(regexp=, 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에 대해 완벽하게 다루고 싶었지만 생각보다 복잡한 방식으로 이루어지고 있는데다 확장포인트를 잡기가 어려워 여기서 대략적인 설명만 달아놓을 수 밖에 없었다.

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


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








Spring3.0 에서 Validation 기능을 보면 이전 버전
에서 사용했던 Validation 방법 뿐만 아니라
새로운 기능들이 추가가 되었습니다.
그중에서 단연 꽃은 JSR-303 스펙 입니다.
혹자는 이것을 Spring 3.0 Validation이라고 합니다.
JSR-303스펙은 쉽게 말해서 자바의 모든 클래스 자체에
검증 로직을 annotation으로 설정 해서 검증이
필요한 어느 곳이든 해당 검증을 수행 할 수 있도록
하는 J2EE Spec 입니다.
ㅋㅋ 저 답지 않게 유식한 척을 했군요
아래 코드를 보면서 간략 하게 설명 드리겠습니다.

public class PersonForm {

@NotNull
@Size(max=64)
private String name;

@Min(0)
private int age;

}


위에 코드를 보면 name 속성은 반드시 값이 존재 해야 하고,
length는 64자를 넘어서는 안된 다는 것입니다.
이렇게 클래스에 검증 로직을 넣고 검증이 필요한 곳
에서 실행을 하면 됩니다.


    * Library 설정

먼저 Spring 3.0 에서 JSR-303 스펙을 구현 하기 위해서는
아래의 라이브러리가 필요 합니다.
(1) jax-validation.jar (JSR-303 스펙 라이브러리)
(2) hibernate-validation.jar (JSR-303 구현체 라이브러리)
위 의 두개의 라이브러리를 다운 받아서 이클립스 classpath
를 설정 합니다.
만약 Maven을 사용 하시는 분들은 아래의 pom.xml을
추가 합니다.

<repositories>
<repository>
<id>jboss</id>
<url>http://repository.jboss.com/maven2</url>
<releases>
   <enabled>true</enabled>
</releases>
<snapshots>
   <enabled>false</enabled>
</snapshots>
</repository>
</repositories>

<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.0.0.Beta2</version>
</dependency>
</dependencies>


스프링 3.0 full pom.xml 설정을 원하 시는 분들은
제 블러그 “Spring3.0 관련 라이브러리 (pom.xml)”
을 참조 하시면 됩니다.

    * 검증 클래스 작성


package org.springshowcase.mvc;

import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.hibernate.validation.constraints.NotEmpty;


public class User {

@NotEmpty()
@Size(min=1,max = 5)
@Pattern(regexp=”[0-9a-zA-Z]”)
private String id;

@NotEmpty()
private String name;

@NotEmail
private String email;

private Integer age;

 …setter & getter method
}


위의 소스를 보면 전형적인 일반 자바 빈즈
클래스 형태 입니다.
“id” 속성의 검증 로직은 아래와 같습니다.
- 반드시 값이 존재 해야함
- 반드시 길이는 1이상 5이하여야 함 (byte 기준 아님)
- 반드시 영문 또는 숫자 형태여야함
“email” 속성에서 “@NotEmail”은 제가 직접
만든 annotation입니다. 이부분은 뒤에서
설명 하도록 하겠습니다.

    * Spring 빈 설정

스프링에서 JSR-303 스펙을 적용 하기 위해서는
다소 복잡한 과정을 거쳐야 합니다.
“LocalValidatorFactoryBean” 빈을 설정 하고 또 이 빈을
“AnnotationMethodHandlerAdapter” 선언된 빈에
injection 해야 하고…
이런거 필요 없이 spring3.0 에서 아주 간단한
네임스페이스를 제공 합니다.

<mvc:annotation-driven />


위와 같이 선언 하면 스프링에서 JSR-303 적용은
끝입니다.

Comming Up Next…

- Controller에서 Validation 사용 하는 방법
- JSR-303 기반 Message 번들 적용 방법
- JSP 설정 방법
- 커스컴 Validation Annotation 작성 방법
- Spring Validation VS JSR 303


Spring 3.0 Validation Part1“에 이어서 설명 드리도록 하겠습니다.
먼저 이전에 작성한 User 도메인 클래스 입니다.

package org.springshowcase.mvc;

import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.hibernate.validation.constraints.NotEmpty;


public class User {

@NotEmpty()
@Size(min=1,max = 5)
@Pattern(regexp="[0-9a-zA-Z]")
private String id;

@NotEmpty()
private String name;

@NotEmail
private String email;

private Integer age;

...setter & getter method
}


* Controller에서 Validation 사용 하는 방법

Controller 에서 사용 하는 방법은 의외로 간단 합니다.

@RequestMapping(method = RequestMethod.POST)
public ModelAndView processSubmit(@Valid @ModelAttribute("user") User user,
BindingResult result, SessionStatus status) {
ModelAndView mav = new ModelAndView();
if (result.hasErrors()) {
System.out.println(".....invalid");
}
return mav;
}


위의 소스를 보면 검증을 위해서 파라미터 부분에
“@Valid” 어노테이션을 설정하면 끝입니다.
즉 저말은 User 도메인에 검증 로직을 실행 하라
는 뜻입니다. 이미 검증이 끝났기 때문에
“result.hasErrors”를 통해서 validation
분기 로직을 적절하게 작성 하면 됩니다.

* JSP 설정 방법

<form:input path="id" /> <br />
<font color="yellow"><form:errors path="id" /></font><br />
<label for="contact_email">Enter user name: </label> <br />
<form:input path="name" /><br />
<font color="yellow"><form:errors path="name" /></font><br />
<label for="contact_email">Enter email: </label> <br />
<form:input path="email" /><br />
<font color="yellow"><form:errors path="email" /></font><br />
<label for="contact_subject">Enter Age: </label> <br />
<form:input path="age" /> <br /><br />
<input type="button" value="Save Changes" onclick="doSave();" class="button" />
</form:form>


JSP 소스는 이전 버전과 동일 하게 위와 같이 코드를
작성 합니다.

* Invalid시 Message 리소스 적용 하기

이 부분은 SpringSource 팀 블러그와 JSR-303 스펙을
보면서 알아낸 방법 입니다.매우 중요한 부분이니
잘 이해하시기 바랍니다.

먼저 아래와 같이 메세지 번들을 설정 합니다.

<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>messages.default</value>
</list>
</property>
</bean>

default_ko.properties
메세지 expression은 “검증 annotation명+도메인명+속성명”
으로 하며 key 작성 규칙은 java method 형식으로
합니다.
NotEmpty.user.id=아이디를 입력 하세요.
Size.user.id=아이디는 5자 이하 입니다.
Pattern.user.id=아이디는 반드시 영문 또는 숫자만 입력 가능 합니다.
NotEmpty.user.name=이름을 입력 하세요.
NotEmail.user.email=이메일 형식이 틀립니다.

public class User {

@NotEmpty()
@Size(min=1,max = 5)
@Pattern(regexp="[0-9a-zA-Z]")
private String id;

.....
}

예를 들어서 “@NotEmpty”의 경우 메세지 키는
NotEmpty(annotation 명) + “.” + user(도메인 클래스명)
+”.” + id(속성명) 그래서 전체 메세지 키는
“NotEmpty.user.id”가 되는 것입니다.

Part3에서는 Custom Validation 작성 방법에
대해서 말씀을 드리겠습니다.


Part1,Part2는 기본적인 스프링에서 JSR-303
Validation 사용 방법에 대해서 설명 드렸습니다.
이번 Part는 Custom Validation에 대해서
말씀 드리겠습니다.
먼저 User 도메인 클래스 보면 아래와
같습니다.

public class User {

@NotEmail
private String email;

.....
}

,
“@NotEmail”은 제가 직접 만든 검증
annotation 입니다.
이제 부터 작성 방법에 대해서 말씀 드리겠습니다.

* NotEmail Annotation 작성 방법

custom validation을 만들려면 2개의 클래스가 필요 합니다.
첫번째는 검증 annotation 클래스 이며 나머지 하나는
annotation을 검증할 validator 클래스 입니다.
이 두클래스의 참조 관계는 서로 의존 하는 관계
입니다.

package org.springshowcase.mvc;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.ConstraintPayload;

@Documented
//@Constraint(validatedBy = EmailValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface NotEmail {
public abstract String message() default "Not Email Format!";
public abstract Class<?>[] groups() default {};
public abstract Class<? extends ConstraintPayload>[] payload() default {};
}

“@Contraint” 부분을 주석 처리 하고 compile을 합니다.
이유는 아직 “EmailValidator” 클래스가 존재 하지 않기
때문입니다.

* EmailValidator 클래스 작성 방법

package org.springshowcase.mvc;


import java.util.regex.Pattern;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

public class EmailValidator implements ConstraintValidator<NotEmail, String> {

@Autowired
@Qualifier("emailPattern")
String pattern;

public void initialize(NotEmail constraintAnnotation) {
// nothing to initialize
}

public boolean isValid(String value, ConstraintValidatorContext context) {
Pattern EMAIL_PATTERN = Pattern.compile(this.pattern);
return EMAIL_PATTERN.matcher(value).matches();
}
}


반드시 “ContraintValidator” 인터페이스를 구현 해야 합니다
“@Autowired” 어노테이션을 보면 스프링 annotation 입니다.
이 얘기는 아래와 같이 스프링 빈을 선언 하면

<mvc:annotation-driven />


“ContraintValidator” 인터페이스를 구현한 모든 클래스는
스프링 ApplicationContext에서 이러한 클래스들을
스프링 빈으로 생성합니다.
이말은 Validator에 다른 스프링 빈을 injection을 할 수있다는
뜻 입니다.
예제를 통해서 email 정규식을 일부러 스프링 빈으로 설정하고
injection을 시켰습니다.
아래는 injection 당한 빈 입니다.

<bean id="emailPattern" class="java.lang.String">
<constructor-arg index="0" type="java.lang.String"
value=".+@.+\\.[a-z]+" />
</bean>


* NotEmail Annotation 재컴파일

이제 “EmailValidator”가 생성이 되었기 때문에
“NotEmail” 어노테이션 클래스의 주석을 해제하고
재컴파일 합니다.

package org.springshowcase.mvc;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.ConstraintPayload;

@Documented
@Constraint(validatedBy = EmailValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface NotEmail {
public abstract String message() default "Not Email Format!";
public abstract Class<?>[] groups() default {};
public abstract Class<? extends ConstraintPayload>[] payload() default {};
}


* Message Bundle 작성 방법

이제 새로운 custom validator를 작성이 끝났습니다.
새로운 annotation을 적용할 도메인 클래스에
선언 하면 됩니다.
메세지 번들 작성 방법은 아래와 같이 하면 됩니다.
NotEmail.user.email=이메일 형식이 틀립니다.

* Spring Validation VS JSR-303

JSR-303 스펙에 대해서 확정을 하기 위한 투표를
하려고 했을때 스프링 진영에서는 참석을 하지
않았다고 합니다. 그래서 jboss 진영에서 심기가
좋지 않았다는 얘기가 있습니다.
역시 둘은 앙숙인듯….
아무래도 스프링 입장에서 자기들 validation이
있는데 굳이 JSR-303 스펙을 넣는게 그다지
좋지는 않았을 것 같습니다.
anyway!
JSR-303은 제가 도메인 클래스를 얘를 들었지만
사실 도메인 클래스만 해당 되는 것은 아닙니다.
모든 클래스(Service,DAO,..)에서 적용이 가능
합니다.
직접 써보니 JSR-303은 장점이 상당히 많았습니다.
첫째 검증 로직의 중복이 없습니다.
대부분 스프링 validation은 매요청시 작성 한거에
반해서 JSR-303은 도메인에 설정 하기때문에
검증 로직이 좀더 명확하고 코딩수도 적다는 얘기
입니다. 그리고 메세지 작성 방법도 직관적이기
때문에 관리도 수월 합니다.
반면 단점이 한나 있습니다. 좀 유연하지 않는다는
문제 입니다.
무슨 얘기냐 예를 들어서 id,name,email 이런 3개의
속성이 있다고 하고 모두 NotNull 조건이라고
설정을 합니다.
insert일 경우야 당연히 NotNull이지만
name만 update를 할경우 id,name 두
속성만 있으면 되는데 validation 조건이
모두 NotNull 조건이기 때문에 굳이
필요 없는 email에 기존 값을 채워서
보내야 한다는 것입니다.
즉 케이스별 검증 체크를 선택적으로 할 수
없다는 것입니다.
이런 이유는 예측컨데 jboss 진영에 힘이
있었지 않나 생각 합니다.
hibernate인 경우는 전체 업데이트를
체크 하기 때문이죠..
그래서 제가 개인적으로 시간이 되면 선택적으로
validation을 수행하는 util 클래스를 만드려고 합니다.
(언제가 될지 모르지만..)
그리고 한가지 더 현재 JSR-303 라이브러리와 구현체
라이브러리가 release가 아닌 running 중이기 때문에
라이브러리의 버전 진행도 유심히 모니터링
할 필요가 있습니다.
개인적으로 JSR-303은 실무에서 사용해도
훌륭한 개발 도구가 될 것 같습니다.

* 소스 다운 로드

본 예제는 제가 진행하는 오픈 소스 “ssc-mvc”를 체크아웃
받으시면 됩니다.
자세한 내용은 오픈 소스 사이트를 참고 하시기 바랍니다.


출처 - http://beyondj2ee.tumblr.com/post/14509177207/spring-3-0-validation-part3








 다양한 기초 예제들이 있다.


http://www.roseindia.net/tutorial/spring/spring3/web/spring-3-mvc-validation-example.html

This tutorial shows you how to validate Spring 3 MVC based applications. In Spring 3 MVC annotation based controller has been added and other type of controllers are not deprecated.

In this example application we will be create a Validation form in  Spring 3.0  MVC  Framework. The validation of the form is done using annotation. The Hibernate validator framework is used for validation of the form data. 

The application will present simple user registration form to the user. Form will have the following fields:

  1. User Name
  2. Age
  3. Password

The validator framework will validate the user input. If there is any validation error application will display the appropriate message on the form.

To use the validation framework in the application "hibernate-validator-4.0.2.GA.jar" and " validation-api-1.0.0.GA.jar" must be added to the project libraries.

Application uses following jar files:




Step 1:
Create index.jsp  under WebContent . The code index.jsp as :

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"

pageEncoding="ISO-8859-1"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">

<title>Spring 3, MVC Examples</title>

</head>

<body>

<h1>Spring 3, MVC Examples</h1>

<ul>

<li><a href="forms/validationform.html">Validation Form</a></li>

</ul>

</body>

</html>

In the above page we are creating a new link to access the Validation Form example from the index page of the application.

Step 2:

Now  we will create a  model class " ValidationForm.java"  under src folder . The code of   "ValidationForm.java" is:

package net.roseindia.form;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.Size;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.format.annotation.NumberFormat;
import org.springframework.format.annotation.NumberFormat.Style;

public class ValidationForm {
       
@NotEmpty
       
@Size(min = 1, max = 20)
       
private String userName;
       
@NotNull
       
@NumberFormat(style = Style.NUMBER)
       
@Min(1)
       
@Max(110)
       
private Integer age;
       
@NotEmpty(message = "Password must not be blank.")
       
@Size(min = 1, max = 10, message = "Password must between 1 to 10 Characters.")
       
private String password;

       
public void setUserName(String userName) {
               
this.userName = userName;
       
}

       
public String getUserName() {
               
return userName;
       
}

       
public void setAge(Integer age) {
               
this.age = age;
       
}

       
public Integer getAge() {
               
return age;
       
}

       
public void setPassword(String password) {
               
this.password = password;
       
}

       
public String getPassword() {
               
return password;
       
}
}

In the above class we have added the proper annotation for validating the form values. Here is the list of annotation used for validation:

@NotEmpty
@Size(min = 1, max = 20)
@NotNull
@NumberFormat(style = Style.NUMBER)
@Min(1)
@Max(110)
@NotEmpty(message = "Password must not be blank.")
@Size(min = 1, max = 10, message = "Password must between 1 to 10 Characters.")

Step 3 :

Now create a folder views under WebContent/WEB-INF . Again  cerate "validationform.jsp"  under WebContent/WEB-INF/views as :

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"

pageEncoding="ISO-8859-1"%>

<%@taglib prefix="form" uri="http://www.springframework.org/tags/form"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">

<title>Insert title here</title>

</head>

<body>

<form:form method="post" action="validationform.html"

commandName="validationForm">

<table>

<tr>

<td>User Name:<font color="red"><form:errors

path="userName" /></font></td>

</tr>

<tr>

<td><form:input path="userName" /></td>

</tr>

<tr>

<td>Age:<font color="red"><form:errors path="age" /></font></td>

</tr>

<tr>

<td><form:input path="age" /></td>

</tr>

<tr>

<td>Password:<font color="red"><form:errors

path="password" /></font></td>

</tr>

<tr>

<td><form:password path="password" /></td>

</tr>

<tr>

<td><input type="submit" value="Submit" /></td>

</tr>

</table>

</form:form>

</body>

</html>

In this above jsp file <form:errors path="..." />  tag is used to display the validation error messages.

Step 4:

Now  create "validationsuccess.jsp" file under  WebContent/WEB-INF/views . The code of  "validationsuccess.jsp"  is :

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"

pageEncoding="ISO-8859-1"%>

<%@taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">

<title>Insert title here</title>

</head>

<body>

<core:out value="${validationForm.userName}" /> Save Successfully.

</body>

</html>

Step 5:

Now create  Contoller class "ValidationController.java" under src folder.  Controller class use for  RequestMapping  and process the user request. The  code of   "ValidationController.java"   as:

package net.roseindia.controllers;

import net.roseindia.form.ValidationForm;
import java.util.Map;
import javax.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/validationform.html")
public class ValidationController {

       
// Display the form on the get request
       
@RequestMapping(method = RequestMethod.GET)
       
public String showValidatinForm(Map model) {
               
ValidationForm validationForm = new ValidationForm();
                model
.put("validationForm", validationForm);
               
return "validationform";
       
}

       
// Process the form.
       
@RequestMapping(method = RequestMethod.POST)
       
public String processValidatinForm(@Valid ValidationForm validationForm,
                       
BindingResult result, Map model) {
               
if (result.hasErrors()) {
                       
return "validationform";
               
}
               
// Add the saved validationForm to the model
                model
.put("validationForm", validationForm);
               
return "validationsuccess";
       
}

}

The @RequestMapping annotation is used for request uri mapping.

Step 6:

Now modify web.xml as:

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

<web-app version="2.5"

xmlns="http://java.sun.com/xml/ns/j2ee"

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

xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_5.xsd">

<display-name>Spring3Example</display-name>

<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>/forms/*</url-pattern>

</servlet-mapping>

<welcome-file-list>

<welcome-file>index.jsp</welcome-file>

</welcome-file-list>

</web-app>

Step 7:

Again create "dispatcher-servlet.xml"  under  WebContent/WEB-INF . The "dispatcher-servlet.xml"  code as :

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

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

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

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

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

xmlns:mvc="http://www.springframework.org/schema/mvc"

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

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

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

">

<!-- Enable annotation driven controllers, validation etc... -->

<mvc:annotation-driven />

<context:component-scan base-package="net.roseindia.controllers"/>

<bean id="viewResolver"

class="org.springframework.web.servlet.view.InternalResourceViewResolver">

<property name="prefix">

<value>/WEB-INF/views/</value>

</property>

<property name="suffix">

<value>.jsp</value>

</property>

</bean>

<bean id="messageSource"class="org.springframework.context.support.ReloadableResourceBundleMessageSource">

<property name="basename" value="/WEB-INF/messages" />

</bean>

</beans>

Where <mvc:annotation-driven> is used for annotation driven controllers and validation. While message Resource configuration is done in "dispatcher-servlet.xml" using following entry:

<bean id="messageSource"class="org.springframework.context.support.ReloadableResourceBundleMessageSource">

<property name="basename" value="/WEB-INF/messages" />

</bean>

Again  create customization  Message file  "messages.properties"  under  WebContent/WEB-INF. The modify  "messages.properties"  as

NotEmpty.validationForm.userName=User Name must not be blank.

Size.validationForm.userName=User Name must between 1 to 20 characters.

Step 8:

Now run Application display output as :

if click  Validation Form  hyperlink then open validationform as :

Step 9:

Now Enter  userName,  Age and Password if find any error display as:

  

Again display  error-

If validation  success then display validationsucess page as :

Download example code

In this we have studies the use of annotation controller and annotated validators.



출처 - http://sway.tistory.com/entry/Spring-3-MVC-Validation-Example








org.springframework.validation.BindException 에 대한 한가지 해결 방안

아래 post에 의하면 @Valid 적용 객체 다음에 BindingResult가 바로 와야한다.



I got a new roo project off the ground successfully, but now I'm having a problem getting validation to work for a login page. It seems that the validator is choking before passing control to my controller. I am never given the opportunity to check the BindingResult. I've examined several similar questions here and on the web, and my code seems to conform with what they are doing.

First the error I get when submitting the form (if i pass validation i get no error message). In this case i didn't meet the minimum length for the password:

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'loginUser' on field 'password': rejected value [b]; codes [Size.loginUser.password,Size.password,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [loginUser.password,password]; arguments []; default message [password],50,5]; default message [Password must be between 1  and 50 characters long]
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:894)
    org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:789)

My entity validation is setup like this:

public class LoginUser {

    @NotNull
    @NotEmpty
    private String username;

    @NotNull
    @NotEmpty(message = "Password must not be blank.")
    @Size(min = 5, max = 50, message = "Password must be between 1 " 
            + " and 50 characters long")
    private String password;
}

Here is my markup:

<form:form method="post" commandName="command">
    <form:label path="username">Username: </form:label>
    <form:input path="username"/>
    <form:errors path="username"/>

    <form:label path="password">Password: </form:label>
    <form:password path="password"/>
    <form:errors path="password"/>

    <input type="submit"/>
</form:form>

And the controller:

    @ModelAttribute("command")
    public LoginUser fbo(){
        LoginUser u = new LoginUser();
        u.setUserType(UserType.USER);
        return u;
    }

    @RequestMapping(value="/login.htm", method=RequestMethod.POST)
    public String doLogin(@Valid LoginUser command, 
            HttpServletRequest request, BindingResult result
        ){
        if(result.hasErrors()){
            return "login";
        }
    }

And just in case it matters, the generated xml in webmvc-config.xml:

<!-- The controllers are autodetected POJOs labeled with the @Controller annotation. -->
<context:component-scan base-package="com.tcg.myproject" use-default-filters="false">
    <context:include-filter expression="org.springframework.stereotype.Controller" type="annotation"/>
</context:component-scan>

<!-- Turns on support for mapping requests to Spring MVC @Controller methods
     Also registers default Formatters and Validators for use across all @Controllers -->
<mvc:annotation-driven conversion-service="applicationConversionService"/>

Been pulling my hair out for hours and I can't figure out what it could be. Thanks for reading!

share|improve this question
Why didn't you simply use Spring Security add-on? – bhagyas Mar 29 '12 at 3:48
One does not simply use spring security. – Matthew Gilliard Feb 13 at 10:43

In your controller handler method, try moving the BindingResult argument so it is immediately after the command argument. Spring looks for command object parameters and BindingResult parameters to be paired up in handler method signatures.

share|improve this answer
Thanks Scott! Not sure I would have ever found this w/o asking. – samspot Mar 29 '12 at 15:38
No problem. Also note that if you have multiple command objects in the method signature, each one should be followed by its own BindingResult - i.e. they must travel in pairs. – Scott Frederick Mar 30 '12 at 17:31
Awesome, thanks again. – samspot Mar 30 '12 at 18:47
Good to know this about the pairing, thanks Scott – frandevel Dec 22 '12 at 12:18











 


출처 - http://stackoverflow.com/questions/9916623/spring-valid-validator-not-invoked-properly-roo-hibernate






Email Regular Expression Pattern

^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*
      @[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$;


Date Format

    @DateTimeFormat(pattern="MM/dd/yyyy")
    @NotNull @Past
    private Date birthday;















'Framework & Platform > Spring' 카테고리의 다른 글

도메인 객체(오브젝트)  (0) 2012.05.21
spring - @SessionAttributes 와 SessionStatus  (0) 2012.05.21
spring - Validator  (0) 2012.05.21
Spring - @ModelAttribute  (0) 2012.05.21
Spring - @RequestParam  (0) 2012.05.21
Posted by linuxism
,