Spring MVC/01.Spring @MVC

02. Controller

  • -
반응형

Controller

이번 포스트에서는 Controller 클래스를 만드는 방법과 Controller 클래스에 request handler method 작성법에 대해 알아보자.

 

Controller와 request handler method

Controller는 클라이언트의 요청인 HttpServletRequest를 처리하는 클래스Handler라고도 불린다. 스프링에서는 Controller를 구현하기 위해 @Controller라는 스테레오 타입 애너테이션을 사용한다.

@Controller

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {...}

@Controller는 클래스 레벨에서 사용할 수 있는 애너테이션으로 내부에는 일반 메서드는 물론 @RequestMapping을 이용하는 다수의 request handler method들이 작성될 수 있다.

 

request handler method

사용자가 어떤 요청(URL)을 서버로 날리면 DispatcherServlet(이하 D.S)은 HandlerMapping을 통해 얻어낸 HandlerAdapter에게  URL을 넘겨주고 HandlerAdapter가 URL에 의거해 특정 Controller의 @RequestMapping가 선언된 메서드를 호출한다.

이처럼 @RequestMapping이 선언된 메서드들을 request handler method라고 한다.

request handler method에서는 전형적인 Servlet의 역할(Model 연결, 자료 저장, View 연결)이 수행된다.

 

handler method의 작성

 

@RequestMapping

@RequestMapping request handling method에 선언하는 애너테이션이다. 

@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequestMapping {
  @AliasFor("path")
  String[] value() default {};
  
  @AliasFor("value")
  String[] path() default {};
  
  RequestMethod[] method() default {};
  
  String [] params() default {};
}

 

@RequestMapping에서 가장 중요한 속성은 path인데 value와 alias로 연동되어있다. path에는 사용자가 호출하는 요청의 경로 즉 URL이 등록된다. 

@RequestMapping은 class level과 method level로 사용할 수 있다. 클래스에 선언된 @RequestMapping의 path는 클래스에 속한 모든 handler method의 path 앞에 추가된다. 일반적으로 Controller 클래스를 구성할 때 업무 도메인에 따라 공통의 URL을 구성하게 되는데 클래스 레벨의 @RequestMapping을 사용하면 아주 손쉽게 URL을 구성할 수 있다.

package com.eshome.mvc.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.eshome.mvc.model.service.HelloService;

@Controller
@RequestMapping("/user")
public class ClassLevelMappingController {

    @Autowired
    HelloService service;

    @RequestMapping
    public String showUser(Model model) {
        String echo = service.echo("사용자 정보");
        model.addAttribute("message", echo);
        return "index";
    }


    @RequestMapping("/add")
    public String addUser(Model model) {
        String echo = service.echo("사용자 추가");
        model.addAttribute("message", echo);
        return "index";
    }
}

 위와 같은 경우 showUser의 path는 /user이고 addUser의 path는 /user/add가 된다.

 

리턴

handler method는 일반적으로 String을 반환하는데 이 값은 logical view name으로 D.S을 거쳐 ViewResolver(예: ThymeleafViewResolver)가 처리해서 실제 View 페이지와 연결시켜준다. 

 

리턴 값으로 view name만 넘긴 경우 내부적으로 forward 기법을 이용해서 template page를 호출한다. forward를 통해서 이동하는 페이지에 정보를 전달할 때는 Model 객체를 이용한다. Model에 저장된 객체는 request scope에 저장된다.

@RequestMapping("/")
public String welcome(Model model) {
    model.addAttribute("message", service.sayHello());
    return "index";
}

 

만약 redirect 형식으로 다른 자원을 호출하려는 경우는 redirect:를 추가해준다. 만약 path가 /로 시작하는 경우 절대경로를 의미하는데 이때 root는 context path 이다. 이때 전달된 model의 속성은 redirect된 path의 요청 파라미터가 된다.

@RequestMapping("/redir")
public String redir(Model model) {
    model.addAttribute("message","what");
    return "redirect:/";                    // /?message=what
}

 

return 타입으로 void를 사용할 수도 있는데 이 경우는 request path 와 동일한 이름의  page를 사용하는 경우이다.

예를 들어 다음 코드는 결과적으로 hello를 리턴하는 것과 동일한 효과가 있다.

@RequestMapping(value = "/hello")

public void helloMVC(Model model) {
  model.addAttribute("message", service.sayHello());
}

 

이 외에도 ModelAndView를 반환하는 등 여러 형태가 사용될 수 있는데 가장 권장되는 형태String으로 명시적인 View 이름을 반환하는 것이다.

 

호출 방식과 매핑

handler method는 path 뿐만 아니라 GET/POST등 어떤 방식으로 호출 되느냐에 따라서도 매핑이 결정된다. 

일반적으로 path만 지정한 경우모든 mathod에 다 매핑된다.

@RequestMapping("/anymethod")
public String onlyUrl(Model model) {
    String attr = service.echo("get 이나 post를 가리지 않는다.");
    model.addAttribute("message", attr);
    return "index";
}

 

