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

Shiba·2023년 11월 15일
0

🍀 스프링 정리

목록 보기
16/21
post-thumbnail

📗 스프링 @MVC (1)

애노테이션을 중심으로 한 새로운 MVC의 확장 기능은 @MVC라고도 불린다. @MVC는 스프링 3.0에서 기존에 가장 많이 사용되던 Controller 타입의 기반 클래스들을 대부분 대체하게 됐다. 4장에서는 @MVC의 상세한 기능과 활용 전략을 자세히 살펴보겠다.

📖 @RequestMapping 핸들러 매핑

@MVC의 가장 큰 특징핸들러 매핑과 핸들러 어댑터의 대상이 오브젝트가 아니라 메소드라는 점.

📝 클래스/메소드 결합 매핑정보

DefaultAnnotationHandlerMapping의 핵심매핑정보로 @RequestMapping 애노테이션을 활용한다는 점. 그런데 @RequestMapping은 메소드 레벨에도 붙일 수 있다. 스프링은 두 가지 위치에 붙은 @RequestMapping의 정보를 결합해서 최종 매핑정보를 생성

◼ @RequestMapping 애노테이션

@RequestMapping 애노테이션에는 다음과 같은 엘리먼트 지정가능.
모든 엘리먼트는 생략 가능

String[] value() : URL 패턴

디폴트 엘리먼트. 스트링 배열 타입으로 URL 패턴을 지정하도록 되어있음
URL은 가장 기본이 되는 매핑정보이며, 대부분의 핸들러 매핑은 요청정보 중에서 URL만을 사용한다.
ANT스타일의 와일드카드 사용가능

@RequestMapping("/hello")
@RequestMapping("/main*")
@RequestMapping("/view.*")
@RequestMapping("/admin/**/user*")

@RequestMapping에 다음과 같이 {}를 사용하는 URL 템플릿을 사용할 수도 있음.
이때 {} 위치에 해당하는 내용을 컨트롤러 메소드에서 파라미터로 전달 받을 수 있음.
{}에 들어가는 이름은 패스 변수라고 불리며 하나 이상 등록 가능하다.

@RequestMapping("/user/{userid}")

URL 패턴은 배열이기 때문에 하나 이상의 URL 패턴을 지정할 수 있음

@RequestMapping("/hello", "/hi")

URL 패턴에서 중요한 사실은 디폴트 접미어 패턴이 적용된다는 점이다.

@RequestMapping("/hello")  // 확장자가 붙지않고 /로 끝나지않는 경우
@RequestMapping({"/hello", "/hello/", "/hello.*"}) // 동일한 결과가 나오게됨

RequestMethod[] method() : HTTP 요청 메소드

RequestMethod는 HTTP 메소드를 정의한 이늄. GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE 메소드가 정의되어 있음.
같은 URL이라도 요청 메소드에 따라 다른 메소드에 매핑해줄 수 있음

@RequestMethod(value="/user/add", method=RequestMethod.GET)
@RequestMethod(value="/user/add", method=RequestMethod.POST)

String[] params : 요청 파라미터

요청의 파라미터와 그 값을 비교해서 매핑해주는 것. 같은 URL을 사용하더라도 HTTP 요청 파라미터에 따라 별도의 작업을 하고싶을 때 사용
조건을 만족하는 경우가 여러 개 있을 때는 그 중 가장 많은 조건을 충족 시킨 쪽이 선택됨

@RequestMapping("/user/edit", params="type=admin")// /user/edit?type=admin
@RequestMapping("/user/edit", params="type=member")// /user/edit?type=member

특정 파라미터가 존재하지 않아야 한다는 조건도 지정 가능

@RequestMapping("/user/edit", params="!type")//파라미터가 아예 존재하지 않는 경우만 매핑

params도 배열로 선언되어 있으므로 하나 이상 지정 가능하다.

String[] headers() : HTTP 헤더

HTTP헤더 정보를 매핑함. 자주 사용되지는 않지만 경우에 따라 매우 유용함.

//헤더의 content-type이 text/html, text/plain 등으로 되어있는 경우에만 매핑
@RequestMapping(value="/view", headers = "content-type=text/*")

◼ 타입 레벨 매핑과 메소드 레벨 매핑의 결합

타입 레벨에 붙는 @RequestMapping은 타입 내의 매핑용 메소드의 공통 조건을 지정할 때 사용. 메소드 레벨에서 조건을 세분화 한다.

@RequestMapping("/user")
public class UserController {
	@RequestMapping("/add") public String add(...) { } // /user/add
    @RequestMapping("/edit") public String add(..) { } // /user/edit
    @RequestMapping("/delete") public String delete(...) { } // /user/delete
}

타입 레벨과 메소드 레벨의 URL을 결합하는 대신, URL은 타입 레벨에서만 정의하고 메소드 레벨에서는 다른 매핑조건을 추가해줄 수도 있음

@RequestMapping("/user/add")
public class UserController {
	@RequestMapping(method=RequestMethod.GET) public String form(...) { }
    @RequestMapping(method=RequestMethod.POST) public String submit(...) { }
}

◼ 메소드 레벨 단독 매핑

메소드 레벨의 매핑조건에 공통점이 없는 경우에는 타입 레벨에 조건을 주지 않고 메소드 레벨에서 독립적으로 매핑정보 지정할 수도 있음.

@RequestMapping
public class UserController {
	//서로 다른 URL에 대해 메소드 호출 가능
	@RequestMapping("/hello") public String hello(...) { }
    @RequestMapping("/main") public String main(...) { }
}

이때 타입 레벨에는 조건이 없는 빈 @RequestMapping을 꼭 부여해주어야 한다.
- 생략한다면 클래스 자체가 매핑 대상에서 제외
- 컨트롤러 클래스에 @Controller 애노테이션을 붙였다면 클래스 레벨의 @RequestMapping 생략 가능

◼ 타입 레벨 단독 매핑

@RequestMapping을 타입 레벨에 단독으로 사용해서 다른 타입 컨트롤러에 대한 매핑을 위해 사용할 수도 있다

@RequestMapping("/hello")
public class UserController {
	...
}

