웹 어플리케이션을 개발하는 것은 다음 코드를 작성하는 것이다.
- 특정 요청 URL을 처리할 코드
- 처리 결과를 HTML과 같은 형식으로 응답하는 코드
첫 번째는 @Controller 애노테이션을 사용한 컨트롤러 클래스를 이용해서 구현한다. 컨트롤러 클래스는 요청 매핑 애노테이션을 사용해서 메서드가 처리할 요청 경로를 지정한다.
RegisterController
@Controller
public class RegisterController {
@RequestMapping("/register/step1")
public String handleStep1() {
return "register/step1";
}
}
스프링 MVC는 별도 설정이 없으면 GET과 POST 방식에 상관없이 @RequestMapping에 지정한 경로와 일치하는 요청을 처리한다.
@GetMapping 애노테이션과 @PostMapping 애노테이션은 스프링 4.3버전에 추가된 것으로 이전 버전까지는 다음 코드처럼 @RequestMapping 애노테이션의 method 속성을 사용해서 HTTP 방식을 제한했다.
public class LoginController {
@RequestMapping(value = "/member/login", method = RequestMethod.GET)
public String form() {
...
}
}
step1.jsp의 코드를 보면 약관에 동의할 경우 값이 true인 'agree'요청 파라미터의 값을 POST 방식을 전송한다.
컨트롤러 메서드에서 요청 파라미터를 사용하는 첫 번째 방법은 HttpServletRequest를 직접 이용하는 것이다.
@Controller
public class RegisterController {
...
@PostMapping("/register/step2")
public String handleStep2(HttpServletRequest request) {
String agreeParam = request.getParameter("agree");
if (agreeParam == null || !agreeParam.equals("true")) {
return "register/step1";
}
return "register/step2";
}
}
요청 파라미터에 접근하는 또 다른 방법은 @RequestParam 애노테이션을 사용하는 것이다. 요청 파라미터 개수가 몇 개 안되면 이 애노테이션을 사용해서 간단하게 요청 파라미터의 값을 구할 수 있다.
@Controller
public class RegisterController {
...
@PostMapping("/register/step2")
public String handleStep2(@RequestParam(value = "agree", defaultValue = "false") Boolean agree) {
if (!agree) {
return "register/step1";
}
return "register/step2";
}
}
agree 요청 파라미터의 값을 읽어와 agree 하라미터에 할당한다. 요청 파라미터의 값이 없으면 "false" 문자열을 값으로 사용한다.
스프링 MVC는 파라미터 타입에 맞게 String 값을 변환해준다. 위 코드는 agree 요청 파라미터의 값을 읽어와 Boolean 타입으로 변환해서 agree 파라미터에 전달한다.
RegisterController 클래스의 handleStep2() 메서드는 POST 방식만을 처리하기 때문에 웹 브라우저에 직접 주소를 입력할 때 사용되는 GET 방식 요청은 처리하지 않는다. 그러면 사용자는 405 코드를 보게 된다.
잘못된 전송 방식으로 요청이 왔을 때 에러 화면보다 알맞은 경로로 리다이렉트하는 것이 더 좋을 때가 있다. 예를 들어 에러 화면 대신 약관 동의 화면으로 이동시키는 것이다.
"redirect:경로"를 뷰 이름으로 리턴하면 리다이렉트 된다.
@Controller
public class RegisterController {
...
// /register/step2 경로를 GET 방식으로 접근하면 리다이렉트 된다.
@GetMapping("/register/step2")
public String handleStep2Get() {
return "redirect:/register/step1";
}
}
"redirect:" 뒤의 문자열이 "/"로 시작하면 웹 어플리케이션을 기준으로 이동 경로를 설정한다.
"/"로 시작하지 않으면 현재 경로를 기준으로 상대 경로를 사용한다.
request.getParamater() 방식은 요청 파라미터 개수가 많아지면 불편하다. 그래서 스프링은 요청 파라미터의 값을 커맨드(command) 객체에 담아주는 기능을 제공한다.
RegisterController
@Controller
public class RegisterController {
private MemberRegisterService memberRegisterService;
public void setMemberRegisterService(MemberRegisterService memberRegisterService) {
this.memberRegisterService= memberRegisterService;
}
...
@PostMapping("/register/step3")
public String handleStep3(RegisterRequest regReq) {
try {
memberRegisterService.regist(regReq);
return "register/step3";
} catch (DuplicateMemberException ex) {
return "register/step2";
}
}
}
ControllerConfig
@Configuration
public class ControllerConfig {
@Autowired
private MemberRegisterService memberRegSvc;
@Bean
public RegisterController registerController() {
RegisterController controller = new RegisterController();
controller.setMemberRegisterService(memberRegSvc);
return controller;
}
}
스프링 MVC는 커맨드 객체의 (첫 글자를 소문자로 바꾼) 클래스 이름과 동일한 속성 이름을 사용해서 커맨드 객체를 뷰에 전달한다.
커맨드 객체이 사용할 속성 이름을 변경하고 싶다면 커맨드 객체로 사용할 파라미터에 @ModelAttribute 애노테이션을 적용하면 된다.
@PostMapping("/register/step3")
public String handleStep3(@ModelAttribute("formData") RegisterRequest regReq) {
}
첫 화면이 단순히 호나영 문구와 회원 가입으로 이동할 수 있는 링크만 제공한다고 하자. 이것을 위해 컨트롤러를 만들면, 이 컨트롤러 코드는 요청 경로와 뷰 이름을 연결해주는 것에 불과하다. 단순 연결을 위해 특별한 로직이 없는 컨트롤러 클래스를 만드는 것은 성가시다.
WebMvcConfigurer 인터페이스의 addViewControllers() 메서드를 사용하면 이런 성가심을 없앨 수 있다.
MvcConfig
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
...
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/main").setViewName("main");
}
}
Respondent
package survey;
public class Respondent {
private int age;
private String location;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
}
AnsweredData
package survey;
import java.util.List;
public class AnsweredData {
private List<String> responses;
private Respondent res;
public List<String> getResponses() {
return responses;
}
public void setResponses(List<String> responses) {
this.responses = responses;
}
public Respondent getRes() {
return res;
}
public void setRes(Respondent res) {
this.res = res;
}
}
스프링 MVC는 커맨드 객체가 리스트 타입의 프로퍼티를 가졌거나 중첩 프로퍼티를 가진 경우에도 요청 파라미터의 값을 알맞게 커맨드 객체에 설정해주는 기능을 제공하고 있다.
- HTTP 요청 파라미터 이름이 "프로퍼티이름[인덱스]" 형식이면 List 타입 프로퍼티의 값 목록으로 처리한다.
- HTTP 요청 파라미터 이름이 "프로퍼티이름.프로퍼티이름"과 같은 형식이면 중첩 프로퍼티 값을 처리한다
SurveyController
@Controller
@RequestMapping("/survey")
public class SurveyController {
@GetMapping
public String form() {
return "survey/surveyForm";
}
@PostMapping
public String submit(@ModelAttribute("ansData") AnsweredData data) {
return "survey/submitted";
}
}
클래스의 @RequestMapping에만 경로를 지정했다. 이 경우 from() 메서드와 submit() 메서드가 처리하는 경로는 "/survey"가 된다. 즉 form() 메서드는 GET 방식의 "/survey" 요청을 처리하고 submit() 메서드는 POST 방식의 "/survey" 요청을 처리한다.
HelloController
@RequestMapping
public class HelloController {
@RequestMapping("/hello")
public String hello(Model model, @RequestParam(value = "name", required = false) String name) {
model.addAttribute("greeting", "안녕하세요, " + name);
return "hello";
}
}
뷰에 데이터를 전달하는 컨트롤러는 두 가지를 하면 된다.
- 요청 매핑 애노테이션이 적용된 메서드의 파라미터로 Model을 추가
- Model 파라미터의 addAttribute() 메서드로 뷰에서 사용할 데이터 전달
addAttribute() 메서드의 첫 번째 파라미터는 속성 이름이다. 뷰 코드는 이 이름을 사용해서 데이터에 접근한다.
Question
public class Question {
private String title;
private List<String> options;
public Question(String title, List<String> options) {
this.title = title;
this.options = options;
}
public Question(String title) {
this(title, Collections.<String>emptyList());
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public List<String> getOptions() {
return options;
}
public void setOptions(List<String> options) {
this.options = options;
}
public boolean isChoice() {
return options != null && !options.isEmpty();
}
}
개별 설문 항목 데이터를 담기 위한 클래스다.
SurveyController
@Controller
@RequestMapping("/survey")
public class SurveyController {
@GetMapping
public String form(Model model) {
List<Question> questions = createQuestions();
model.addAttribute("questions", questions);
return "survey/surveyForm";
}
private List<Question> createQuestions() {
Question q1 = new Question("당신의 역할은 무엇입니까?", Arrays.asList("서버", "프론트", "풀스택"));
Question q2 = new Question("많이 사용하는 개발도구는 무엇입니까?", Arrays.asList("이클립스", "인텔리J", "서브라임"));
Question q3 = new Question("하고 싶은 말을 적어 주세요");
return Arrays.asList(q1, q2, q3);
}
@PostMapping
public String submit(@ModelAttribute("ansData") AnsweredData data) {
return "survey/submitted";
}
}
지금까지 구현한 클래스는
ModelAndView를 사용하면 이 두 가지를 한 번에 처리할 수 있다. ModelAndView는 모델과 뷰 이름을 함께 제공한다.
@GetMapping
public ModelAndVeiw form() {
List<Question> questions = createQuestions();
ModelAndView mav = new ModelAndView();
mav.addObject("questions", questions);
mav.setViewName("survey/surveyForm");
return mav;
}