Baeldung - Spring MVC and the @ModelAttribute Annotation

sycho·2024년 4월 1일
0

Baeldung - Spring MVC

목록 보기
5/7

Baeldung의 이 글을 정리 및 추가 정보를 넣은 글입니다.

1. Overview

  • 이 글에선 Spring MVC의 핵심적인 annotation 중 하나, @ModelAttribute에 대해 알아보겠다.

  • javadoc, documentation

  • 이름에서 유추가능하듯이 MVC의 model과 관련된 개념이다. 특정 method의 parameter, 혹은 return value에 대한 model attribute를 만드는데 사용된다. 그러면 view에서 추후 해당 model attribute를 활용해서 전시를 하는 것이 가능하겠죠.

  • Baeludng에서는 어떤 직장의 직원이 사이트에 특정 양식의 서류를 제출했을 때 나타나는 변화를 예시로 이 개념을 설명했다.

2. @ModelAttribute in Depth

시작하기 전에 한가지 언급하자면, Spring의 Model instance는 session scope이다. request때마다 하나씩 새로 생성된다는 점에 유의. 그리고 유효기간도 그 request의 처리기간 동안에만 유효하다.

2.1 At the Method Level

  • 일단 위에 나온 내용으로 유추 가능한건, method나 method의 parameter에서 해당 annotation을 사용할 수 있다는 것이다. 전자부터 알아보자.

  • 밑의 코드는 msg라는 model attribute를 만든다.

@ModelAttribute
public void addAttributes(Model model) {
    model.addAttribute("msg", "Welcome to the Netherlands!");
}
  • 자, 만약 이게 일반 @Controller annotated class에 들어있는 경우, 해당 controller이 처리하는 모든 request에 관한 controller method (@RequestMapping이 annotate된 method) 실행 전에 위의 method를 실행하게 된다.

  • 즉 method에 annotate하는 경우는 controller의 모든 request에 대해 통일된 model attribute를 넣고, 그것을 view가 렌더링에 활용할 수 있도록 하기 위해서다.

  • 그러면 이왕 그런거 모든 controller들에 대해 request를 받았을 때 위와 같은 특정 model attribute를 넣도록 편하게 선언하는 법이 없나?라고 생각할 수 있다. 그리고 가능하다. @ControllerAdvice를 annotate한 class에다가 위와 같이 @ModelAttribute가 annotate된 method를 넣으면 된다.

@ControllerAdvice는 모든 controller들이 가질 공통된 행동을 정의하는데 사용되는 annotation이다. 앞의 @ModelAttribute말고도 @InitBinder, @ExceptionHandler등의 활용이 가능하다. javadoc

참고로 위와 같은 서순을 가지는 이유는 controller method를 통한 처리 전에 model object를 형성하기에 어쩔수없이 그렇다고 한다.

2.2 As a Method Argument

  • 그러면 method argument에 사용되는 경우엔 어떨까? 대응되는 model attribute를 가지고 특정 객체를 initialize하기, 혹은 그 attribute에 해당하는 data가 model에 없는 경우 (현재 form 형식을 활용 중이므로) 자동으로 form의 attribute를 parameter의 class의 field들에 대응시키는 형태로 initialize할 때 사용이 된다.

  • 말이 어려운데 예시를 가지고 알아보도록 하자. 밑의 코드를 보도록 하자.

@RequestMapping(value = "/addEmployee", method = RequestMethod.POST)
public String submit(@ModelAttribute("employee") Employee employee) {
    // Code that uses the employee object

    return "employeeView";
}
  • 만약 저 /addEmployee endpoint상의 POST request와 엮인 model에 employee라는 model attribute가 있으면 그걸 가지고 employee를 구성할 것이다. 그러나 앞에 말했듯 model은 request-scope이기 때문에 우리가 @ModelAttribute method같은걸로 미리 조작한게 아니면 그럴 확률이 없다.

  • 그러면 employee라는 model attribute가 없다는 것인데, 이 경우 POST로 온 내용물이 form이라는 것을 활용, employee의 각 field의 이름과 대응되는 form의 field 이름들이 있는지를 확인해 있는경우 그 값들을 employee의 field에다가 집어넣어서 initialize를 한다.