특정 method에만 매핑되도록 handler method를 작성하려면 method 속성을 사용한다. 예를 들어 다음 요청은 POST 방식으로 호출 할때만 매핑된다.

@RequestMapping(value = "/onlypost", method = RequestMethod.POST)
public String onlyPost(Model model) {
    String attr = service.echo("post 요청에만 응답함");
    model.addAttribute("message", attr);
    return "index";
}

 

만약 onlypost를 get 방식으로 요청한다면 아래와 같이 405 Method Not Allowed 오류를 만나게 된다.

 

login이나 join 처럼 어떤 동작을 위한 form을 먼저 보여주고 실제 동작으로 연결하는 경우가 많은데 이때 path를 동일하게 하고 method를 다르게 작성하는 경우에 필요한 기능이다.

method에 대한 설정을 좀 더 간단하게 처리하기 위해 @GetMapping, @PostMapping등을 사용할 수도 있다. (사용법은 method 속성이 없는것 만 빼면 @RequestMapping과 동일하다.)

@GetMapping == @RequestMapping(method=RequestMethod.GET)
@PostMapping == @RequestMapping(method=RequestMethod.POST)

 

다음은 @GetMapping과 @PostMapping을 이용해서 login을 처리하는 예이다.

@GetMapping("/login")
public String loginForm(Model model) {
    // 사용자에게 login form 제공
    return "login";
}
    
@PostMapping("/login")
public String doLogin(Model model) {
    // 실제 로그인 처리
    return "main";
}

 

요청 파라미터와 매핑

path와 method외에도 요청 파라미터를 기준으로 매핑을 처리할 수 있다. 이때는 @RequestMapping의 params 속성을 이용한다.

@RequestMapping(value = "/needParam", params = {"name", "age"})
public String needParam(Model model) {
    String attr = service.echo("요청 파라미터에 name과 age가 있어야 함");
    model.addAttribute("message", attr);
    return "index";
}

위와 같은 handler method가 매핑되기 위해서는 path는 /needParam, http method는 상관 없지만 반드시 요철 파라미터에 name과 age가 포함되어야 한다. 따라서 http://localhost:8080/needParam?name=hong&age=30 형태의 요청은 제대로 매핑이 되지만 http://localhost:8080/needParam?name=hong 경우처럼 모든 파라미터가 전달되지 않은 경우는 400 BadRequest가 발생한다.

 

단지 요청 파라미터의 존재 뿐 아니라 값 까지 설정할 수도 있다.

@RequestMapping(value = "/checkValue", params = {"name=andy", "age"})
public String checkValue(Model model) {
    String attr = service.echo("name의 값은 반드시 andy, age도 필수지만 값은 무관");
    model.addAttribute("message", attr);
    return "index";
}

위의 handler method가 매핑되기 위해서는 name과 age가 반드시 요청 파라미터에 있어야 하며 name은 andy로 한정된다. 역시 이를 위배했을 경우는 400 오류가 발생한다.

 

handler method의 parameter

 

다양한 파라미터 활용 가능

handler method의 파라미터는 신기하게도 웹 프로그래밍에서 필요한 대부분의 것들을 필요에 따라 선언할 수 있다. 자바에서 웹 프로그래밍을 할 때 반드시 필요한 녀석들은 무엇일까? 아마도 대부분 HttpServletRequest, HttpServletResponse, HttpSession을 떠올릴 것이다.  여기에 Model, ModelAttribute, SessionStatus, Locale 등의 객체들이 필요에 따라 선언될 수 있다. 당연히 선언되는 순서도 상관 없다.

@GetMapping("/req")
public String reqTest(Model model, HttpServletRequest req) {
    StringBuilder sb = new StringBuilder();
    sb.append("path: ").append(req.getServletPath()).append(", ")
      .append("param: ").append(req.getParameter("name"));
    model.addAttribute("message", sb);
    //req.setAttribute("message", sb); -- model을 이용하자!!
    return "index";
}

위 handler method는 HttpServletRequest 객체를 파라미터로 선언하고 servlet path와 name으로 전달된 요청 파라미터를 출력하는 예이다.

HttpServletRequest를 통해서 많이 하던 작업 중 하나는 request scope에 정보를 저장하고 forward 된 페이지에서 EL로 접근해서 사용하던 일이었다. 스프링에서는 이 작업을 Model이 전담해서 처리한다. 또한 요청 파라미터를 처리하기 위해서는 @RequestParam이나 @ModelAttribute를 사용하기 때문에 직접적으로 HttpServletRequest를 사용할 일은 많지 않다.

 

HttpSession의 활용도 매우 빈번한데 역시나 필요하면 선언하면 된다.!!

@GetMapping("/ses")
public String sesTest(Model model, HttpSession ses) {
    StringBuilder sb = new StringBuilder();
    sb.append("session id: ").append(ses.getId()).append(", ")
      .append("유지시간: ").append(ses.getMaxInactiveInterval());
    model.addAttribute("message", sb);
    return "index";
}

 

