[toby의스프링] 13장 - 스프링 @MVC #1
13장에서는 12장 내용보다 한 발 더 나아가, 실무에서 실제 빈번하게 쓰이는 @애노테이션 MVC 기법을 상세하게 다루고 있다.
개인적으로 매우 재미있게 읽은, 또 매우 유용했던 챕터다. 그동안 애노테이션을 쓰면서도 각 애노테이션의 하위 정보와 이 애노테이션이 영향을 미치는 범위를 정확하게 알지 못해 소극적으로(매번 쓰는 부분만 쓰는) 사용하곤 했는데, 그런 부분에 대해 상당부분 명쾌하게 정의가 되어 있었다.
13장은 160페이지가 넘는 매우 방대한 분량이고, 대부분의 내용이 용례에 가깝기 때문에 한 번에 정리하기가 어렵다.
따라서 내용에 따라 크게 세 부분으로 나누어 정리해보도록 하자.
12장에서 한참 떠들었던 내용인 Controller 타입 클래스는 스프링 3.0 에서는 @MVC로 대체되고 있다.
그러므로 12장의 내용을 잘 숙지하지 못했다고 해도, 좀 답답하기는 하겠지만 크게 슬퍼할 필요는 없다. 13장을 잘 익히면 된다.
이게 '혹시 걷지 못해도 상관없다, 뛸 줄 알면 된다' 따위의 말로 들릴 수도 있겠으나.. 뭐 크게 상관은 없다.
왜냐면 대부분 작업을 하다보면, 고급/최신 정보를 몰라서 삽질을 하기는 해도 과거에 쓰이던 기술을 몰라서 난감해지는 건 별로 없기 때문이다.
물론 스프링 2.x로 개발된 사이트의 유지보수가 주된 업무라면 얘기가 좀 달라지겠지만, 애초에 그런 상황에서 Controller의 쓰임을 모른 채 @MVC를 공부하고 있다면 그 자체로 말이 안 되는 일이다. 서두가 지나치게 길다. 이만 본론으로 가도록 하자.
1. URL 매핑
Controller 인터페이스와 메소드를 연결하는 방식에서는 대개 URL이 하나의 메소드와 연결된다. 따라서 매 URL 마다 Controller 인터페이스를 계속 만들어줘야 하는 부담이 있다. 물론 특정 URL 밑에 파라미터 값에 따라 하위 메소드를 연결하는 방법도 있긴 하지만, 이건 그다지 좋은 코드가 되지 못한다. 일단 알아 보기 힘들고 번거롭기 때문이이다.
하지만 @MVC에서는 메소드 단위 별로 URL을 매핑할 수 있다. 아래 예시를 보자.
... (중략) ...
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
... (중략) ...
@RequestMapping("/member")
static class MemberController {
@RequestMapping("/edit") public String edit() { return "edit.jsp"; }
@RequestMapping("/delete") public String delete() { return "delete.jsp"; }
@RequestMapping("/add", method=RequestMethod.GET) public String get() { return "get.jsp"; }
@RequestMapping("/add", method=RequestMethod.POST) public String post() { return "post.jsp"; }}
예제코드만 봐도 뭔 얘긴지 확 감이 올 것이다.
부연 설명이 필요하지는 않을 것 같지만 그래도 하나 덧붙이자면, 위와 같이 상위 메소드의 RequestMapping 정보를 하위 메소드가 상속하는 경우는 별 문제가 없지만 슈퍼클래스를 구현하는 서브클래스에서 RequestMapping를 설정할 때는 주의해야 한다. 하위 타입에서의 RequestMapping 정보는 항상 상위 타입의 정보를 대체하게 되는데, 오버라이드한 경우에는 @RequestMapping 애노테이션만 붙였을 때 상위 타입의 정보가 그대로 상속이 된다는 점이다. 이는 특수한 경우이고 추후 어떻게 변할지 모르기 때문에 추천하지 않는다고 한다.
그런데 보통 작업을 하다보면, URL 매핑 작업은 대개 정해진 네이밍에 따라 진행되곤 한다.
작업 대상이 게시판이든, 회원가입이든 대개 regist나 add 와 같은 이름은 뭔가를 추가 하는 페이지로 연결되고, modify나 edit와 같은 것들은 대개 정보를 수정하는 페이지로 연결된다는 것이다. 그리고 보통 이런 네이밍 규칙은 작업을 시작할 때 공통으로 정해두기 마련이다.
그렇다면 이런 규칙에 따라 각 메소드들을 보다 쉽게 URL로 매핑하는 방법이 있다면 쉬울 것이다. 아래 코드를 보자.
<추상클래스>
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(K id){}
@RequestMapping("/list") public List<T> list(){}
}<개별 컨트롤러>
@RequestMapping("/user")
public class UserController extends GenericController<User, Integer, UserService>{
public void add(User user){
// 상위 메소드 오버라이드 : 개별 메소드 로직이 들어간다.
// 별도의 설정이 없는 경우 추상클래스에서 정의된 RequestMapping 정보가 상속된다.
}
@RequestMapping("/login")
public String login(String userId, String userPassword){
// 상위 메소드에서 규정되지 않은 메소드는 자유롭게 추가해서 쓸 수 있다.
// 당연히 별도의 RequestMapping 이 필요하다.
}
}
쉽다. 별다른 설명이 필요없을 것 같다.
2. 메소드 파라미터
@MVC에서는 매핑된 URL의 request 파라미터나, Session 정보와 같은 다양한 정보를 메소드에 넘겨 줄 수 있다.
이때 넘겨 줄 수 있는 메소드 파라미터의 종류는 다음과 같다.
- HttpServletRequest, HttpServletResponse : 말 안해도 뻔하다. 예전에 작업한 소스를 가져다 쓰고 싶을 때 크게 수정하지 않고 사용할 수 있을 것이다.
- HttpSession : 세션이다. 역시 뻔하다.
- WebRequest, NativeWebRequest : 서블릿에 종속적이지 않은 오브젝트라고 하는데, 일단은 이런 종류가 있다는 것만 알아두고 넘어 가자.
- Locale : 지역정보를 받아 온다. 책에는 DispatcherServlet의 Locale 오브젝트를 상속한다고 적혀 있는데, 그 대상이 서버인지 클라이언트인지는 확인이 필요하다.
- InputStream, Reader
- OutpitStream, Writer
- @PathVariable : 다중패스 변수를 사용할 수 있다.
다중패스변수
@RequestMapping("/member/{membercode}/order/{orderid}") // /member/kunner/order/1/ 과 같은 URL이 매핑될 것이다.
public String lookup(
@PathVariable("membercode") String code,
@PathVariable("orderid") int orderid){
//메소드 로직
}
- @RequestParam : HTTP의 Request 파라미터를 메소드 파라미터에 넘겨준다. 설정된 @RequestParam는 필수 정보이므로, 반드시 HTTP 요청으로 넘어와야 한다. 만약 생략 가능한 파라미터라면 @RequestParam의 두번째, 세번째 인수를 생략하면 안 된다.
HTTP 파라미터 전달
public String view(@RequestParam("id") int id, @RequestParam("name") String name){}
public String view(@RequestParam(value="id", required=false, defaultValue=-1) int id){}
public String view(@RequestParam Map<String, String> params{}
- @CookieValue : 쿠키 값을 메소드 파라미터에 넘겨 준다. 쿠키값이 생략 가능할 때는 역시 두번째, 세번째 인수를 넣어 줘야 한다.
쿠키 정보 전달
public String check(@CookieValue("auth") String auth){}
public String check(@CookieValue(value="auth",required=false,defaultValue="NONE") String auth){}
- Map, Model, ModepMap : 말 그대로 해당 콜렉션들을 넘겨 준다.
- @ModelAttribute : HTTP 요청을 통해 넘어 온 값들을 자동으로 모델에 추가해 View에 넘길 때 쓴다. 개별 파라미터를 각각 정의해야 하는 @RequestParam에 비해 매우 유용하고 강력하다.
HTTP 요청 파라미터를 자동으로 모델로 생성해 전달
public class UserSearch{ //search 에서 연결될 모델
int id;
String name;
int level;
String email;
// 수정자, 접근자 생략
}
public String search(@ModelAttribute UserSearch userSearch){
List<User> list = userService.search(userSearch);
model.addAttribute("userlist",list);
}
public class User{ //add에서 연결될 모델
int id;
String name;
int level;
String email;
// 수정자, 접근자 생략
}
@RequestMapping("/user/add", mothod=RequestMethod.POST)public String add(@ModelAttribute User user){
//폼으로 넘어 온 정보를 자동으로 모델과 매핑한다.
userService.add(user);
//만약 user 말고 다른 이름으로 모델을 만들고 싶다면 @ModelAttribute("원하는이름") 으로 설정하면 된다.
}
- Errors, BindingResult : 파라미터 검증 오류 시 전달받을 리턴값. @ModelAttribute를 쓸 때 반드시 함께 사용해야 한다. 자세한 내용은 검증 부분에서 다시 다룬다.
- @RequestBody : XML이나 JSON 기반의 메시지를 사용하는 경우 Body의 값을 그대로 전달.
- @Value : 빈의 값 주입에서 사용하던 @Value 애노테이션과 동일.
- @Valid : JSR-303의 빈 검증기를 사용하도록 하는 지시자. 자세한 내용은 검증 부분에서 다시 다룬다.
3. 리턴 타입
@Controller의 리턴타입은 일부 예외가 있지만 결국 ModelAndView 라고 봐도 무방하다. 메소드의 다양한 정보가 Model로 취합되고 View로 리턴된다는 것이다.
다음 정보는 모델에 자동으로 추가된다.
- @ModelAttribute 로 넘어 온 메소드 파라미터의 모델
- Map, Model, ModelMap 파라미터
- @ModelAttribute 메소드 : 모델에만 붙이는 게 아니라 메소드에도 붙일 수 있다. 어떤 메소드가 실행될 때 같이 실행되어야 하는 메소드가 있는 경우 해당 메소드에 @ModelAttribute 애노테이션을 부여하면 함께 실행되어 모델로 추가된다. 회원가입 화면에서 주소 지역 필드를 DB에서 조회해 폼에 출력해야 할 때, 유용하게 사용할 수 있다.
- BindingResult : 검증 결과를 모델로 담아 리턴한다. org.springframework.validation.BindingResult.모델이름 으로 리턴된다.
그 외 리턴 타입은 다음과 같다.
- ModelAndView : 전통적인 스프링 MVC에서처럼 ModelAndView를 리턴한다. 하지만 @Controller에서는 ModelAndView를 직접 리턴하는 방법을 잘 쓰지 않는다.
- String : 모델은 파라미터로 맵을 가져와 넣고, 리턴값은 뷰 이름을 스트링으로 선언하는 방식이다. @Controller에서 가장 흔하게 사용되는 방법이다. 촌스럽게 ModelAndView 를 왜 리턴하지 않느냐고 당황해 하지 말자.
@RequestMapping("/user/search")
public String hello(@RequestParam String name, Model model){
model.addAttribute("name",name); // 모델에 추가
return "hello"; // View 이름 리턴
}
- void : RequestToViewNAmeResolver 전략을 통해 메소드 이름이 곧 리턴할 view의 이름이 된다. 메소드 이름과 view의 이름을 통일할 수 있는 경우 string을 리턴할 것도 없이 바로 void를 써도 된다. 역시 리턴값이 없다고 당황하지 말자.
- 모델 오브젝트 : 위의 void에서처럼 뷰 이름은 메소드 이름과 같고, 모델에 추가해야 할 오브젝트가 하나 뿐이라면(즉, 전달할 파라미터가 한개라면) 바로 모델 자체를 리턴할 수 있다. 모델을 리턴하는 경우에도 실제로는 메소드 이름과 같은 뷰가 리턴된다.
@RequestMapping("/view")
public String hello(@RequestParam int id){
return userService.getUser(id); // 모델에 연결되는 파라미터 값이 하나일 때
}
- Map/Model/ModelMap : 각 콜렉션을 모델로 만들어서 리턴하는 경우에도 모델 오브젝트를 리턴하는 것과 같은 효과를 낸다. 단, 맵 타입의 단일 오브젝트 리턴은 절대 해서는 안 된다.
Map 오브젝트 리턴의 잘못된 사용
@RequestMapping("/view")
public String hello(@RequestParam int id){
Map userMap = userService.getUserMap(id);
return userMap; // 이렇게 맵을 리턴하면 맵을 다시 모델로 감싸서 리턴하기 때문에 안 된다.
}
Map 오브젝트 리턴의 올바른 사용
@RequestMapping("/view")
public void hello(@RequestParam int id, Model model){
model.addAttribute("userMap", userService.getUserMap(id)); // 아예 모델로 싸서 리턴하면 된다.
}
- View : XML 뷰어와 같이 별도의 뷰 오브젝트로 넘기고 싶을 때 리턴타입을 View로 설정한다.
- @ResponseBody : 메소드의 실행 결과가 바로 HTTP Response로 출력된다.
@RequestMapping("/view")
@ResponseBody
public String hello(){
return "<html>블라블라블라</html>"; // 원래 String 리턴타입은 뷰 이름을 말하지만, @ResponseBody 애노테이션 때문에 바디에 출력되는 스트림으로 인식된다.
}
4. 세션을 이용한 상태 저장
아다시피 웹은 비동기식 프로그램이라, 상태를 저장하지 않는다.
좀 더 쉽게 말하면 웹에서는 클라이언트가 서버와 완전히 동기화된 채 작업하는게 아니라는 뜻이다. 그 말이 그 말인가? -ㅅ-;
부연하면 - 웹 환경에서는 클라이언트가 서버에 정보를 요청하면, 서버는 해당 요청에 따른 작업결과를 클라이언트에게 전달한다. 이렇게 요청 - 전달의 순간에만 서버와 클라이언트가 연결되어 있고, 그 후에는 연결이 끊긴다. 비동기식 프로그램이란 바로 이런 뜻이다. 뭐 대단한 얘기는 아니다. 그냥 그렇구나, 하고 끄덕이면 된다.
아무튼 누구나 다 아는 이런 얘기를 하려는게 아니고..
이렇게 상태가 유지되지 않는 웹프로그램의 특성 상 여러 화면이 하나의 결과를 만드는 - 즉, 위저드 방식으로 제작되어 폼 페이지가 여러 번 나오고 Submit은 최종적으로 한번만 해야 되는 경우 처리하기가 매우 곤란하다. 꼼수를 부려 처리하는 여러가지 방법이 있긴 하지만, 그건 깔끔하지 못하다.
이런 경우 세션을 이용해 상태를 저장하면 쉽게 처리할 수 있다.
세션을 이용한 상태 저장
@Controller
@SessionAttributes("user") // 메소드에서 다룰 세션 정보를 지정한다. 복수로 지정될 수 있다.
static class UserController {
@RequestMapping(value="/user/edit", method=RequestMethod.GET)
public User form(@RequestParam int id) {
return new User(1, "Spring", "mail@spring.com"); // 특별한 지시가 없어도 리턴된 모델은 세션으로 저장된다.
}
@RequestMapping(value="/user/edit", method=RequestMethod.POST)
public void submit(@ModelAttribute User user, SessionStatus sessionStatus) {
sessionStatus.setComplete(); // 이 메소드에서 사용된 세션 정보를 비운다.
}
}p.1173을 보면, 세션을 이용한 폼 모델의 저장/복구 과정을 이해하기 쉽게 도식으로 설명해 놓았다.
뭐 사실 그림으로 보면서까지 이해를 해야 할 내용은 아니지만, 각 작업의 시점을 명확하게 아는 것은 중요하다.
여기까지가 13장의 1/3 정도 분량이다.
나머지는 #2, #3으로 나누어 정리할 예정이다.