@ModelAttribute 와 커맨드 객체(Command Object)

Dope
Webdev TechBlog
Published in
8 min readJul 21, 2020

서론

@ModelAttribute 어노테이션의 기능과 커맨드 객체의 기능에 대해서 알아보겠습니다. 추가적으로 @RequestParam 어노테이션과 HttpServletRequest 에대해서도 추가적으로 다뤄보겠습니다.

순서는 HttpServletRequest > @RequestParam > 커맨드 객체 > @ModelAttribute순서로 이해해야 세 가지 개념에 대해서 확실하게 이해하실 수 있습니다.

HttpServletRequest

Extends the ServletRequest interface to provide request information for HTTP servlets.

The servlet container creates an HttpServletRequest object and passes it as an argument to the servlet's service methods (doGet, doPost, etc).

docs.oracle.com 에서는 위 와 같이 설명이 되어있습니다. HttpServletRequest 는 Http Servlet 에 대한 요청 정보를 제공하도록 ServletRequest 를 확장한 인터페이스 입니다.

HttpServletRequest 는 서블릿 컨테이너가 생성하며 서블릿의 service() 메서드의 매개변수로 보냅니다.

서블릿의 생명주기에 대해서 잠깐 다루고 가겠습니다.

서블릿의 생명주기(Life Cycle)

  1. 서블릿 컨테이너가 서블릿 인스턴스의 init() 메서드를 호출하여 초기화한다.
  • 최초 요청시 한번만 초기화되며 그 이후로는 이 과정을 생략한다.

2. 서블릿이 초기화된 다음부터 클라이언트가 요청을 처리할 수 있다. 각 요청은 별도의 스레드로 처리하고 이때 서블릿의 service() 메서드를 호출한다.

  • 이 안에서 HTTP 요청을 받고 클라이언트로 보낼 HTTP 응답을 만든다.
  • service() 는 Http Method 에 따라 doGet() 또는 doPost() 등으로 위임하여 처리한다.

3. 서블릿 컨테이너 판단에 따라 서블릿을 메모리에서 내려야할 시점에 destroy() 를 호출한다.

서블릿(servlet)은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱합니다. 그리고 그 결과를 HttpServletRequest 객체에 담아서 제공합니다. 서블릿 덕분에 개발자들은 HTTP 스펙을 편리하게 사용할 수 있습니다.

HttpServletRequest 의 핵심 기능은 HttpServlet 의 요청을 받아서 꺼내서 쓸 수 있다는 것입니다.

그럼 이제 HttpServletRequest 로 어떻게 컨트롤러에서 값을 꺼내는지 알아 보겠습니다.

위 처럼 JSP 에서 사람 정보를 입력하고 POST 로 넘기면 formData 형식 처럼 key 와 value 의 값으로 HttpServletRequest 에 담아서 컨트롤러로 전달합니다.

  • content-type : application/x-www/form-urlencoded
  • 메시지 바디에 쿼리 파라미터 형식으로 전달 username=dope&age=28

GET 의 경우 URL 뒤에 /user/ins?name=abc&phone=010–1234–5678 형식으로 전달되는데 물음표(?) 뒤의 문자열들을 쿼리스트링 또는 요청 파라미터라고 부릅니다. GET 방식의 경우에도 ?key=value&key=value 형식으로 HttpServletRequest 에 담아서 컨트롤러로 전달합니다.

컨트롤러에서 위 처럼 꺼내서 쓸 수 있습니다. 즉, 속성 하나당 변수 1개가 생성되는 1:1 방식이라고 할 수 있습니다. GET 방식이든 POST 방식이든 HttpServletRequest 를 통해서 동일하게 값을 꺼내 쓸 수 있습니다.

@RequestParam

HttpServletRequest 과 동일하게 @RequestParam 은 1:1 방식입니다. 차이점은 HttpServletRequest 대신 @RequestParam 이라는 어노테이션을 사용한다는 점입니다.

JSP 예제는 HttpServletRequest 예제와 동일한 것으로 사용하겠습니다.

HttpServletRequest 와 @RequestParam 을 이용하여 받아오는 경우 요청 파라미터가 많아지면 많아질 수록 컨트롤러 내부 코드나 매개변수가 증가하여 코드 가독성이 떨어지고, 작성되는 코드 양이 많아집니다. 이러한 문제를 해결하고자 나온것이 커맨드 객체(Command Object) 입니다.

커맨드 객체(Command Object)

커맨드 객체(Command Object) 란 HttpServletRequest 를 통해 들어온 요청 파라미터들을 setter 메서드를 이용하여 객체에 정의되어있는 속성에 바인딩이 되는 객체를 의미합니다.

커맨드 객체는 보통 DTO 를 의미하며, HttpServletRequest 로 받아오는 요청 파라미터의 key 값과 동일한 이름속성들과 setter 메서드를 가지고 있어야 합니다.

