[Spring] MVC1: 요청 매핑, 커맨드 객체, 리다이렉트, 폼 태그, 모델

첫걸음 - 11

Posted by owin2828 on 2019-12-30 16:27 · 25 mins read

0. 들어가기 앞서


본 문서에서 다루는 뷰는 크게 다음과 같다.

  • step1: 약관 동의, 별다른 기능 없음
  • step2: 회원가입 정보입력, step1에 동의 해야만 접근 가능
  • step3: 회원가입 완료, 첫 화면으로 돌아가는 기능 제공
  • surveyFrom: 설문조사 항목을 표기
  • submitted: 설문조사 결과를 화면에 표기

1. 요청 매핑 어노테이션을 이용한 경로 매핑


웹 어플리케이션을 개발하는 것은 다음 코드를 작성하는 것

  1. 특정 요청 URL을 처리할 코드
  2. 처리 결과를 HTML과 같은 형식으로 응답하는 코드
    이 중 첫 번째는 @Controller 어노테이션을 이용한 컨트롤러 클래스를 이용해 구현하며, 요청 매핑 어노테이션은 다음과 같다.
    • @RequestMapping
    • @GetMapping
    • @PostMapping

이를 이용하여 간단히 화면을 띄우는 방법은 다음 예시와 같다.

    // RegisterController.java
    @Controller
    public class RegisterController {
        ...
        @RequestMapping("/register/step1")
        public String handleStep1() {
            return "register/step1";
        }
            ...
  • 요청 경로가 /register/step1인 경우 “register/step1”을 리턴하며 이는 곧 보여줄 뷰의 이름
  • step1.jsp 파일을 생성 후, 설정파일에 RegisterController를 Bean으로 등록

1-1. GET, POST의 구분: @GetMapping, @PostMapping

  • 스프링 MVC는 별도 설정이 없으면 GET과 POST 방식에 상관없이 @RequestMapping에 지정한 경로와 일치하는 요청을 처리
  • POST 방식의 요청만 처리하고 싶다면, @PostMapping 어노테이션을 사용해 제한
  • 마찬가지로, GET 방식은 @GetMapping 어노테이션을 사용해 제한
      // RegisterController.java
      @Controller
      public class RegisterController {
          ...
              // regist/step2 경로로 들어오는 요청중 POST 방식만 처리 
          @PostMapping("/register/step2")
          public String handleStep2(
              ...
              }
    
              // regist/step2 경로로 들어오는 요청중 GET 방식만 처리
          @GetMapping("/register/step2")
          public String handleStep2Get() {
              return "redirect:/register/step1";
          }
              ...
      }
    


2. 요청 파라미터 접근


step1.jsp 코드에는 다음처럼 ‘agree’ 요청 파라미터의 값을 POST 방식으로 전송하며,
폼에서는 지정한 agree 요청 파라미터의 값을 이용해 약관 동의 여부를 확인 가능

    <!-- step1.jsp -->
    <!-- 약관 동의 화면을 생성하는 코드 -->
    ...
    <form action="step2" method="post">
    <label>
        <input type="checkbox" name="agree" value="true"> 약관 동의
    </label>
    <input type="submit" value="다음 단계" />
    </form>
    ...

컨트롤 매서드에서 요청 파라미터를 사용하는 방법은 다음과 같이 2가지 방법이 존재

  1. HttpServletRequest를 직접 이용
  2. @RequestParam 어노테이션을 사용

2-1. HttpServletRequest를 이용

    // RegisterController.java 
    @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";
        }
            ...
    }
  • 컨트롤러의 처리 매서드 파라미터로 HttpServletRequest 타입을 사용
  • HttpServletRequest의 getPatameter() 매서드를 이용해 파라미터의 값을 구함

2-2. @RequestParam 어노테이션을 이용

    // RegustController.java
    @Controller
    public class RegisterController {
        ...
        @PostMapping("/register/step2")
        public String handleStep2(
                            /*
                            * agree 요청 파라미터의 값을 읽어, agreeVal에 할당 
                            * 요청 파라미터의 값이 없다면, "false" 문자열을 값으로 사용 
                            */
                @RequestParam(value = "agree", defaultValue = "false") Boolean agreeVal){
            if (!agree) {
                return "register/step1";
            }
            return "register/step2";
        }
            ...
    }
  • 요청 파라미터의 개수가 몇 개 안 되면 @RequestParam 어노테이션을 사용해 간단히 값을 구하는 것이 가능
  • @RequestParam 어노테이션의 속성은 다음이 존재
    1. value(String): HTTP 요청 파라미터의 이름을 지정
    2. required(boolean): 필수 여부 지정
    3. defaultValue(String): 값이 없을 때 사용할 문자열 지정