좀 더(...) 구체적인 원리 분석

  • 앞에서 employee에 해당하는 model attribute가 없을 때 POST로 온 내용물을 employee의 field에 적절히 대응시키는 것을 data binding이라고 표현한다.

  • 이 data binding 하는 방식은 사실 여러가지가 있고 우선순위도 정해져있는데, 다음과 같다.

  1. Accessed from the model where it could have been added by a @ModelAttribute method.
  2. Accessed from the HTTP session if the model attribute was listed in the class-level @SessionAttributes annotation.
  3. Obtained through a Converter if the model attribute name matches the name of a request value such as a path variable or a request parameter (example follows).
  4. Instantiated through a default constructor.
  5. Instantiated through a “primary constructor” with arguments that match to Servlet request parameters. Argument names are determined through runtime-retained parameter names in the bytecode.
  • 앞의 예제에서 활용한 방식은 5번에 해당한다. 위에가 제일 우선순위가 높은것이길래 제일 우선순위가 낮다는 것을 알 수 있다.

  • 이미 존재하는 경우에 대응시킨다고 설명한것은 위의 1번에 해당한다. 2~3번의 경우 이래저래 추가 코드를 작성하는 것이고 굳이 지금 알 필요는 없는 내용이며 (알고 싶으면 documentation을 참고하면 잘 나와있다.) 4~5번이 좀 중요한데, 보시다시피 default constructor이 있으면 그걸 우선으로 사용한다. 즉 만일 Employee에 해당하는 default constructor이 존재시 위에서 언급한 data binding이 일어나지 않는다는 점... 주의바란다.

form?

  • 그리고 아까부터 form이라는 내용이 계속 나왔는데 이게 정확히 뭔지 모르는 사람들이 있을 수도 있다. 프론트엔드(정확히는 HTML)랑 관련된 내용인데, 이 글을 참고해보도록 하자.

  • 저게 무슨 용도로 쓰이는지는 위 글을 보면 되고, 동작 부문에서 중요한 것은 form<input> tag다. 이 form을 기반으로 client가 GET요청을 한 경우 input tag들이 URL의 query parameter에 달린 형태로 전달되며, POST 요청을 한 경우 그냥 저 form 내용물이 request body에 존재하게 된다. 뭐 GET의 경우에는 DB 탐색을 해서 관련 내용물을 전달해줘야 할거고, POST의 경우에는 받은 내용물을 DB에 저장하든가 해야 한다.

  • 이 때 후자의 경우가 코드 예시에 해당한다. DB에 저장하려면 관련해서 적절한 Java object를 만들어야 하고 그 과정에서 앞의 5번에 해당하는 data binding이 필요하다. 이 5번이 동작하는 방식이 사실 form HTML과 관련이 있는데, 채워넣는 객체의 type의 field랑 이름이 같은 form<input> tag의 name을 수색해서 그 값을 field에다가 채워넣는 것이다.

  • 더 구체적으로 설명하자면 다음과 같다.

    • 위의 form HTML파일은 POST HTTP request를 보낼 때 encode 과정을 거친다. 기본은 application/x-www-form-urlencoded로 설정되어 있는데, 그냥 field 이름과 value를 key-value pair 형태로 저장해서 전달을 한다.
    • 이때 encode 방식이 뭔지를 또 Content-Type이라는 header을 통해 전달한다.
    • 저 request를 처리할 dispatcher servlet이 배당된 이후, 그 녀석은 본인의 controller method의 parameter에 붙은 annotation을 확인, @ModelAttribute가 있는 것을 보고 ModelAttributeMethodProcessor이라는, HandlerMethodArgumentResolver의 파생 class의 instance를 가지고 parameter을 적절히 initialize한다.
    • 이 때 관련 model attribute가 없고 받은 data의 Content-Type header에 application/x-www-form-urlencoded가 있는것을 발견하면 받은 data를 기반으로 key-value map을 형성한다음에 parameter을 구성하고, 해당 model attribute를 model에다가 넣게 된다.
  • 물론 위는 이상적인 경우고, 만약 multipart/form-data와 같이 다른 content-type의 형태로 data가 오면 또 그에 맞는 resolver을 활용해가지고 해결해야 한다.

  • 이처럼 원리는 복잡하지만 data binding은 프로그래머가 받은 data를 어떻게 parse해야 하는지에 대한 고민을 줄여서 좀 더 logic에 집중할 수 있는 환경을 제공해준다.