어떻게 자동으로 바인딩을 시켜주냐 하면, 스프링이 내부적으로 HttpServletRequest 와 커맨드 객체의 setter 메서드를 이용하여 알아서 바인딩 시켜줍니다. 마치 객체를 JSON 형식으로 변환하기 위해 Jackon2ObjectMapperBuilder 가 autoDetectGettersSetters() 메서드를 이용하는 것과 비슷하다고 생각하시면 됩니다.

HttpServletRequest 나 @RequestParam 을 사용하는 것보다 훨씬 코드 양도 줄고, 가독성도 좋아지고 간편해졌습니다.

위에서 user 파라미터를 model 에 담는 것을 볼 수 있습니다. 이 코드 또한 @ModelAttribute 어노테이션을 사용하여 제거할 수 있습니다.

@ModelAttribute

@ModelAttribute 의 사용 위치에따라 기능이 달라집니다. 크게 메서드명 위에 사용되는 경우와 파라미터 옆에 사용되는 경우로 나뉩니다.

@ModelAttribute 의 기능 중 하나를 먼저 말하자면, 커맨드 객체와 같이 요청 파라미터들을 객체 프로퍼티에 바인딩 시켜준다는 것입니다. 하지만 @ModelAttribute 를 생략해도 커맨드 객체를 이용해서 바인딩이 되는데, @RequestParam 또한 생략해도 사실상 바인딩이 가능합니다.

그 이유는 스프링이 내부적으로 String 이나 int 등은 @RequestParam 으로 보고, 그 외의 복잡한 객체들은 @ModelAttribute 가 생략됬다고 간주하기 때문입니다. 하지만 그렇다고 무조건 생략하는 것은 위험합니다. 스프링은 간단한 숫자나 문자로 전달된 요청 파라미터를 제법 복잡한 객체로 변환할 수도 있기 때문입니다.

이제 나머지 기능에 대해서 알아보겠습니다.

파라미터 객체 옆에 @ModelAttribute 사용하는 경우

@ModelAttribute 의 역할 중 하나는 model 에 객체를 담아준다는 것입니다. 파라미터 객체 옆에 @ModelAttribute 를 사용했을 때 얻는 또 다른 이점은 @ModelAttribute 가 붙은 파라미터를 처리할 때는 @RequestParam 과 달리 검증(Validation) 작업을 내부적으로 진행합니다.

@RequestParam 의 경우 스프링의 기본 타입 변환 기능을 이용해서 요청 파라미터 값을 메서드 파라미터 타입으로 변환하는데, 만약 숫자 타입의 파라미터라면 문자열 타입으로 들어온 요청 파라미터의 타입 변환을 시도하고 실패하면 Http 400 Bad Request 응답이 클라이언트로 가게 됩니다.

이 경우, 친절하게 메시지를 보여주고 싶으면 org.springframework.beans.TypeMismatchException 예외를 처리하는 예외 리졸버를 추가해주면 됩니다.

하지만 @ModelAttribute 의 경우 내부적으로 검증(Validation) 작업을 진행하기 때문에 setter 메서드를 이용하여 값을 바인딩하려고 시도하다가 예외를 만나지만 작업이 중단되면서 Http 400 Bad Request 가 발생하지는 않습니다. 타입 변환에 실패해도 작업은 계속되며 BindingException 타입의 객체에 담겨서 컨트롤러로 전달됩니다.

보통 등록이나, 수정을 처리하는 핸들러 메서드의 경우 다양한 검증을 실시해야 하고, 사용자의 입력 값에 오류가 있을 때에는 이에 대한 처리를 컨트롤러에게 맡겨야 합니다. 따라서 @ModelAttribute 를 통해서 폼의 정보를 전달 받는 경우 Errors 객체나 BindingResult 객체를 @ModelAttribute 가 붙은 파라미터 바로 뒤에 선언해서 검증 처리를 실시합니다.

Errors 나 BindingResult 는 자신의 바로 앞에 있는 파라미터 검증에서 발생한 오류들만 전달해주기 때문에 @Valid 나 @Validated, @ModelAttribute 가 붙은 파라미터 바로 뒤에 선언되어야 합니다.

메서드 위에 @ModelAttribute 가 사용되는 경우

컨트롤러에서 메서드 위에 @ModelAttribute 가 사용되는 경우는, 해당 컨트롤러 내의 어떠한 핸들러 메서드들보다 먼저 동작하게 됩니다.

따라서 여러 핸들러 메서드에서 공통으로 쓰이며, View 단에서도 꺼내 쓸 일이 있는 것들은 이런식으로 처리해서 사용하기도 합니다.

결론

HttpServletRequest 부터 @ModelAttribute 까지 알아봤습니다. 해당 기능들을 잘 숙지하여 적절한 상황에 알맞게 사용하는 것이 좋습니다.

References.

  • 토비의 스프링 3

--

--

Dope
Webdev TechBlog

Developer who is trying to become a clean coder.