3. 리다이렉트 처리


step2는 step1에서 약관을 동의해야만 접근 가능한 페이지이므로, URL을 직접 입력하여 접근하는 GET 방식은 허용하지 않음
따라서 다음과 같이 URL을 직접 입력하는 경우, step1로 리다이렉트 시킴

    // RegisterController.java
    @Controller
    public class RegisterController {
        ...
            // regist/step2 경로로 들어오는 요청중 POST 방식만 처리 
        @PostMapping("/register/step2")
        public String handleStep2(
            ...
            }

            // regist/step2 경로로 들어오는 요청중 GET 방식만 처리, step1로 리다이렉트 시킴
        @GetMapping("/register/step2")
        public String handleStep2Get() {
            return "redirect:/register/step1";
        }
            ...
    }

리다이렉트할 경로를 설정하는 방법은 다음과 같이 3가지가 존재

  1. 웹 어플리케이션을 기준으로 이동 경로를 생성:
    • “redirect:” 뒤의 문자열이 “/”로 시작하는 경우
    • “redirect:/register/step1”은 웹 어플리케이션 경로와 합쳐져 “/sp5-chap11/register/step1”이 됨
  2. 현재 경로를 기준으로 상대 경로를 이용:
    • ”/”로 시작하지 않을 경우
  3. 절대 경로를 이용:
    • 완전한 URL을 사용

4. 커맨드 객체


4-1. 커맨드 객체를 이용해 요청 파라미터 사용