이외 SessionStatus나 ModelAttribute는 다음에 살펴보기로 하자.

 

@RequestParam

전통적인 웹 프로그래밍에서 요청 파라미터를 활용하기 위해서는 HttpServletRequest가 제공하는 getParameter() 메서드를 이용했었다. 네트워크를 통해 오가는 파라미터는 오로지 문자열만 가능했기 때문에 사용 전 원하는 타입으로의 형 변환은 필수였다.

스프링에서는 요청 파라미터의 값을 사용하려는 경우 @RequestParam을 사용한다.

@Target(ElementType.PARAMETER)
public @interface RequestParam {
  String value() default "";
  boolean required() default true;
  String defaultValue() default ValueConstants.DEFAULT_NONE;
}

@RequestParam은 메서드의 파라미터에 선언할 수 있는 애너테이션이다. 변수의 이름은 전달된 요청 파라미터와 동일하게 하고 만약 다른 이름을 사용하려는 경우 value 속성을 이용한다.

신기한 점은 변수의 타입을 선언할 때 원하는 타입을 적어주면 자동으로 형변환 처리해준다는 점이다.!!

@RequestMapping("/add")
public String add(@RequestParam double a, @RequestParam int b, Model model) {
    String message = String.format("%3.1f와 %d의 합은 %3.1f입니다.", a, b, a + b);
    model.addAttribute("message", message);
    return "index";
}

이때 @RequestParam은 생략 가능하다.

 

만약 동일한 이름으로 전달되는 값이 여러 개일 경우배열이나 List/Set 형태로 받을 수 있다. 

@GetMapping("/multival")
public String multival(@RequestParam List<String> hobby, Model model) {
    model.addAttribute("message", hobby);
    return "index";
}

하지만 이 경우는 @RequestParam을 생략할 수 없다. 

List를 사용하면서 @RequestParam을 생략하면 기본 생성자를 찾는 과정에서 오류가 발생한다. 아마 뒤에서 다루는 @ModelAttribute가 먼저 적용되면서 발생하는 일로 생각된다. @RequestParam을 언제는 생략가능 하고 언제는 불가능한지를 기억하는 것도 좋지만 혼선이 올 수 있으니 굳이 생략하지 않는것이 좋겠다.

 

@ModelAttribute

@RequestParam만 해도 요청 파라미터 처리가 한층 쉬워지는데 @ModelAttribute는 파라미터 처리의 끝판왕이라고 볼 수 있다.

@ModelAttribute전달된 파라미터를 DTO의 속성으로 바로 연결해서 DTO 객체를 만들어 준다. 당연히 파라미터의 name과 DTO의 속성 이름은 동일해야 한다. 예를 들어 다음과 같은 User DTO를 생각해보자.

package com.eshome.mvc.model.dto;

import java.util.List;
import lombok.Data;

@Data
public class UserInfo {
    private String id;
    private String password;
    private String name;
    private int age;
    private List<String> hobbies;
}

 

그리고 전달된 파라미터를 이용해서 User 객체를 구성하고 싶다면 다음 처럼 handler method를 구성할 수 있다.

@GetMapping(value = "/dto")
public String useDto(@ModelAttribute UserInfo user, Model model) {
    model.addAttribute("message", user);
    return "index";
}

 

이제 적절한 파라미터로 호출해보면 서버 단에서는 개별 파라미터가 아닌 하나의 DTO 객체로 바로 처리가 가능하다.

 

@CookieValue

Cookie는 세션과 함께 웹에서 정보 저장/유지를 위해 자주 사용되는 방식이다. 기존에는 Cookie 값을 사용할 때 일단 HttpServletRequest에서 Cookie의 배열을 얻어온 후 원하는 이름의 Cookie가 있는지 탐색해야 했다.

하지만 @CookieValueCookie의 name을 변수 명으로 선언하면 직접 값을 할당해준다. 물론 자동 형변환은 기본이다.

@RequestMapping("/cookieMaker")
public String setCookie(Model model, HttpServletResponse res) {
    Cookie cookie1 = new Cookie("userName", "홍길동");
    cookie1.setMaxAge(60 * 5);
    res.addCookie(cookie1);

    model.addAttribute("message", "쿠키 설정 완료");
    return "index";
}

@RequestMapping("/cookieConsumer")
public String getCookie(Model model, @CookieValue String userName) {
    model.addAttribute("message", userName);
    return "index";
}
반응형

'Spring MVC > 01.Spring @MVC' 카테고리의 다른 글

06. 파라미터의 formatting  (0) 2020.07.07
05. Handler Interceptor  (1) 2020.07.03
04. 웹과 관련된 설정  (0) 2020.07.02
03. Controller에 대한 단위 테스트  (2) 2020.07.01
01. Spring MVC 개요  (2) 2020.06.29
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.