클래스 레벨의 URL 패턴이 /*로 끝나는 경우 메소드 레벨의 URL 패턴으로 메소드 이름이 사용되게 할 수 있음

@RequestMapping("/user/*")
public class UserController {
	@RequestMapping public String add(...) { }  // ("/user/add")
    @RequestMapping public String edit(...) { } // ("/user/edit")
}

📝 타입 상속과 매핑

@RequestMapping이 적용된 클래스를 상속해서 컨트롤러로 사용하는 경우 슈퍼클래스의 매핑정보는 어떻게 될까?
@RequestMapping 정보는 상속된다. 하지만 서브 클래스에서 @RequestMapping을 재정의하는 경우, 슈퍼클래스의 정보는 무시된다.

위에서 말한 모든 내용이 인터페이스에서도 똑같이 적용된다.
(인터페이스 안에서의 타입 레벨과 메소드 레벨 사이의 관계와 매핑조건 결합이 클래스의 경우와 똑같이 적용)

◼ 매핑정보 상속의 종류

클래스 상속이나 인터페이스 구현에서 어떻게 상속되고 적용되는지 몇 가지 대표적인 경우를 살펴보자

상위 타입과 메소드의 @RequestMapping 상속

슈퍼클래스에만 @RequestMapping을 적용하고 이를 그대로 상속한 서브클래스에는 아무런 @RequestMapping을 사용하지 않았을 경우, 슈퍼클래스에 정의된 매핑정보를 그대로 서브클래스가 물려받음

@RequestMapping("/user") public class Super {
	@RequestMapping("/list") public String list() { ... } // ("/user/list")
}
public class Sub extends Super { //Super의 list()메소드를 그대로 상속. 
}

오버라이드를 했더라도 @RequestMapping을 붙이지 않으면 슈퍼클래스 메소드의 매핑정보는 그대로 상속된다.

public class Sub extends Super {
	public String list() { ... } //오버라이드 했더라도 list()의 매핑정보 그대로 상속
}

인터페이스의 경우에도 위와 같은 원리로 상속된다.

@RequestMapping("/user") public interface Intf{
	@RequestMapping("/list") String list();
}

public class Impl implements Intf {
	public String list() { ... }
}

상위 타입의 @RequestMapping과 하위 타입 메소드의 @RequestMapping 결합

슈퍼클래스에는 타입에만 @RequestMapping이 선언되어 있고, 서브클래스에는 타입 레벨에는 매핑정보가 없고 메소드에만 @RequestMapping이 있는 경우
슈퍼클래스의 타입 레벨 @RequestMapping이 그대로 상속되고 메소드 레벨의 @RequestMapping과 결합하여 조건을 만들어냄.
=> 슈퍼클래스 타입 레벨 매핑정보 + 서브클래스의 메소드 레벨 매핑정보

@RequestMapping("/user") public class Super { }
public class Sub extends Super { //Super의 매핑정보 상속("/user")
	@RequestMapping("/list") public String list() { ... } // ("/user/list")
}

인터페이스의 경우에도 마찬가지로 적용된다.

상위 타입 메소드의 @RequestMapping과 하위 타입의 @RequestMapping 결합

앞의 경우와 반대로 슈퍼클래스에는 메소드에만 @RequestMapping이 있고, 서브클래스에는 타입 레벨에 @RequestMapping이 부여된 경우
위 경우와 똑같이 @RequestMapping의 정보가 그대로 상속된 후에 결합된다.
=> 서브클래스 타입 레벨 매핑정보 + 슈퍼클래스의 메소드 레벨 매핑정보

public class Super {
	@RequestMapping("/list") public String list() { ... }
}
@RequestMapping("/user") public class Sub extends Super {
}

오버라이드한 경우에도 같은 결과를 얻을 수 있음.
인터페이스의 경우도 동일한 방식으로 적용. 단, 인터페이스를 구현하는 메소드에 URL이 없는 빈 @RequestMapping을 붙이면 인터페이스 메소드의 매핑정보가 무시됨

하위 타입과 메소드의 @RequestMapping 재정의

상속 또는 구현을 통해 만들어진 하위 타입에 @RequestMapping을 부여하면 상위 타입에서 지정한 @RequesMapping 매핑정보를 대체해서 적용
슈퍼클래스의 @RequestMapping은 모두 무시되고 새로 정의한 서브클래스의 @RequestaMapping 적용
- 모든 조건이 다 재정의됨(정의하지 않은 값은 디폴트 값이 적용됨)

@RequestMapping("/usr") public class Super {
	@RequestMapping(value="/catalog", method=RequestMethod.POST)
    public String list() { ... }
}
@RequestMapping("/user") public class Sub extends Super {
	@RequestMapping("/list") public String list() { ... } // ("/user/list")
    // method=RequestMethod.POST조건이 상속되지 않음!
}

◼ 제너릭스와 매핑정보 상속을 이용한 컨트롤러 작성

제너릭스를 사용하여 상위 타입에는 타입 파라미터와 메소드 레벨의 공통 매핑정보를 지정해놓고, 이를 상속받는 개별 컨트롤러에는 구체적인 타입과 클래스 레벨의 기준 매핑정보를 지정해주는 기법 사용 가능
- CRUD를 만들 때 동일한 코드가 중복되는 경우 사용 가능

젼형적인 CRUD와 검색기능의 컨트롤러 코드

public class UserController {
	UserService service;
    
    public void add(User user) { ... }
    public void update(User user) { ... }
    public User view(Integer id) { ... }
    public void delete(Integer id) { ... }
    public List<User> list() { ... }
}

UserController에 적용할 수 있는 제네릭 추상 클래스
- 모든 CRUD 컨트롤러에서 상속받을 수 있게 만들어진 제네릭 추상 클래스

public abstract class GenericController<T, K, S> {
	S service;
    public void add(T entity) { ... }
    public void update(T entity) { ... }
    public T view(K id) { ... }
    public void delete(P id) { ... }
    public List<T> list() { ... }
}

위 추상 클래스의 도메인 오브젝트 타입인 T, 조회나 삭제에 사용할 ID 타입인 K, 서비스 계층 오브젝트 타입인 S 세 가지 타입 파라미터만 정의해주면 하나의 컨트롤러가 완성됨.

public class UserController extends GenericController<User, Integer, UserService> { }

CRUD외의 User에 대한 추가 작업을 위한 컨트롤러 메소드가 필요하다면 추상클래스를 상속받은 컨트롤러 클래스에 넣어주면 됨.

public class UserController extends GenericController<User, Integer, UserService> {
	public String login(String userId, String password) { ... }
}

아직 UserController에는 매핑정보가 없음. 매핑정보를 어디에 어떻게 넣어야 할까? 바로 이런 경우에 @RequestMapping 상속을 이용하면 된다.
여기서 상위 클래스의 타입 메소드와 서브 클래스의 타입 레벨을 결합하는 방법을 사용한다.
- 미리 추상클래스의 메소드에 적용해둠으로서 동일한 작업을 하는 다른 클래스의 메소드들이 같은 애노테이션과 매핑정보를 부여하지 않아도 됨.

추상클래스에 애노테이션을 부여해주자

public abstract class GenericController<T, K, S> {
	S service;
    @RequestMapping("/add") public void add(T entity) { ... }
    @RequestMapping("/update") public void update(T entity) { ... }
    @RequestMapping("/view") public T view(K id) { ... }
    @RequestMapping("/delete") public void delete(P id) { ... }
    @RequestMapping("/list") public List<T> list() { ... }
}

이를 상속하는 컨트롤러 클래스에는 다음과 같이 클래스 레벨에 @RequestMapping을 부여하여 URL 매핑과 컨트롤러 로직이 모두 적용된 컨트롤러 완성 가능

@RequestMapping("/user")
public class UserController extends GenericController<User, Integer, UserService> {
	//login()은 추상클래스에 없으므로 직접 애노테이션을 부여
	@RequestMapping("/login")
    public String login(String userId, String password) { ... }
}
// 추상 클래스의 메소드들의 매핑정보는 ("/user")와 결합하여 최종적으로 만들어짐
//ex) add() 메소드는 ("/user/add")로 매핑하게됨

📖 @Controller

스프링 3.0의 @MVC와 @Controller는 스프링 역사상 가장 획기적인 변신
기존에 스프링이 자랑하던 Controller를 확장한 기반 컨트롤러의 사용 방법을 완전히 대체할 수 있을 뿐만 아니라, 훨씬 더 편리하다고 이야기 함.
이번 절에서 @Controller의 간단한 예를 살펴보고, @Controller에서 허용하는 파라미터의 리턴 값 종류와 사용방법을 구체적으로 알아볼 것이다

📝 메소드 파라미터의 종류

이제부터 소개할 타입의 오브젝트와 애노테이션은 @Controller의 메소드 파라미터로 자유롭게 사용 가능. 단, 몇 가지 파라미터 타입은 순서와 사용 방법상의 제약이 있으니 주의

HttpServletRequest, HttpServletResponse

대개는 좀 더 상세한 정보를 담은 파라미터 타입을 활용하면 되기 때문에 필요 없음. 그래도 원한다면 서블릿의 HttpServletRequest, HttpServletResponse 오브젝트를 파라미터로 사용 가능

HttpSession

HTTP 세션만 필요한 경우라면 HttpServletRequest가 아닌 HttpSession 타입 파라미터를 선언해서 직접 받는 편이 낫다. 서버에 따라서 멀티스레드 환경에서 안전성이 보장되지 않으니 안전하게 사용하기 위해서는 핸들러 어댑터의 synchronizeOnSession 프로퍼티를 true로 설정하자.

WebRequest, NativeWebRequest

HttpServletRequest의 요청정보를 대부분 그대로 갖고 있는, 서블릿 API에 종속적이지 않은 오브젝트 타입

Locale

java.util.Locale 타입으로 DispatcherServlet의 지역정보 리졸버가 결정한 Locale 오브젝트를 받을 수 있음

InputStream, Reader

HttpServletRequest의 getInputStream()을 통해서 받을 수 있는 콘텐트 스트림 또는 Reader 타입 오브젝트를 제공받을 수 있음

OutputStream, Writer

HttpServletResponse의 getOutputStream()으로 가져올 수 있는 출력용 콘텐트 스트림 또는 Writer 타입 오브젝트를 받을 수 있음

@PathVariable

@RequestMapping의 URL에 {}로 들어가는 패스 변수를 받음
요청 파라미터를 URL의 쿼리 스트링으로 보내는 대신 URL 패스로 풀어서 쓰는 방식을 쓰는 경우 매우 유용함

URL을 쿼리 스트링으로 파라미터를 전달할때
/user/view?id=10
파라미터를 URL 경로에 포함시키는 방식
/user/view/10
// @PathVariable 사용 예
@RequestMapping("/user/view/{id}") // {패스 변수}
// 패스 변수의 이름을 애노테이션의 값으로 부여해주기
public String view(@PathVariable("id") int id) { // /user/view/10 -> int id=10
	...
}

패스 변수는 하나의 URL 템플릿 안에 여러 개 선언 가능

@RequestMapping("/member/{membercode}/order/{orderid}")
public String lookup(@PathVariable("membercode") String code,
	@PathVariable("orderid") int orderid) {
    ...
}

❗ 파라미터의 타입은 URL의 내용이 적절히 변환될 수 있는 것을 사용해야 한다!
- 일치하지 않으면 변환이 불가능하여 예외 발생(HTTP 400 - Bad Request)

@RequestParam

단일 HTTP 요청 파라미터를 메소드 파라미터에 넣어주는 애노테이션
가져올 요청 파라미터의 이름을 @RequestParam 애노테이션의 기본 값으로 지정해주면 된다.

public String view(@RequestParam("id") int id) { ... }

@RequestParam은 다음과 같이 하나 이상의 파라미터에 적용할 수 있다. 스프링의 내장 변환기가 다룰 수 있는 모든 타입을 지원한다.

public String view(@RequestParam("id") int id, @RequestParam("name") 
	String name, @RequestParam("file") MultipartFile file) { ... }

파라미터 이름을 지정하지 않고 Map<String, String> 타입으로 선언하면 모든 요청 파라미터를 담은 맵으로 받을 수 있음. 파라미터 이름은 맵의 키, 파라미터 값은 맵의 값에 담겨 전달

public String add(@RequestParam Map<String, String\> params) { ... }

@RequestParam을 사용했다면 해당 파라미터가 반드시 있어야 한다.
없다면 HTTP 400 - Bad Request를 받게됨. 파라미터를 선택적으로 제공하게 하려면 required 엘리먼트를 false로 설정해주면 된다. 요청 파라미터가 존재하지 않을 때 사용할 디폴트 값도 설정 가능

public void view(@RequestParam(valie="id", required=false, defaultValue="-1") 
	int id) { ... }

@CookieValue

HTTP 요청과 함께 전달된 쿠키 값메소드 파라미터에 넣어주도록 @CookieValue를 사용할 수 있음. 애노테이션의 기본 값에 쿠키의 이름을 지정해주면 된다.

//'auth'라는 이름의 쿠키 값을 메소드 파라미터 auth에 넣어주는 메소드 선언
public String check(@CookieValue("auth") String auth) { ... }

@CookieValue도 지정된 쿠키 값이 반드시 존재해야 한다. 없다면 예외 발생.
없을 경우에도 예외가 발생하지 않게 하려면 required 엘리먼트를 false로 선언해야 한다.
디폴트 값을 선언해서 쿠키 값이 없을 때 디폴트 값으로 대신하게 할 수 있다.

public String check(@CookieValue(value="auth", required=false,
	defaultValue="NONE") String auth) { ... }

@RequestHeader

요청 헤더정보를 메소드 파라미터에 넣어주는 애노테이션. 애노테이션의 기본 값으로 가져올 HTTP 헤더의 이름을 지정

public void header(@RequestHeader("Host") String host,
					@RequestHeader("Keep-Alive") long keepAlive)

@RequestHeader도 헤더값이 반드시 존재해야 한다. @CookieValue와 마찬가지로 설정 시, 값이 존재하지 않아도 상관없게 설정하거나 디폴트 값을 줄 수 있음.

Map, Model, ModelMap

다른 애노테이션이 붙어 있지 않다면 Map,Model,ModelMap 타입의 파라미터는 모두 모델정보를 담는 데 사용할 수 있는 오브젝트가 전달됨.
- 파라미터로 정의해서 핸들러 어댑터에서 미리 만들어 제공해주는 것을 사용하면 편리함
Model과 ModelMap은 모두 addAttribute() 메소드를 제공

@RequestMapping(...)
public void hello(ModelMap model) {
	User user = new User(1, "Spring");
    // 자동 이름 생성 기능을 이용해 오브젝트만 넣기. 타입 정보를 참고해 이름 부여
    model.addAttribute(user); // addAttribute("user", user)와 동일
}

@ModelAttribute

@ModelAttribute는 메소드 파라미터에 부여할 수도 있고 메소드 레벨에 적용할 수도 있다.
- 사용 목적이 분명히 다르므로 차이점에 주의
@ModelAttribute가 붙은 모델은 여러 개의 모델 오브젝트를 담아 전달하는 모델 맵
- 클라이언트로부터 컨트롤러가 받는 요청정보 중에서 하나 이상의 값을 가진 오브젝트 형태로 만들 수 있는 구조적인 정보@ModelAttribute 모델이라고 함
@RequestParam과 달리 한 번에 모든 요청정보를 담아서 전달 가능 - 커맨드 패턴

Errors, BindingResult

@ModelAttribute는 단지 오브젝트에 여러 개의 요청 파라미터 값을 넣어서 넘겨주는게 전부가 아니라 검증 작업이 추가적으로 진행됨.
검증 과정중에 오류가 발견됐다고 하더라도 예외를 발생시키며 작업을 종료하지 않음
- 이에 대한 처리를 컨트롤러에게 맡김.
- 컨트롤러가 적절한 에러 페이지를 호출하거나 에러 메세지를 보여주며 수정 기회를 제공해야 한다.
=> @ModelAttribute를 통해 폼의 정보를 전달받을 때는 Errors 또는 BindingResult 타입의 파라미터를 같이 사용해야 한다.
- 사용하지 않으면 타입이 일치하지 않을때 BindingException 예외가 던져짐

@RequestMapping(value="add", method=RequestMethod.POST)
public String add(@ModelAttribute User user, BindingResult bindingResult) { ... }

❗ 반드시 @ModelAttribute 파라미터 뒤에 나와야 한다.
- 자신의 바로 앞에 있는 @ModelAttribute 파라미터 검증 작업에서 발생한 오류만을 전달

SessionStatus

파라미터로 선언해두면 현재 세션을 다룰 수 있는 SessionStatus 오브젝트 제공
컨트롤러가 제공하는 기능 중에 모델 오브젝트를 세션에 저장했다가 다음 페이지에서 다시 활용하게 해주는 기능이 있다. 더 이상 세션 내에 모델 오브젝트를 저장할 필요가 없을 경우에는 코드에서 직접 작업 완료 메소드를 호출해서 세션 안에 저장된 오브젝트를 제거해주어야 한다.

@RequestBody

이 애노테이션이 붙은 파라미터에는 HTTP 요청의 본문 부분이 그대로 전달됨
XML이나 JSON기반의 메세지를 사용하는 요청의 경우 이 방법이 매우 유용

@Value

빈의 값 주입에서 사용하던 @Value 애노테이션도 컨트롤러 메소드 파라미터에 부여할 수 있다. 사용방법은 DI에서 @Value를 사용하는 것과 동일함

@RequestMapping(...)
public String hello(@Value("#{systemProperties['os.name']}") String osName) { ... }

컨트롤러도 일반적인 스프링 빈이기 때문에 @Value를 메소드 파라미터 대신 컨트롤러 필드에 DI 해주는 것이 가능. 주입된 값을 컨트롤러 메소드에서 사용 가능

public class HelloController {
	@Value("#{systemProperties['os.name']}") String osName
    
    @RequestMapping(...)
    public String hello() {
    	String osName = this.osName;
    }
}

@Valid

@Valid는 JSP-303의 빈 검증기를 이용모델 오브젝트를 검증하도록 지시하는 지시자. 모델 오브젝트의 검증 방법을 지정하는 데 사용하는 애노테이션
보통 @ModelAttribute와 함께 사용

📝 리턴 타입의 종류

@MVC 컨트롤러 메소드에는 파라미터 뿐만 아니라 리턴타입도 다양하게 결정 할 수 있다.

◼ 자동 추가 모델 오브젝트와 자동생성 뷰 이름

다음 네 가지 정보는 메소드 리턴 타입에 상관없이 조건만 맞으면 모델에 자동으로 추가됨

@ModelAttribute 모델 오브젝트 또는 커맨드 오브젝트

메소드 파라미터 중에서 @ModelAttribute를 붙인 모델 오브젝트@ModelAttribute는 생략했지만 단순 타입이 아니라서 커맨드 오브젝트로 처리되는 오브젝트인 경우
- 자동으로 컨트롤러가 리턴하는 모델에 추가됨
- 기본적으로 모델 오브젝트의 이름은 파라미터 타입 이름을 따름
(이름을 직접 지정하고 싶다면 @ModelAttribute("모델이름")으로 지정해주면 됨)

Map, Model, ModelMap 파라미터

컨트롤러 메소드에 Map,Model,ModelMap 타입의 파라미터를 사용하면 미리 생성된 모델 맵 오브젝트를 전달받아서 오브젝트에 추가 가능
- DispatcherServlet을 거쳐 뷰에 전달되는 모델에 자동으로 추가된다.
- 컨트롤러에서 ModelAndView를 별도로 만들어 리턴하는 경우에도 파라미터로 받은 맵에 추가한 오브젝트는 빠짐없이 모델에 추가

@ModelAttribute 메소드

@ModelAttribute는 컨트롤러 클래스의 일반 메소드에도 부여 가능
- 뷰에서 참고정보로 사용되는 모델 오브젝트를 생성하는 메소드를 지정하기 위해 사용
- @ModelAttribute가 붙은 메소드는 컨트롤러 클래스 안에 정의하지만 컨트롤러 기능을 담당하지 않음. 따라서 @RequestMapping을 함께 붙이지 않아야 한다.

@ModelAttribute("codes")
public List<Code> codes() { // 코드정보의 리스트를 받아서 리턴
	//리턴되는 오브젝트는 codes라는 이름으로 다른 컨트롤러가 실행될 때 모델에 자동 추가
	return codeService.getAllCodes(); 
}
  • @ModelAttribute 메소드가 필요한 이유?
    - 참조정보가 필요한 경우, 같은 클래스 내의 모든 컨트롤러 메소드에서 공통적으로 활용하는 정보라면 @ModelAttribute 메소드를 사용하는게 편리하기 때문

BindingResult

@ModelAttribute 파라미터와 함께 사용하는 BindingResult 타입의 오브젝트도 모델에 자동으로 추가
모델 맵에 추가될 때의 키는
'org.springframework.validation.BindingResult.모델이름'이다.

  • BindingResult 오브젝트가 모델에 자동으로 추가되는 이유?
    - 스프링의 JSP, 프리마커, 벨로시티 등의 뷰에 사용되는 커스텀 태크나 매크로에서 사용되기 때문
    - 주로 잘못 입력된 폼 필드의 잘못 입력된 값을 가져오거나 바인딩 오류 메세지를 생성할 때 사용된다.

◼ ModelAndView

컨트롤러가 리턴해야 하는 정보를 담고 있는 가장 대표적인 타입
하지만 @Controller에서는 ModelAndView를 이용하는 것 보다 편리한 방법이 많아 자주 사용되지는 않는다
- @Controller가 없는 이전 버전에서 만든 코드를 조금씩 변경하여 다듬을 때 사용함

◼ String

메소드의 리턴 타입이 스트링이면 이 리턴 값은 뷰 이름으로 사용
모델정보는 모델 맵 파라미터로 가져와 추가해주는 방법을 사용해야 한다.
모델은 파라미터로 맵을 가져와 넣어주고 리턴 값은 뷰 이름을 스트링 타입으로 선언하는 방식은 흔히 사용되는 @Controller 메소드 작성 방법이다.

@RequestMapping("/hello")
public String hello(@RequestParam String name, Model model) { //모델정보 가져오기
	model.addAttribute("name", name);
    return "hello";
}

◼ void

메소드의 리턴 타입을 아예 void로 할 수도 있음. 이때는 RequestToViewNameResolver 전략을 통해 자동생성되는 뷰 이름이 사용
URL과 뷰 이름을 일관되게 통일할 수만 있다면 적극 고려할만한 방법

@RequestMapping("/hello")
public void hello(@RequestParam String name, Model model) { //모델정보 가져오기
	model.addAttribute("name", name);
    //뷰 이름은 RequestToViewNameResolver를 통해 자동생성됨
}

◼ 모델 오브젝트

뷰 이름은 RequestToViewNameResolver로 자동생성하고 코드를 이용해 모델에 추가할 오브젝트가 하나뿐이라면 모델 오브젝트를 바로 리턴해도된다.
스프링은 리턴 타입이 미리 지정된 타입이나 void가 아닌 단순 오브젝트라면 이를 모델 오브젝트로 인식해 모델에 자동으로 추가해준다.
모델 이름은 리턴 값의 타입 이름을 따른다

@RequestMapping("/view")
public User view(@RequestParam int id) {
	return userService.getUser(id); // id값을 이용해 User오브젝트를 가져와 리턴 
}

◼ Map/Model/ModelMap

메소드의 코드에서 Map이나 Model, ModelMap 타입의 오브젝트를 직접 만들어서 리턴해주면 이 오브젝트는 모델로 사용됨.
- 직접 Map/Model/ModelMap 타입의 오브젝트를 코드에서 생성하는 것은 비효율
그럼에도 리턴 타입의 특징은 기억해두어야 한다!
단일 모델 오브젝트를 직접 리턴하는 방식을 사용하다가 실수할 수 있기 때문

//잘못된 코드
@RequestMapping("/view")
public Map view(@RequestParam int id) {
	Map userMap = userService.getUserMap(id);
    return userMap; // 그 자체로 모델 맵으로 인식해서 
    			   //엔트리 하나하나를 개별적인 모델로 다시 등록
}
@RequestMapping("/view")
public Map view(@RequestParam int id, Model model) { //Model 오브젝트 파라미터
	model.addAttribute("userMap", userService.getUserMap(id)); //코드에서 추가
}

◼ View

스트링 리턴 타입은 뷰 이름으로 인식한다고 설명했다. 그런데 뷰 이름 대신 뷰 오브젝트를 사용하고 싶다면 리턴 타입을 View로 선언하고 뷰 오브젝트를 넘겨주면 된다.

public class UserController {
	@Autowired MarshallingView userXmlView;
    
    @RequestMapping("/user/xml") public View userXml(@RequestParam int id) {
		...
        return this.userXmlView;
    }
}

◼ @ResponseBody

@RequestBody와 비슷한 방식으로 동작
@ResponseBody가 메소드 레벨에 부여되면 메소드가 리턴하는 오브젝트는 뷰를 통해 결과를 만들어내는 모델로 사용되는 대신, 메시지 컨버터를 통해 바로 HTTP 응답 메시지 본문으로 전환

//메세지 컨버터가 스트링 리턴 값을 HttpServletResponse의 출력 스트림에 넣음
@RequestMapping("/hello")
@ResponseBody 
public String hello() { 
	return "<html><body>Hello Spring</body></html>"; //스트링 리턴 값
}

📝 @SessionAttributes와 SessionStatus

HTTP 요청에 의해 동작하는 서블릿은 기본적으로 상태를 유지하지 않음. 하지만 애플리케이션은 상태를 유지할 필요가 있음.

◼ 도메인 중심 프로그래밍 모델과 상태 유지를 위한 세션 도입의 필요성

사용자 정보의 수정 기능을 예로 들어보자. 수정 기능을 위해서는 최소한 두 번의 요청이 서버로 전달되어야 한다. 첫번째는 수정 폼을 띄워달라는 요청이고, 두번째는 수정완료 버튼을 눌러서 서버에 전달할 때이다.
수정작업은 두 번의 요청과 두 개의 뷰 페이지만 있으면 되는 간단한 기능이다. 하지만 사용자가 수정한 폼에 오류가 있는 경우, 에러 메세지와 함께 수정 화면을 다시 보여줘야하여 복잡해질 수 있음. 또한, 상태를 유지하지 않고 폼 수정 기능을 만드려면 도메인 오브젝트 중심 방법보다는 계층 간의 결합도가 높은 데이터 중심 방식을 사용하기가 쉬워진다.

여기서는 폼 처리 시 상태 유지 문제만 살펴보겠다. 서버에서 하나의 요청 범위를 넘어서 오브젝트를 유지시키지 않으면 왜 데이터 중심 코드가 만들어지고 계층 간의 결합도가 올라가는지 생각해보자.

수정 폼 출력 컨트롤러는 아마 다음과 같이 만들어질 것이다.

@RequestMapping(value="/user/edit", method=RequestMethod.GET)
public String form(@RequestParam int id, Model model) {
	//DAO를 이용해 User오브젝트에 담기
	model.addAttribute("user", userService.getUser(id)); 
    return "user/edit";
}

문제는 여기서부터다. 사용자 정보 수정 화면이라고 모든 정보를 다 수정하는 것이 아니다. 사용자가 수정할 수 있는 필드는 제한되어있다. 따라서, User 오브젝트에 담겨 전달된 내용중에 수정 가능한 일부 정보만 폼의 <input>이나 <select>를 이용한 수정용 필드로 출력된다.
그렇기 때문에 저장버튼을 눌렀을 때는 수정 가능한 일부 필드만 서버로 전송된다는 문제가 발생하게 된다. 폼의 submit을 받는 컨트롤러 메소드에서 만약 다음과 같이 User오브젝트로 폼의 내용을 바인딩하게 했다면 어떻게 될까?

//submit을 처리하는 메소드는 요청 메소드 조건을 POST로 설정해 구분함
@RequestMapping(value="/user/edit" , method=RequestMethod.POST)
public String submit(@ModelAttribute User user) {
	this.userService.updateUser(user);
    return "user/editsuccess";
}

위 코드의 문제는 폼에서 <input>이나 <select>로 정의한 필드의 정보만 들어간다는 점이다. 따라서, submit()메소드의 파라미터로 전달되는 user오브젝트에는 그 외의 정보들이 비어있을 것이다. 이 반쪽짜리 user 오브젝트를 사용하게 된다면 서비스 계층의 로직을 처리하는 중에 치명적인 오류가 발생할지도 모른다
그렇다면 이러한 문제를 해결할 수 있는 방법을 한번 생각해보자.

히든 필드

수정을 위한 폼에 User의 모든 프로퍼티가 들어가지 않아 발생한 문제이니, User 오브젝트의 프로퍼티를 모두 넣어주는 방법을 사용할 수 있다. 수정하면 안되는 정보를 히든 필드를 통해 화면에는 보이지 않지만 submit하면 다시 서버로 전송되게할 수 있다.

<input type="hidden" name="level" value="1" />
<input type="hidden" name="point" value="300" />
<input type="hidden" name="lastAccessIp" value="100.10.20.30" />

이 방법은 두 가지 심각한 문제가 있다

  • 데이터 보안에 심각한 문제를 일으킨다.
    - 폼의 히든 필드는 HTML소스를 열어보면 그 내용과 필드 이름까지 쉽게 알아낼 수 있다.
  • 사용자 정보에 새로운 필드가 추가되거나 수정되었을 경우, 폼에 이 정보에 대한 히든 필드를 추가해주지 않으면 null로 저장되는 현상이 발생함.

DB 재조회

폼으로부터 수정된 정보를 받아 User 오브젝트에 넣어줄 때 빈 User 오브젝트 대신 DB에서 다시 읽어온 User 오브젝트를 이용하는 것이다.

@RequestMapping(value="/user/edit", method=RequestMethod.POST)
//폼에서 전달된 정보를 받을 별도의 User 오브젝트를 정의
public String submit(@ModelAttribute User formUser, @RequestParam int id) {
	//DB에서 모든 필드 정보가 다 담긴 User 오브젝트를 다시 가져온다.
	User user = this.userService.getUser(id);
    
    //forumUser의 내용을 DB에서 가져온 user오브젝트에 넣어준다.
    user.setName(formUser.getName());
    user.setPassword(formUser.getPassword());
    user.setEmail(forumUser.getEmail());
    ...
    
    this.userService.updateUser(user);
    return "user/editsuccess";
}

완벽해 보이긴 하지만 이 방법에는 몇 가지 단점이 있다.

  • 폼을 submit할 때마다 DB에서 사용자 정보를 다시 읽는 부담이 있다.
    - 폼에서 전달받은 정보를 일일이 복사하는 일은 매우 번거롭고 실수하기 쉽다. 폼에서 전달되는 필드가 어떤 것인지 정확히 알고 이를 복제해주어야 한다.

계층 사이의 강한 결합

계층 사이에 강한 결합을 준다. 강한 결합이라는 의미는 각 계층의 코드가 다른 계층에서 어떤 일이 일어나고 어떤 작업을 하는지를 자세히 알고 그에 대응해서 동작하도록 만든다는 뜻이다.

이 방식은 앞에서 지적한 폼 수정 문제의 전제를 바꾸어 문제 자체를 제거한다.
기본 전제는 서비스 계층의 updateUser() 메소드가 User라는 파라미터를 받으면 그 User는 getUser()로 가져오는 User와 동등하다고 본다는 것이다.
=> 수정가능한 일부 정보만 전달되는 것이 아닌 동등한 User 오브젝트를 받아 수정해버린다.

하지만 이렇게 결합도가 높은 코드를 만들 경우, 애플리케이션이 복잡해지기 시작하면 단점이 드러난다.

  • 코드의 중복이 늘어난다. - 재사용하기가 힘듦
  • 계층 사이에 강한 결합이 있기 때문에 한쪽을 수정하면 다른 쪽도 수정을 해주어야 한다.
  • 테스트하기가 힘들다.

◼ @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) { ... }
}

기존에 만든 form()과 submit()은 전혀 손을 대지 않고 @SessionAttributes에 "user"라는 이름을 넣어서 클래스에 부여해주는 것만으로 모든 문제가 해결된다.
@SessionAttributes는 두 가지 기능을 한다.

  • 컨트롤러 메소드가 생성하는 모델정보 중에서 @SessionAttributes가 지정한 이름과 동일한 것이 있다면 이를 세션에 저장해준다.
  • @ModelAttribute가 지정된 파라미터가 있을 때 이 파라미터에 전달해줄 오브젝트를 세션에서 가져오는 것이다. - form()에서 세션에 저장한 것 가져옴

❗ 단, @SessionAttributes의 기본 구현인 HTTP 세션을 이용한 세션 저장소는 모델 이름을 세션에 저장할 애트리뷰트 이름으로 사용한다는 점을 주의하자.

◼ SessionStatus

@SessionAttribute를 사용할 때는 더 이상 필요 없는 세션 애트리뷰트를 코드로 제거해줘야 한다는 점을 잊지 말자. 이를 제거하는 책임은 컨트롤러에게 있다.
세션을 남겨두는 이유폼을 한번 submit했다고 해서 항상 작업이 완료되는 것은 아니기 때문이다. - 오류가 난 경우에는 다시 수정 폼을 띄워주고 수정하기를 기다려야 함.
그래서 폼의 작업을 마무리하는 코드에서는 작업을 성공적으로 마쳤다면 SessionStatus 오브젝트의 setComplete() 메소드를 호출해서 세션에 저장해뒀던 오브젝트를 제거해줘야 한다.

@RequestMapping(value="/user/edit", method=RequestMethod.POST)
public String submit(@ModelAttribute User user, SessionStatus sessionStatus) {
	this.userService.updateUser(user);
    sessionStatus.setComplete(); // 현재 컨트롤러에 저장된 정보를 모두 제거
    return "user/editsuccess";
}

❗ HTTP세션에 불필요한 오브젝트가 쌓여가는 것은 위험하므로, SessionStatus를 이용해 세션을 정리해주는 코드가 항상 같이 따라다녀야 한다는 사실을 기억해두자.

◼ 등록 폼을 위한 @SessionAttributes 사용

등록 폼은 수정 폼과 달리 DB에서 가져온 정보를 출력할 필요가 없다. 따라서 초기 모델 오브젝트를 세션에 저장해뒀다가 폼 submit시 이를 가져와 프로퍼티를 넣어주는 식으로 하지 않아도 된다. - @SessionAttribute를 사용할 필요가 없음.

하지만, @SessionAttributes가 등록 화면을 위한 컨트롤러에서도 유용하게 쓰일 수 있다.

  • 등록 폼을 위한 도메인 오브젝트를 미리 초기화해놓고 사용하는 경우
    - 도메인 오브젝트가 복잡한 경우
    - 사용자의 입력을 돕기 위해 디폴트 값을 보여주는 경우
  • 스프링 폼 태그를 사용하기 위해서
    - 스프링 폼 태그는 등록, 수정 화면에 상관없이 폼을 출력할 때 폼의 내용을 담은 모델 오브젝트를 필요로 한다.
    - 폼의 입력 값에 오류가 있는 경우, 기존에 입력한 값을 보여주어야 한다.

=> 처음부터 모델 오브젝트를 만들어서 폼에 그 내용을 보여줄 생각이라면 아예 최초에 폼을 출력하는 컨트롤러에서 빈 모델 오브젝트를 만들어서 리턴하는 편이 낫다.
또한, 불필요하게 오브젝트가 다시 생성되지 않도록 @SessionAttributes를 이용해 모델 오브젝트를 저장했다가 다시 사용하는게 좋다.

📖 모델 바인딩과 검증

컨트롤러 메소드에 @ModelAttribute가 지정된 파라미터를 @Controller 메소드에 추가하면 크게 세 가지 작업이 자동으로 진행


1. 파라미터 타입의 오브젝트를 만든다. - 이를 위해 디폴트 생성자가 반드시 필요
@SessionAttributes에 의해 세션에 저장된 모델 오브젝트가 있다면, 새로운 오브젝트를 생성하는 대신 세션에 저장되어 있는 오브젝트를 가져온다.

2. 준비된 모델 오브젝트의 프로퍼티에 웹 파라미터를 바인딩해준다.
HTTP를 통해 전달되는 파라미터는 기본적으로 문자열로 되어있음. 만약 모델 오브젝트의 프로퍼티가 스트링 타입이 아니라면 적절한 변환이 필요.
전환이 불가능한 경우라면 BindingResult 오브젝트 안에 바인딩 오류를 저장해서 컨트롤러로 넘겨주거나 예외를 발생시킨다.

3. 모델의 값을 검증한다.
타입에 대한 검증 이외의 검증할 내용이 있다면 적절한 검증기를 등록해서 모델의 내용을 검증할 수 있다.

여기서는 두 번째 작업인 파라미터 바인딩이 어떻게 진행되는지와, 세 번째 작업인 검증을 어떻게 진행할 수 있는지 자세히 살펴보겠다.

스프링에서 바인딩이라고 말할 때는 오브젝트의 프로퍼티에 값을 넣는 것을 말한다.
스프링에서는 크게 두 가지의 프로퍼티 바인딩을 지원한다.

  • XML 설정파일을 사용해서 빈을 정의하면서 <property> 태그로 값을 주입하도록 설정하는 것
  • 바인딩이 필요한 곳HTTP를 통해 전달되는 클라이언트의 요청모델 오브젝트 등으로 변환할 경우

📝 PropertyEditor

스프링이 기본적으로 제공하는 바인딩용 타입 변환 API
각 프로퍼티 타입에 대해 프로퍼티 창의 문자열과 자바빈의 프로퍼티 타입 사이의 타입 변환을 담당.

◼ 디폴트 프로퍼티 에디터

1장에서 프로퍼티 값 설정을 설명할 때 이미 스프링이 제공하는 기본 프로퍼티 에디터의 종류와 지원 타입에 대해 살펴보았다. 프로퍼티 에디터는 XML의 value 애트리뷰트 뿐아니라 @Controller의 파라미터에도 동일하게 적용된다.

◼ 커스텀 프로퍼티 에디터

스프링이 디폴트로 등록해서 적용해주는 프로퍼티 에디터는 자바의 기본적인 타입 20여 가지에 불과함.
애플리케이션에서 직접 정의한 타입으로 직접 바인딩을 하고 싶다면, 프로퍼티 에디터를 직접 작성하면 된다.

프로퍼티 에디터를 잘 활용하면 반복적으로 등장하는 변환 코드 제거 가능
특히, 클라이언트의 요청 파라미터를 적절한 타입으로 가져올 수 있게 해주면 컨트롤러 코드와 뷰 모두 깔끔하게 만들 수 있음

◼ @InitBinder

컨트롤러 메소드에서 바인딩이 일어나는 과정에서 @Controller 메소드를 호출해줄 책임이 있는 AnnotationMethodHandlerAdapterHTTP 요청을 파라미터 변수에 바인딩해주는 작업이 필요한 애노테이션을 만나면 먼저 WebDataBinder를 만든다.

WebDataBinder의 기능 중에서 HTTP 요청으로부터 가져온 문자열을 파라미터 타입의 오브젝트로 변환하는 기능이 포함되어있음. 여기서 커스텀 프로퍼티 에디터를 적용하려면 WebDataBinder에 프로퍼티 에디터를 직접 등록해주어야 함.

하지만 WebDataBinder는 AnnotationMethodHandlerAdapter가 바인딩을 진행하는 과정 내부에 생성되므로 외부로 직접 노출이 되지 않음!
- 스프링이 제공하는 WebDataBinder 초기화 메소드를 이용해야 함!

@InitBinder 라는 애노테이션이 부여되어있고, 파라미터로 WebDataBinder 타입이 정의된 메소드를 하나 생성한다. - 파라미터를 바인딩하기 전에 자동으로 호출되어 커스텀 프로퍼티 에디터를 추가할 기회를 제공

WebDataBinder에 커스텀 프로퍼티 에디터를 등록하는 방법은 다시 두 가지로 구분 가능

특정 타입에 무조건 적용되는 프로퍼티 에디터 등록

적용 타입과 프로퍼티 에디터 두 개를 파라미터로 받는 registerCustomerEditor()를 사용프로퍼티 에디터를 등록했다면, 해당 타입을 가진 바인딩 대상이 나오면 항상 프로퍼티 에디터가 적용된다.

특정 이름의 프로퍼티에만 적용되는 프로퍼티 에디터 등록

타입과 프로퍼티 에디터 오브젝트 외에, 추가로 적용할 프로퍼티 이름을 지정
- 같은 타입이지만 프로퍼티 이름이 일치하지 않는 경우에는 등록한 커스텀 프로퍼티 에디터가 적용되지 않는다.
- 프로퍼티 이름이 필요하므로 @RequestParam과 같은 단일 파라미터 바인딩에는 적용되지 않음.
- 이미 프로퍼티 에디터가 존재하는 경우 사용하기 적합하다. WebDataBinder는 바인딩 작업 시 커스텀 프로퍼티 에디터를 먼저 적용해보고 적절한 프로퍼티 에디터가 없으면 그때 디폴트 프로퍼티 에디터를 사용

◼ WebBindingInitializer

모든 컨트롤러에 적용해도 될 만큼 많은 곳에서 필요한 프로퍼티 에디터라면 한 번에 모든 컨트롤러에 적용하는 것이 편하다. 이런 상황에 WebBindingInitializer를 이용하면 된다.

//인터페이스 구현하기
public class MyWebBindingInitializer implements WebBindingInitializer {
	public void initBinder(WebDataBinder binder, WebRequest request) {
    	binder.registerCustomEditor(Level.class, new LevelPropertyEditor());
    }
}
<bean class=
"org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
  	<property name="webBindingInitializer">
  		<bean class="springbook...MyWebBindingInitializer" />
  	</property>
</bean>

빈으로 등록 후 initBinder() 메소드를 사용하면 일괄적으로 모든 컨트롤러에 바인딩 작업에 적용된다.

◼ 프로토타입 빈 프로퍼티 에디터

오브젝트를 매번 새로 만들지 않고 프로퍼티 에디터를 싱글톤 빈으로 등록해두고 공유하여 쓸수는 없을까? - 안타깝지만 싱글톤 빈으로 등록할 수가 없음.
- 프로퍼티 에디터를 통해 변환되는 오브젝트는 프로퍼티 에디터 내부에 잠시 저장됨.
=> 상태를 가진다.

(상태를 가지면 싱글톤 빈으로 공유할 수 없음 - 데이터 꼬임)

프로퍼티 에디터가 다른 스프링 빈을 참조해야 한다면 어떨까?
다른 빈을 참조해서 DI 받으려면 자신도 스프링의 빈으로 등록이 되는게 원칙
하지만 앞에서 프로퍼티 에디터는 싱글톤 빈으로 등록이 불가능하다고 했음.
-> 프로토타입 스코프의 빈으로 만들어져야 함.

어떤 경우에 프로퍼티 에디터가 다른 빈을 참조해야 할까?
자주 활용되는 건 HTTP 요청 파라미터로 도메인 오브젝트의 ID를 제공받으면 이를 바인딩 과정에서 ID에 해당되는 실제 도메인 오브젝트로 만들어주는 것

// 다른 도메인 오브젝트를 참조하는 도메인 오브젝트
public class User {
	int id;
    String name;
    Code userType; // DB에 저장되는 독립적인 도메인 오브젝트 타입
    // 수정자, 접근자 코드 생략
}
// 참조정보 모델 메소드
@ModelAttributes("userTypes")
public List<Code> userTypes() {
	return this.codeService.findCodesByCodeCategory(CodeCategory.USERTYPE);
}
//사용자 타입을 선택하는 <select>
<select name="userType">
	<option value="1">관리자</option>
    <option value="2">회원</option>
    <option value="3">손님</option>
</select>

User의 id, name 프로퍼티에 해당하는 HTTP 요청 파라미터는 간단히 바인딩 되지만, userType 프로퍼티에 전달되는 value 값은 간단히 Code 타입으로 전환되지 않음
- 프로퍼티 에디터가 필요함.

이처럼 폼의 파라미터가 모델의 프로퍼티에 바인딩될 때 단순 타입이 아닌 경우 어떻게 이를 바인딩할 수 있는지 살펴보자

별도의 codeid 필드로 바인딩하는 방법

가장 단순한 방법이다. Code userType 프로퍼티로 직접 바인딩하는 대신 참조 ID 값을 저장할 수 있도록 임시 프로퍼티를 만들고 이 프로퍼티로 값을 받음.

// 다른 도메인 오브젝트를 참조하는 도메인 오브젝트
public class User {
	...
    Code userType; // DB에 저장되는 독립적인 도메인 오브젝트 타입
    int userTypeId; // id를 저장할 임시 프로퍼티
    ...
}
//사용자 타입을 선택하는 <select>
<select name="userTypeId">
	<option value="1">관리자</option>
    <option value="2">회원</option>
    <option value="3">손님</option>
</select>

가장 간단한 방법이지만 그만큼 단점이 있다.

  • 매번 Code정보를 DB에서 가져와야하는 부담
  • 매번 컨트롤러나 서비스 계층 코드에서 위와 같이 id에 해당하는 임시 프로퍼티 값을 이용해서 도메인 오브젝트 타입의 프로퍼티를 설정해주는 작업을 해야함.
    - 프로퍼티 개수가 많으면 코드가 지저분해지고 빼먹을 위험도 존재한다
  • 임시 저장용 프로퍼티가 추가되어야 한다.

모조 오브젝트 프로퍼티 에디터

userType이라는 이름으로 전달되는 1, 2, 3과 같은 id 값을 Code 오브젝트로 변환해주는 프로퍼티 에디터를 만드는 것
- 오직 id 값만 가진 불완전한 오브젝트 -> 모조 오브젝트

// Code 모조 프로퍼티 에디터
public class FakeCodePropertyEditor extends PropertyEditorSupport {
	public void setAsText(String text) throws IllegalArgumentException {
    	// Code 오브젝트를 만들고 폼의 셀렉트박스에서 전달된 id 값만 넣어줌
    	Code code = new Code();
        
        code.setId(Integer.parseInt(text));
        setValue(code);
    }
    
    public String getAsText() {
    	return String.valueOf(((Code)getValue()).getid());
    }
}
// Code 모조 프로퍼티 에디터 등록
@InitBinder public void initBinder(WebDataBinder dataBinder) {
	dataBinder.registerCustomEditor(Code.class,
    	new FakeCodePropertyEditor());
}

이 방법의 문제점

  • userType 프로퍼티에 들어간 Code 오브젝트는 사실 id를 제외한 나머지가 다 null인 비정상적인 오브젝트라는 점

하지만 잘 활용하면 꽤나 유용한 방법이다.

  • 통째로 저장할 필요가 없는 오브젝트폼에서 수정하거나 등록한 사용자 정보를 단순히 저장하는 것이 목적인 경우

프로토타입 도메인 오브젝트 프로퍼티 에디터

Code 타입의 프로퍼티 에디터를 적용하는데, 두 번째 방법과는 달리 DB에서 읽어온 완전한 Code오브젝트로 변환해준다. - 모조 오브젝트와 달리 다른 계층에서 사용할 때 제한을 받지 않음
프로퍼티 에디터가 DI를 받기 위해 빈으로 등록되어야하기 때문에 프로토타입 빈으로 등록되어야 한다.

@Component
@Scope("prototype") //프로토타입 빈으로 사용
public class CodePropertyEditor extends PropertyEditorSupport {
	@Autowired CodeService codeService;
   
   public void setAsText(String text) throws IllegalArgumentException {
   	setValue(this.codeService.getCode(Integer.parseInt(text)));
   }
   
   public String getAsText() {
   	return String.valueOf(((Code)getValue()).getId());
   }
}
public class UserController {
	@Inject Provider<CodePropertyEditor> codeEditorProvider;
    
    @InitBinder public void initBinder(WebDataBinder dataBinder) {
    	dataBinder.registerCustomEditor(Code.class, codeEditorProvider.get());
    }
}
  • 매번 DB에서 Code 오브젝트를 새로 읽어와야 해서 미미하지만 성능에 부담을 줌

📝 Converter와 Formatter

프로퍼티 에디터는 근본적인 위험성을 지니고 있고, 불편하다.
그래서 스프링 3.0에는 PropertyEditor를 대신할 수 있는 새로운 타입 변환 API가 도입됨.

바로 Converter 인터페이스이다.
- Converter는 변환과정에서 메소드가 한 번만 호출됨.
한 번만 호출된다는 것은 상태를 인스턴스 변수로 저장하지 않는다는 뜻
즉, 멀티스레드 환경에서 안전하게 공유하여 사용 가능

◼ Converter

양방향 변환 기능을 제공하는 PropertyEditor와 다르게 Converter 메소드는 소스 타입에서 타깃 타입으로의 단방향 변환만 지원
Converter는 소스와 타깃의 타입을 임의로 지정할 수 있음
- 스트링 타입으로 한쪽이 고정되어있는 Property Editor와 달리 범용적인 컨버터 정의 가능

// Converter 인터페이스
public interface Converter<S, T> { //제너릭스로 소스와 타깃의 타입을 미리 정의 가능
	T convert(S source);
}

Converter의 소스와 타깃을 바꿔서 컨버터를 하나 더 만들면 양방향 변환이 가능

◼ ConversionService

ConversionService여러 종류의 컨버터를 이용해서 하나 이상의 타입 변환 서비스를 제공해주는 오브젝트를 만들 때 사용하는 인터페이스
컨트롤러의 바인딩 작업에 컨버터를 적용하기 위해서 ConversionService 타입의 오브젝트를 통해 WebDataBinder에 설정해주어야 한다.

스프링 3.0에 추가된 새로운 타입 변환 오브젝트인 GenericConverter와 ConverterFactory를 이용해서도 만들수 있다.

  • GenericConverter
    - 하나 이상의 소스-타깃 타입 변환을 한 번에 처리할 수 있는 컨버터를 만들 수 있음.
    - 모델의 프로퍼티에 대한 바인딩 작업을 할 때 제공받을 수 있는 메타정보인 필드 컨텍스트를 제공받을 수 있음.
  • ConverterFactory
    - 제너릭스를 활용해서 특정 타입에 대한 컨버터 오브젝트를 만들어주는 팩토리를 구현할 때 사용

@MVC 컨트롤러의 메소드 파라미터를 위해 사용하는 WebDataBinder에 ConversionService를 구현한 GenericConversionService를 설정하는 방법은 두 가지가 있다.

@InitBinder를 통한 수동 등록

  • 일부 컨트롤러에만 직접 구성한 ConversionService를 적용하는 경우
  • 하나 이상의 ConversionService를 만들어두고 컨트롤러에 따라 다른 변환 서비스를 선택하고 싶은 경우
    => @InitBinder 메소드를 통해 직접 원하는 ConversionService를 설정 가능

GenericConversionService직접 만든 컨버터 등의 변환 오브젝트를 추가하는 방법

  1. GenericConversionService를 상속해서 새로운 클래스를 만들고, 생성자에서 addConverter() 메소드를 이용추가할 컨버터 오브젝트를 등록하는 방법
    - 확장한 클래스를 빈으로 등록하여 사용
  2. 추가할 컨버터 클래스를 빈으로 등록해두고 ConversionServiceFactoryBean을 이용해서 프로퍼티로 DI 받은 컨버터들로 초기화된 GenericConversionService를 가져오는 방법

ConfigurableWebBindingInitializer를 이용한 일괄 등록
컨버터는 싱글톤이라서 모든 컨트롤러의 WebDataBinder에 적용해도 문제가 되지 않으므로, 모든 컨트롤러에 한 번에 적용하는 방법이 존재.

모든 컨트롤러에 적용하는 프로퍼티 에디터를 정의할 때 사용한 WebBindingInitializer를 이용.
ConversionService를 적용할 때는 ConfigurableWebBindingInitializer를 사용하면 편리
- 코드를 따로 작성하지 않고 빈 설정만으로도 WebBindingInitializer 빈을 등록할 수 있기 때문

<bean class=
"org.springframework.web.servlet.mvc.annotation.AnnotaionMethodHandlerAdapter">
  	<property name="webBindingInitializer" ref="webBindingInitilaizer"/>
</bean>

<bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
  	<property name="conversionService" ref="conversionService"/>
</bean>

<bean id="conversionService" class=
      "org.springframework.context.support.ConversionServiceFactoryBean">
  	<property name="converters">
      ...<!-- 적용할 컨버터 빈 목록 -->
    </property>
</bean>

◼ Formatter와 FormattingConversionService

스프링은 Formatter 타입의 추가 변환 API를 제공. 이 포맷터는 스트링 타입의 폼 필드 정보컨트롤러 메소드의 파라미터 사이양방향으로 적용할 수 있도록 두 개의 변환 메소드를 가짐

Formatter는 그 자체로 Converter와 같이 스프링이 기본적으로 지원하는 범용 API가 아니므로 GenericConversionService 등에 직접 등록할 수는 없음.
대신 Formatter 구현 오브젝트를 GenericConverter 타입으로 포장해서 등록해주는 기능을 가진 FormattingConversionService를 통해서만 적용될 수 있다.

◼ 바인딩 기술의 적용 우선순위와 활용 전략

사용자 정의 타입의 바인딩을 위한 일괄 적용: Converter

애플리케이션에서 정의한 타입이면서 모델에서 자주 활용되는 타입인 경우
- 컨버터로 만들고 컨버전 서비스로 묶어서 일괄적용하는 것이 가장 편리함

필드와 메소드 파라미터, 애노테이션 등의 메타정보를 활용하는 조건부 변환 기능: ConditionalGenericConverter

특정 타입에 대해 동일한 변환 작업을 하는 것이 아닌, 바인딩이 일어나는 필드와 메소드 파라미터 등의 조건에 따라 변환을 할지 말지 결정하는 경우나, 이런 조건을 변환 로직에 참고할 필요가 있는 경우
- ConditionalGenericConverter를 이용
(Converter와 직접적인 관계가 없는 인터페이스)

ConditionalGenericConverter도 GenericConversionService에 등록해서 바인딩에 일괄 적용 가능

애노테이션 정보를 활용한 HTTP 요청과 모델 필드 바인딩 : AnnotationFormatterFactory와 Formatter

@NumberFormat이나 @DateTimeFormat처럼 필드에 부여하는 애노테이션 정보를 이용해 변환 기능을 지원하려면 AnnotationFormatterFactory를 이용애노테이션에 따른 포맷터를 생성해주는 팩토리를 구현해야 한다.
AnnotationFormatterFactory 타입 오브젝트는 FormattingConversionService를 통해 등록해주어야 한다. 이때, 내부적으로 애노테이션으로 변환 대상을 선별하는 ConditionalGenericConverter로 변환됨.

특정 필드에만 적용되는 변환 기능: PropertyEditor

특정 모델의 필드에 제한해서 변환 기능을 적용해야 할 경우가 존재.

  • 디폴트 프로퍼티 에디터로 처리되는 타입이지만, 특정 필드에 대해 별도의 변환 기능을 제공하고 싶은 경우
    - 커스텀 프로퍼티 에디터를 만들어서 컨트롤러의 @InitBinder에서 필드 조건을 추가해서 등록 가능.

📝 WebDataBinder 설정 항목

WebDataBinder는 여러 가지 유용한 바인딩 옵션을 지정해줄 수 있다.

◼ allowedFields, disallowedFields

스프링은 WebBinder 안에 바인딩이 허용된 필드 목록을 넣을 수 있는 allowedFields금지 필드 목록을 넣을 수 있는 disallowedFields 프로퍼티를 설정할 수 있게 해준다.

  • allowedFields : 매우 적극적인 방법. 바인딩할 수 있는 목록을 지정해서 그 외의 모든 요청 파라미터는 다 막아줌.
  • disallowedFields : 기본적으로는 다 허용하지만 특별한 이름을 가진 요청 파라미터만 막아줌.
@InitBinder
public void initBinder(WebDataBinder dataBinder) {
	//세 개의 필드 이외의 것이 오면 바인딩에서 제외
	dataBinder.setAllowedFields("name", "email", "tel"); 
}

◼ requiredFields

컨트롤러가 자신이 필요로 하는 필드 정보가 폼에서 모두 전달됐는지를 확인하고 싶은 경우, 이를 예외를 발생시켜서 알아내는 방법
requiredFields는 바인딩 시 필수 파라미터 중에서 빠진게 있다면 바인딩 에러로 처리하도록 만들어줄 수 있음.

◼ fieldMarkerPrefix

자동 바인딩의 단점 : HTTP 요청 파라미터가 전달되는 것이 있으면 그것만 모델의 프로퍼티에 바인딩해줌
- 폼에서 필요한 필드정보를 제공해주지 않으면 해당 프로퍼티에는 아무런 변화도 일어나지 않음. (requiredFields를 사용)

하지만, 폼에 필드를 넣어줬음에도 불구하고 HTTP 파라미터가 제공되지 않는 희한한 경우가 한 가지 존재 - 폼의 체크박스

HTML 폼의 체크박스는 체크를 하지 않으면 아예 해당 필드의 값을 서버로 전송 X

위 문제를 해결하기 위해 특별한 접두어가 붙은 필드 이름을 가진 마커 히든 필드를 추가하는 방식을 사용해 폼의 특정 필드를 체크박스라고 알려주도록 함.

<input type="checkbox" name="autoLogin" />
<input type="hidden" name="_autoLogin" value="on" />

필드 이름 앞에 '_'를 붙인 것을 필드마커라고 부르고, 이를 히든 필드 이름으로 넣어주면 스프링은 필드마커가 있는 HTTP 파라미터를 발견하면 필드마커를 제외한 이름의 필드가 폼에 존재한다고 생각함.
- 만일 필드가 폼에 존재하지 않으면 체크박스를 해제했기 때문이라 판단하고 값을 리셋시켜준다.

◼ fieldDefaultPrefix

WebDataBinder의 프로퍼티인 fieldDefaultPrefix의 디폴트 값은 느낌표(!)이다.
필드 디폴트는 히든 필드를 이용해 체크박스에 대한 디폴트 값을 지정하는데 사용
- 때로는 체크박스에 대응하는 모델의 프로퍼티 타입이 boolean이 아닐수도 있음

<!--type이라는 이름의 파라미터가 존재하지 않으면 디폴트 값으로 member를 바인딩-->
<input type="hidden" name="!type" value="member" />

📝 Validator와 BindingResult, Errors

스프링은 타입 이외의 검증과정에서 사용할 수 있는 Validator 인터페이스 제공
검증과정의 결과는 BindingResult를 통해 확인할 수 있다.
Errors는 오류를 발견하면 그에 대한 정보를 등록할 수 있는 메소드를 제공

◼ Validator

스프링에서 범용적으로 사용할 수 있는 오브젝트 검증기를 정의할 수 있는 API
@Controller로 HTTP요청을 @ModelAttribute 모델에 바인딩할 때 주로 사용

package org.springframework.validation;
...
public interface Validator {
	// 검증기가 검증할 수 있는 오브젝트 타입인지 확인해주는 메소드
	boolean supports(Class<?> clazz); 
    
    // supports() 메소드를 통과한 경우에만 호출. 값을 검증하는 코드 작성
    void validate(Object target, Errors errors);
}

🔷 스프링에서 Validator를 적용하는 방법

컨트롤러 메소드 내의 코드

Validator는 빈으로 등록이 가능하므로 이를 컨트롤러에서 DI 받아두고 각 메소드에서 필요에 따라 직접 validate() 메소드를 호출해서 검증 작업을 진행 가능

// 컨트롤러 코드에서 검증기를 사용하는 방법
@Controller
public class UserController {
	@Autowired UserValidator validator;
    
    @RequestMapping("/add")
    public void add(@ModelAttribute User user, BindingResult result) {
    	//모델 오브젝트 타입을 확인할 필요가 없으므로 supports()는 생략
    	this.validator.validate(user, result);
        if (result.hasErrors()) {
        	// 오류가 발견된 경우의 작업
        }
        else {
        	// 오류가 없을 때의 작업
        }
    }
    ...
}

@Valid를 이용한 자동 검증

JSR-303의 @javax.validation.Valid 애노테이션을 사용하는 것
사실 스프링이 @Valid라는 애노테이션을 차용했을 뿐, 내부적으로는 Validator를 이용한 검증 수행

@Controller
public class UserController {
	@Autowired UserValidator validator;
    
    @InitBinder
    public void initBinder(WebDataBinder dataBinder){
    	dataBinder.setValidator(this.validator);
    }
    
    @RequestMapping("/add")
    public void add(@ModelAttribute User user, BindingResult result) {
    }
    ...

서비스 계층 오브젝트에서의 검증

자주 사용되는 방법은 아니지만 Validator를 원한다면 외부에서 사용가능
- 비즈니스 로직이 적용된 Valiator를 서비스 계층에서도 활용할 수 있다.
서비스 계층 오브젝트에서 반복적인 검증 기능이 사용된다면 Valiator를 통해 코드를 분리하고 DI받아 사용하게 만들 수 있음.

서비스 계층을 활용하는 Validator

Validator를 어떤 방법이든 적용했을 경우Validator를 빈으로 등록해서 서비스 계층의 기능을 사용해 검증 작업 수행 가능

  • 서비스 계층에 담긴 검증 로직을 재사용 가능
    - 그 결과는 컨트롤러 내에서 BindingResult를 통해 전달받을 수 있음.
    -> 예외를 만들어 던지고 받는 코드가 없어도 된다.
  • 두 번 이상 서비스 계층을 호출한다는 단점 존재

◼ JSR-303 빈 검증 기능

JSR-303 빈 검증 방식도 스프링에서 사용 가능. 스프링에서는 LocalValidatorFactoryBean을 이용해 JSR-303의 검증 기능 사용 가능
LocalValidatorFactoryBean을 빈으로 등록하면 컨트롤러에서 Validator 타입을 DI 받아서 @InitBinder에서 WebDataBinder에 설정하거나 코드에서 직접 Validator처럼 사용 가능

  • 모델 오브젝트 필드에 달린 제약조건 애노테이션을 이용해 검증 진행 가능
    - @NotNull : 필드의 값이 null이 아님을 확인하는 제약조건
    - @Min : 최소값을 지정할 수 있는 제약조건
    제약조건은 새로 정의할 수도 있음!

◼ BindingResult와 MessageCodeResolver

BindingResult에는 모델의 바인딩 작업 중에 나타난 타입 변환 오류정보와 검증 작업에서 발견된 검증 오류정보가 모두 저장됨.
이 오류정보는 보통 컨트롤러에 의해 폼을 다시 띄울 때 활용

등록된 오류정보에는 에러 코드가 기본적으로 들어감. 이 에러 코드는 MessageCodeResolver를 통해 좀 더 상세한 메세지 키 값으로 확장.

EX) user라는 모델을 검증하던 중 name 필드 이름과 field.required라는 에러코드가 BindingResult에 등록된 경우

MessageCodeResolver는 다음과 같이 4개의 후보를 생성

1. 에러코드.모델이름.필드이름 : field.required.user.name
2. 에러코드.필드이름 : field.required.name
3. 에러코드.타입이름 : field.required.User
4. 에러코드 : field.required

위에서부터 우선적으로 메세지를 찾음.
- 우선순위가 높은 메세지를 찾으면 낮은 메세지는 무시

검증 작업 중발견한 오류가 특정 필드에 속한 것이 아니라면 reject()를 사용해 모델 레벨의 글로벌 에러로 등록할 수 있음.

DefaultMessageCodeResolver가 다음과 같은 두 개의 메세지 키 후보를 만들어줌.

1. 에러코드.모델이름 : invalid.data.user
2. 에러코드 : invalid.data

◼ MessageSource

메세지 코드는 messages.properties 리소스 번들 파일을 한 번 거치고, MessageSourceResolver를 한번 더 거쳐서 최종적인 메세지로 만들어진다.

스프링에서 MessageSource 구현 방식에는 두 가지 종류가 있다

  • 코드로 메세지를 등록할 수 있는 StaticMessageSource
  • messages.properties 리소스 번들 방식을 사용하는 ResourceBundleMessageSource
    - 기본적으로는 리소스 번들 파일에 저장해두고 관리하는 것이 편해 이 방법 사용

MessageSource는 기본적으로 다음 네 가지 정보를 활용해 최종적인 메세지를 만들어 냄

코드

BindingResult 또는 Errors에 등록된 에러 코드DefaultMessageCodeResolver를 이용해서 필드와 오브젝트 이름의 조합으로 만들어낸 메세지 키 후보 값

메세지 파라미터 백업

BindingResult 이나 Errors의 rejectValue(), reject()에는 Object[] 타입의 세 번째 파라미터를 지정할 수 있음
- messages.properties에는 다음과 같이 메세지에 적용 가능한 파라미터 값 지정 가능

field.min{0}보다 적은 값은 사용할 수 없습니다.
// rejectValue()에서 {0}에 들어갈 파라미터 값을 Object 배열로 넣어줄 수 있다.

디폴트 메세지
메세지 키 후보 값을 모두 이용해 messages.properties를 찾아봤지만 메세지가 발견되지 않는 경우를 대비디폴트 에러 메세지를 등록해둘 수 있음

rejectValue()의 네 번째 파라미터가 바로 이 디폴트 에러 메세지다.

// "입력해주세요" - 디폴트 에러 메세지
rejectValue("name", "field.required", null, "입력해주세요");

지역정보

LocaleResolver에 의해 결정된 현재의 지역정보
- 지역에 따른 리소스 번들 파일 사용 가능.

📝 모델의 일생

스프링 MVC를 제대로 이해하고 활용하려면 모델이 어떻게 만들어지고, 다뤄지고, 사용되는가에 대한 이해가 가장 중요하다.
지금까지 다뤄왔던 모델에 관련된 기능을 모델의 생성부터 제거까지의 전 과정을 각 단계별로 참여하는 관련 기능과 함께 정리해보자.

◼ HTTP 요청으로부터 컨트롤러 메소드까지

업로드중..

@ModelAttribute 메소드 파라미터

컨트롤러 메소드의 모델 파라미터와 @ModelAttribute로부터 모델 이름, 모델 타입 정보를 가져옴.

@SessionAttribute 세션 저장 대상 모델 이름

모델 이름과 동일한 것이 있다면 HTTP 세션에 저장해둔 것이 있는지 확인한다.
만약 있다면 모델 오브젝트를 새로 만드는 대신 세션에 저장된 것을 가져와 사용

WebDataBinder에 등록된 프로퍼티 에디터, 컨버전 서비스

WebBindingInitializer나 @InitBinder 메소드를 통해서 등록된 변환 기능 오브젝트를 이용HTTP 요청 파라미터를 모델의 프로퍼티에 맞도록 변환해서 넣어줌.
타입 변환에 실패하면 BindingResult 오브젝트에 필드 에러가 등록. 필드마커나 필드 디폴트가 발견되면 그에 따라 지정된 필드의 값이 설정되기도 한다.
새로 만들어진 모델 오브젝트에 바인딩할 HTTP 파라미터가 없다면 이 과정은 생략

WebDataBinder에 등록된 검증기

모델 파라미터에 @Vaild가 지정되어 있다면 WebBindingInitializer나 @InitBinder 메소드를 통해 등록된 검증기로 모델을 검증. 검증 결과는 BindingResult 오브젝트에 등록.
WebDataBinder에 등록된 검증기가 없거나 @Vaild 애노테이션이 없다면 이 과정은 생략

ModelAndView의 모델 맵

모델 오브젝트컨트롤러 메소드가 실행되기 전에 임시 모델 맵에 저장된다. 이렇게 저장된 모델 오브젝트는 컨트롤러 메소드의 실행을 마친 뒤에 추가로 등록된 모델 오브젝트와 함께 ModelAndView 모델 맵에 담겨 DispatcherServlet으로 전달

컨트롤러 메소드와 BindingResult 파라미터

HTTP 요청을 담은 모델 오브젝트가 @ModelAttribute 파라미터로 전달되면서 컨트롤러 메소드가 실행됨. 메소드의 모델 파라미터 다음에 BindingResult 타입 파라미터가 있다면 바인딩과 검증 작업의 결과가 담긴 BindingResult 오브젝트가 제공
BindingResult는 ModelAndView의 모델 맵에도 자동으로 추가된다.

바인딩 작업은 모델에 국한해서 설명한 것이므로, 다른 종류의 파라미터에 대한 바인딩 작업이나 요청 파라미터 대신 요청 메세지 본문 자체를 변환하는 작업은 별도로 생각하기

◼ 컨트롤러 메소드로부터 뷰까지

업로드중..

ModelAndView의 모델 맵

컨트롤러 메소드의 실행을 마치고 최종적으로 DispatcherServlet이 전달받는 결과
메소드 안에서 직접 또는 간접적으로 생성된 @ModelAttribute 오브젝트가 ModelAndView의 모델 맵에 담겨 있음.

WebDataBinder에 기본적으로 등록된 MessageCodeResolver

WebDataBinder에 등록되어 있는 MesageCodeResolver는 바인딩 작업 또는 검증 작업에서 등록된 에러 코드를 확장해서 메세지 코드 후보 목록을 만들어준다.
메세지 코드 후보를 만드는 로직은 WebDataBinder에 디폴트로 등록된 것을 사용하면 됨.

빈으로 등록된 MessageSource와 LocaleResolver

LocaleResolver에 의해 결정된 지역정보MessageCodeResolver가 생성한 메세지 코드 후보 목록을 이용MessageSource가 뷰에 출력할 최종 에러 메세지를 결정
MessageSource는 기본적으로 messages.properties 파일을 이용하는 ResourceBundleMessageSource를 등록해서 사용

@SessionAttribute 세션 저장 대상 모델 이름

모델 맵에 담긴 모델 중에서 @SessionAttribute로 지정한 이름과 일치하는 것이 있다면 HTTP 세션에 저장됨. 세선에 저장된 모델 오브젝트는 다음 요청에서 활용하면 된다.

뷰의 EL과 스프링 태그 또는 매크로

뷰에 의해 최종 콘텐트가 생성될 때 모델 맵으로 전달된 모델 오브젝트뷰의 표현식 언어(EL)을 통해 참조되어 콘텐트에 포함됨. MessageSource에 의해 결정된 에러 메세지JSP라면 스프링 전용 태그나, 템플릿 엔진이라면 스프링 매크로를 통해 출력 가능

(2) 편에서 계속...

profile
모르는 것 정리하기

0개의 댓글