step2.jsp가 생성하는 폼은 다음 파라미터를 이용해 정보를 서버에 전송

  • email / name / password / confirmPassword
    이를 처리하기 위해 다음과 같은 전송 코드를 사용가능
      @PostMapping("/register/step3")
      public String handleStep3(HttpServletRequest request){
          String email = request.getParameter("email");
          String name = request.getParameter("name");
          String password = request.getParameter("password");
          String confirmPassword = request.getParameter("confirmPassword");
              ...
    

    하지만 파라미터의 개수가 훨씬 많아진다면 일일히 값을 읽어올 것인가?

  • 이런 불편함을 줄이기 위해 스프링은 요청 파라미터의 값을 커맨드 객체에 담아주는 기능을 다음과 같이 제공
      @PostMapping("/register/step3")
      public String handleStep3(RegisterRequest regReq){
          ...
      }
    
  • 다음과 같이 요청 파라미터의 값을 전달 받을 수 있는 setter 매서드를 포함하는 객체를 커맨드 객체로 사용
      // RegisterRequest.java
      public class RegisterRequest {
          ...
          public void setEmail(String email) {
              this.email = email;
          }
    
          public String getPassword() {
              return password;
          }
    
          public void setPassword(String password) {
              this.password = password;
          }
              ...
      }
    


4-2. 뷰 JSP 코드에서 커맨드 객체 사용

가입할 때 사용한 이메일 주소와 이름을 가입 완료 화면에서 커맨드 객체를 이용해 다음과 같이 표시 가능

    ...
    <body>
        <p><strong>${registerRequest.name}님</strong> 
            회원 가입을 완료했습니다.</p>
        <p><a href="<c:url value='/main'/>">[첫 화면 이동]</a></p>
    </body>
    </html>
  • ${registerRequest.name}에서 registerRequest가 커맨드 객체에 접근시 사용한 속성 이름
  • 스프링 MVC는 다음과 같이 커맨드 객체의 클래스 이름과 동일한 속성 이름을 사용해 커맨드 객체를 뷰에 전달

    JSTL c tag여기를 참조

      @PostMapping("/register/step3")
      public String handleStep3(RegisterRequest regReq){
          ...
      }
      // 위와 같은 RegisterRequest 클래스 이름에서 첫 글자만 소문자로 변경해 아래와 같이 전달
    
      <p><strong>${registerRequest.name}님</strong> 
          회원 가입을 완료했습니다.</p>
    


4-3. @ModelAttribute 어노테이션으로 커맨드 객체 속성 이름 변경

@PostMapping("/register/step3")
// 뷰 코드에서는 "fromData"라는 이름으로 커맨드 객체에 접근 가능
public String handleStep3(@ModelAttribute("formData") RegisterRequest regReq){
      ...
}
  • 커맨드 객체에 접근할 때 사용할 속성 이름을 @ModelAttribute 어노테이션을 사용해 변경

4-4. 커맨드 객체와 스프링 폼 연동

  • 회원 정보를 입력할 때 중복된 이메일을 입력하면 텅 빈 폼을 보여주는데, 비어있으므로 입력한 값을 다시 입력해야 하는 불편함이 발생
  • 다시 폼을 보여줄 때 커맨드 객체의 값을 폼에 채워주면 이런 불편함을 해소 가능
  • 스프링 MVC가 제공하는 커스텀 태그를 사용해 간단한 커맨드 객체의 값 출력 가능
  • 스프링은 <form:form> 태그와 <form:input> 태그를 제공
      <!-- step2.jsp -->
    
      <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
      ...
      <body>
          <h2>회원 정보 입력</h2>
          <form:form action="step3" modelAttribute="registerRequest">
          <p>
              <label>이메일:<br>
              <form:input path="email" />
              </label>
          </p>
          <p>
              <label>이름:<br>
              <form:input path="name" />
              </label>
          </p>
          <p>
              <label>비밀번호:<br>
              <form:password path="password" />
              </label>
          </p>
      ...
    
  • <form:form> 태그를 사용하려면 커맨드 객체가 존재해야 함
  • 따라서 다음과 같이 step2.jsp 뷰를 호출하는 컨트롤러 코드에 “registerRequest” 객체모델에 넣어야 함
      // RegisterController.java
      @Controller
      public class RegisterController {
          ...
          @PostMapping("/register/step2")
          public String handleStep2(
                  @RequestParam(value = "agree", defaultValue = "false") Boolean agree,
                  Model model) {
              if (!agree) {
                  return "register/step1";
              }
                      // 커스텀 태그 사용을 위해 커맨드 객체를 모델에 넣어줌
              model.addAttribute("registerRequest", new RegisterRequest());
              return "register/step2";
          }
              ...
      }
    


5. 컨트롤러 구현 없는 매핑


step3.jsp에 존재하는 다음과 같은 코드는 가입 완료 후 첫 화면으로 복귀하는 역할

    <p><a herf="c:url value='/main/'>">[첫 화면 이동]</a></p>

이 첫 화면이 단순히 환영 문구와 회원 가입으로 이동하는 링크를 제공한다면, 이를 위해 다음과 같은 컨트롤러를 구현해야 함

    @Controller
    public class MainController{
        @RequestMapping("/main")
        public String main(){
                return "main";
        }
    }
  • 단순히 요청 경로와 뷰를 연결하기 위해 컨트롤러를 작성하는 낭비 발생
  • WebMvcConfigurer 인터페이스의 addViewControllers() 매서드를 이용해 다음과 같이 보완 가능
      // MvcConfig.java
      @Configuration
      @EnableWebMvc
      public class MvcConfig implements WebMvcConfigurer {
          ...
          @Override
          public void addViewControllers(ViewControllerRegistry registry) {
              registry.addViewController("/main").setViewName("main");
          }
      }
    


6. 커맨드 객체: 중첩, 콜렉션 프로퍼티


스프링 MVC는 다음과 같은 경우에 요청 파라미터의 값을 알맞게 커맨드 객체에 전달

  1. 커맨드 객체가 리스트 타입의 프로퍼티를 가졌거나
  2. 중첩 프로퍼티를 가진 경우

각 경우의 규칙은 다음과 같다.

  1. HTTP 요청 파라미터 이름이 "프로퍼티이름[인덱스]" 형식이면 리스트를 처리
  2. HTTP 요청 파라미터 이름이 "프로퍼티이름.프로퍼티이름" 형식이면 중첩 프로퍼티를 처리
    // AnsweredData.java
    public class AnsweredData{
        private List<String> responses;
        private Respondent res;
        ...
    }

    // Respondent.java
    public class Respondent{
        private int age;
        private String location;
        ...
    }
  • AsnweredData 객체는 리스트 타입의 프로퍼티 responses를 가짐
  • AsnweredData 객체는 중첩 프로퍼티 res를 가짐
    <!-- surveyForm.jsp -->
    ...
    1. 당신의 역할은?<br/>
    <label><input type = "radio" name = "response[0]" value = "서버">
        서버개발자</label>
    <label><input type = "radio" name = "response[0]" value = "프론트">
        프론트개발자</label>
    <label><input type = "radio" name = "response[0]" value = "풀스택">
        풀스택개발자</label>
    </p>
    <p>
    2. 가장 많이 사용하는 도구는?<br/>
    <label><input type = "radio" name = "response[1]" value = "Eclipse">
        Eclipse</label>
    <label><input type = "radio" name = "response[1]" value = "Intellij">
        Intellij</label>
    <label><input type = "radio" name = "response[1]" value = "Sublime">
        Sublime</label>
    ...
    <label>응답자 위치:<br>
    <input type="text" name="res.location">
    </label>
    ...


7. Model을 통해 컨트롤러에서 뷰에 데이터 전달


컨트롤러는 뷰가 응답 화면을 구성하는데 필요한 데이터를 생성해서 Model을 이용하여 전달하며 다음과 같은 두 가지를 수행

  1. 요청 매핑 어노테이션이 적용된 매서드의 파라미터로 Model을 추가
  2. Model 파라미터의 addAttribute() 매서드로 뷰에서 사용할 데이터 전달

앞선 예제에서 JSP 파일에 설문 항목을 하드 코딩한 부분을 컨트롤러에서 생성해 뷰에 전달하는 방법으로 변경

    // SurveyController.java
    @Controller
    @RequestMapping("/survey")
    public class SurveyController {

        @GetMapping
            /*
            * 1. 파라미터로 Model 타입을 추가
            * 2. addAttribute() 매서드로 전달
            */
        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";
        }

    }
    <!-- surveyForm.jsp -->
    ...
    <body>
        <h2>설문조사</h2>
        <form method="post">
        <c:forEach var="q" items="${questions}" varStatus="status">
        <p>
            ${status.index + 1}. ${q.title}<br/>
            <c:if test="${q.choice}">
                <c:forEach var="option" items="${q.options}">
                <label><input type="radio" 
                            name="responses[${status.index}]" value="${option}">
                    ${option}</label>
                </c:forEach>
            </c:if>
            <c:if test="${! q.choice }">
            <input type="text" name="responses[${status.index}]">
            </c:if>
        </p>
        </c:forEach>
    ...