이 글도 참고하면 좋다. data binding과 관련해서 좋은 습관도 알려주기 때문.

3. Form Example

  • 위에까지는 이론적인 내용이며, 이를 실제로 어떻게 적용할 수 있는지 예시 코드를 한번 봐보도록 하자.

  • spring-mvc-basics/addEmployee endpoint에 form 파일을 만들어서 직원이 내용을 기입해 서버에 전달하면, 서버에서 해당 내용물을 새로운 화면에다가 그대로 보여주는 것을 구현해 볼 것이다.

3.1 The View

  • 먼저 간단한 form HTML 파일을 만들어보도록 하자.
<form:form method="POST" action="/spring-mvc-basics/addEmployee" 
  modelAttribute="employee">
    <form:label path="name">Name</form:label>
    <form:input path="name" />
    
    <form:label path="id">Id</form:label>
    <form:input path="id" />
    
    <input type="submit" value="Submit" />
</form:form>
  • 보면 일반적인 HTML form파일과 좀 다르다. Spring form tag library를 활용하기 때문.

  • 먼저 modelAttribute가 있는것을 볼 수 있는데 이는 explciit하게 form이 Employee라는 model attribute에 대응된다는 것을 알린다.

  • 그리고 form:labelform:input을 활용해서 입력하는 칸을 만들었는데, 이 때 path에 지정한 이름은 대응되는 Employee의 field 이름이랑 똑같아야 한다. 그러면 이제 자동으로 위에서 설명한 형태로 request body가 만들어져서 Employee instance를 구성하는 것이 가능해진다.

3.2 The Controller

@Controller
@ControllerAdvice
public class EmployeeController {

    private Map<Long, Employee> employeeMap = new HashMap<>();

    @RequestMapping(value = "/addEmployee", method = RequestMethod.POST)
    public String submit(
      @ModelAttribute("employee") Employee employee,
      BindingResult result, ModelMap model) {
        if (result.hasErrors()) {
            return "error";
        }
        model.addAttribute("name", employee.getName());
        model.addAttribute("id", employee.getId());

        employeeMap.put(employee.getId(), employee);

        return "employeeView";
    }

    @ModelAttribute
    public void addAttributes(Model model) {
        model.addAttribute("msg", "Welcome to the Netherlands!");
    }
}
  • BindingResult는 data binding의 결과물을 저장하는 parameter이다. @ModelAttribute에 대한 결과물이 저기에 들어가 있다. 어떻게 자동으로 들어가는거냐면 (model처럼) Spring MVC에서 context configure할때 미리 해당 request에서 Model, BindingResult의 instance가 필요하다는 것을 파악하고, 실제로 해당 controller method가 실행될 예정일 때 mapping하면서 미리 준비한다음에 그대로 전달하는 것이다.BindingResult의 scope도 당연히 request scope이다.

3.3 The Model

@XmlRootElement
public class Employee {

    private long id;
    private String name;

    public Employee(long id, String name) {
        this.id = id;
        this.name = name;
    }

    // standard getters and setters removed
}
  • 앞에 말한 이유로 default constructor이 없다.

  • @XmlRootElement는 여기선 딱히 의미를 가지지 않는걸로 보여서 생략.

3.4 Wrap Up

  • 다음 단계를 거친다.
    • 앞의 form html로 request body가 controller에게 전달
    • @ModelAttribute annotated method에 의해 model에 msg attribute 추가
    • @ModelAttribute annotated parameter employee에 request body를 기반으로 한 map을 통해 Employee instance가 형성.
    • 내용물은 employeeMap에 저장, 그리고 전시를 위해 내용물 관련 model attribute를 저장
    • 밑에 소개할 Results View에서 정의한 employeeview 이름을 return해가지고 결과물 전시를 할 View가 뭔지를 알림
    • 결과물을 해당 view를 사용해 전시.

3.5 Results View

  • 결과물 표시 view는 간단하게 이렇게 만들었다.
<h3>${msg}</h3>
Name : ${name}
ID : ${id}
profile
안 흔하고 싶은 개발자. 관심 분야 : 임베디드/컴퓨터 시스템 및 아키텍처/웹/AI

0개의 댓글