7-1. ModelAndView를 통한 뷰 선택과 모델 전달

  • 지금까지 구현한 컨트롤러는 다음과 같은 특징을 지님
    1. Model을 이용해 뷰에 전달할 데이터 설정
    2. 결과를 보여줄 뷰 이름을 리턴
  • ModelAndView를 사용하면 이 두 가지를 한번에 다음과 같이 처리 가능
      // SurveyController.java  
      @Controller
      @RequestMapping("/survey")
      public class SurveyController {
    
          /*
              * 기존 코드
          @GetMapping
          public String form(Model model) {
              List<Question> questions = createQuestions();
              model.addAttribute("questions", questions);
              return "survey/surveyForm";
          }
              */
    
          @GetMapping
          public ModelAndiew form() {
              List<Question> questions = createQuestions();
              mav.addObject("questions", questions);
              mav.setViewName("survey/surveyFrom");
              return mav;
          }
      }
    


8. 주요 폼 태그


스프링 MVC는 <form:form>, <form:input> 등 HTML 폼과 커맨드 객체를 연동하기 위한 JSP 태그 라이브러리를 제공

8-1. <form> 태그를 위한 커스텀 태그 : <form:form>

  • <form> 커스텀 태그는 <form> 태그를 생성할 때 사용
      <form:form>
      ...
      <input type="submit" value="가입 완료">
      </form:form>
    
  • <form:form> 태그의 method 속성action 속성의 기본값은 각각 post현재 요청 URL

    만약 요청 URL이 “/sp5-chap11/register/step2” 라면 앞선 태그는 다음의 <form> 태그를 생성 ~~~jsp

    ... </from> ~~~
  • 커맨드 객체의 이름이 기본값인 “command”가 아니라면 다음과 같이 modelAttribute 속성값으로 설정
      <form:form modelAttribute="loginCommand">
      ...
      </form>
    
  • 다음과 같이 커멘드 객체를 이용해 이전에 입력한 값을 출력 가능
      <form:from modelAttribute="loginCommand">
      ...
          <input type="text" name="id" value="${loginCoomand.id}" />
      ...
      </form:form>
    

    이때 input을 직접 사용하기 보다는 <form:input> 등의 태그를 사용하면 편리

8-2. <input> 관련 커스텀 태그: <form:input>, <form:password>, <form:hidden>

  • 스프링은 다음과 같은 커스텀 태그를 제공
    • <form:input>: text 타입 <input> 태그
    • <form:password>: passowrd 타입 <input> 태그
    • <form:hidden>: hidden 타입 <input> 태그
  • <form:input> 커스텀 태그는 다음과 같이 path 속성을 사용해 연결할 커맨드 객체의 프로퍼티를 지정
      <form:form modelAttribute="registerRequest" action=step3">
      <p>
          <label>이메일:<br/>
          <form:input path="email"/>
          </label>
      </p>
    
      <!-- 위 코드는 아래와 같이 HTML <input> 태그를 생성 -->
    
      <form id="registerRequest" action=step3" method="post">
      <p>
          <label>이메일:<br/>
          <input id="email" name="email" type="text" value=""/>
          </label>
      </p>
    


8-3. <select> 관련 커스텀 태그: <form:select>, <form:options>, <form:option>

  • 스프링은 다음과 같은 커스텀 태그를 제공
    • <form:select>: <select> 태그를 생성, <option> 태그를 생성할 때 필요한 콜렉션을 전달 받을 수 있음
    • <form:password>: 지정한 콜렉션 객체를 이용하여 <option> 태그를 생성
    • <form:hidden>: <option> 태그를 한 개 생성
  • <select> 태그는 선택 옵션을 제공할 때 주로 사용
  • <select> 태그에서 사용할 옵션 목록을 Model을 통해 전달하면 뷰에서 다음과 같이 간단하게 태그 생성 가능
      <form:form modelAttriute="login">
      <p>
          <label for="loginType">로그인 타입</label>
          <!-- loginTypes에는 모델에서 넘어온 로그인 타입 관련 array등이 저장됨 -->
          <form:select path="loginType" items="${loginTypes}" />
      </p>
      ...
      </form:form>
    


8-4. 체크박스 관련 커스텀 태그: <form:checkboxes>, <form:checkbox>

  • 배열이나 List 타입을 이용해 한 개 이상의 값을 커맨드 객체에 저장 후 HTML 폼에서는 checkbox 타입 태그 사용
  • 스프링은 다음과 같은 커스텀 태그를 제공
    • <form:checkboxes>: 커맨드 객체의 특정 프로퍼티와 관련된 checkbox 타입의 <input> 태그 목록 생성
    • <form:checkbox>: 커맨드 객체의 특정 프로퍼티와 관련된 한 개의 checkbox 타입 <input> 태그 생성
        <p>
            <!-- favoriteOsNames에 존재 하는 값들에 대한 체크박스를 생성 -->
            <label>선호 OS</label>
            <form:checkboxes items="${favoriteOsNames}" paht = "favoriteOs" />
        </p>
      


8-4. 라디오버튼 관련 커스텀 태그: <form:radiobuttons>, <form:radiobutton>

  • 여러 옵션중 한 가지를 선택해야 하는 경우 radio 타입의 <input> 태그를 사용
  • 스프링은 다음과 같은 커스텀 태그를 제공
    • <form:radiobuttons>: 커맨드 객체의 특정 프로퍼티와 관련된 radio 타입의 <input> 태그 목록 생성
    • <form:radiobutton>: 커맨드 객체의 특정 프로퍼티와 관련된 한 개의 radio 타입 <input> 태그 생성
  • 다음과 같이 items 속성에 값으로 사용할 콜렉션을 전달받고 path 속성에 커맨드 객체의 프로퍼티를 지정
      <p>
          <label>주로 사용하는 개발툴</label>
          <form:radiobuttons items="${tools}" path="tool"/>
      </p>
    


8-5. <textarea> 태그를 위한 커스텀 태그: <form:textarea>

  • 게시글 내용과 같이 여러 줄을 입력받아야 하는 경우 사용
  • 스프링은 커맨드 객체와 관련된 <textarea>태그를 생성하는 <form:textarea> 커스텀 태그를 제공
      <p>
          <label for="etc">기타</label>
          <form:textarea path="etc" cols="20" rows="3" />
      </p>