@ModelAttribute vs @RequestParam

burningminor·2023년 10월 13일

기술적 오류가 있어 본문 삭제하였습니다. (자세한 내용은 댓글에)

get api를 작성하는 데 필터링 기능으로 인해 클라이언트에서 보내주는 쿼리스트링 변수의 개수가 많았다. 그래서 한번에 깔끔하게 받고 싶었는데, 찾아보니 @ModelAttribute를 사용하라는 조언이 있어 그렇게 인자를 받았다.
그런데, ModelAttribute는 Body이므로 get 요청에서 사용을 지양해달라는 리뷰를 받았다.
swagger에서도 쿼리스트링으로 파라미터가 보내지길래 몰랐는데, @ModelAttribute는 모든 소스의 request parameter를 맵핑해주는 거였다.
너무나 기초적인 부분을 모르고 지나쳤던 게 부끄러워서, 관련된 내용을 정리해 보았다.

What is request parameter in REST?

엄밀히 말해 request parameter는 클라이언트에서 서버로 보내지는 데이터를 말한다.
크게 네 가지의 전송 형태(resource)가 있다.
1. query string
2. form-data
3. request body
4. request header
그러나, 흔하게는 좁은 의미로 form-data와 query string의 두 가지만을 뜻하는 용도로 많이 쓰이며, 이 경우 query parameter라고도 한다. 해당 문서에서 역시 좁은 의미로 사용한다.

What is Model?

Spring MVC 패턴할때 그 Model 이다. @ModelAttribute를 잘 이해하기 위해서는 Model의 개념을 알아야 한다. 여기서는 간략하게만 설명한다.
백엔드가 API 서버로 많이 사용되며 이제는 MVC 패턴이 많이 퇴색되었지만, 원래는 하나의 Spring 프로젝트가 프론트(View, 화면)와 백엔드(Controller) 의 역할을 모두 담당했다.
그리고 동적인 화면(View)를 만들기 위해, View에 일종의 갈아끼울 데이터를 Controller에서 전달해 주어야 했는데 이때 둘을 연결해주는 일종의 데이터 컨테이너가 바로 Model이다. Controller에서 Model에 데이터를 저장해 View를 호출하면, View에서 이 Model 데이터를 사용해 화면을 동적으로 구성한다.

What is @ModelAttribute

@ModelAttribute is an annotation that binds a method parameter or method return value to a named model attribute, and then exposes it to a web view.

@ModelAttribute는 한마디로 이 Model의 attribute에 접근하는 매서드이다. 위의 설명에서처럼, 매서드의 반환값이나 매서드의 파라미터의 값으로 동일한 이름을 가지는 모델 attribute의 값을 업데이트 한다.
아래 예시를 보면 이해가 쉽다.

@Controller
public class MyController {

    // 예시 1
    // 모델의 "message" attribute 가 "Hello, World!"라는 값을 가짐
    @ModelAttribute("message")
    public String addMessage() {
        return "Hello, World!";
    }
    
    // 예시 2
    // 모델의 "myModel" attribute 가 myModel라는 값을 가짐
    @GetMapping("/example")
    public String example(@ModelAttribute("myModel") MyModel myModel) {
        // 이 메서드에서는 "myModel" 모델 속성을 사용할 수 있음
        // 만약 "myModel"이 모델에 존재하지 않으면 여기에서 새로 생성됨
        return "exampleView"; // View 이름
    }
    
    // 예시 3
    // 모델의 "msg" attribute가 "Welcome to the Netherlands!"라는 값을 가짐
    @ModelAttribute
    public void addAttributes(Model model) {
        model.addAttribute("msg", "Welcome to the Netherlands!");
    }
    
}

예시 1은 매서드의 반환값을 모델 객체에 저장하는 것이다.
예시 2는 매서드의 파라미터에 사용된 건데, 추후 자세히 살펴본다.
예시 3은 독특한데, @ModelAttribute가 붙은 매서드는 @RequestMapping이 붙은 매서드보다 먼저 실행된다. 따라서 컨트롤러에서 가장 먼저 실행되므로, 해당 컨트롤러의 모든 @RequestMapping 함수가 실행되기 전에 생성되며 접근이 가능하므로 일종의 전역변수 느낌이라 할 수 있다.
어떻게 보면 예시 1은, 예시 3의 연장선이라 할 수 있다. 예시 1도 매서드 레벨에서 @ModelAttribute가 쓰였으니 가장 먼저 "message"라는 model attribute를 설정할 수 있는 것이다. 단지, 직접 .addAttribute()를 사용하느냐 아니면 자동으로 매서드의 return값을 사용하느냐의 차이인 것 같다.
정리하자면, @ModelAttribute가 매서드 레벨과 매서드 파라미터 레벨에서 모두 사용이 가능하며, 매서드 파라미터와 매서드 반환값 모두를 모델 객체에 바인딩할 수 있다.
이제, 예시 2를 조금 더 자세히 살펴보자.

@Controller
public class MyController {

    @ModelAttribute("example")
    public String addMessage() {
        return "example1";
    }
    
    @GetMapping("/example")
    public String exampleGet(@ModelAttribute("example") String example) {
        return "exampleView"; // View 이름
    }
}

위와 같은 Controller가 있을 때, 아래와 같이 호출하면 어떻게 될까?
어떤 과정을 거쳐 example 파라미터가 example2라는 값을 가지게 되는 것일까?

{도메인}/exampleGet?example=example2

1. example 파라미터를 초기화한다.
1.1. 기존 Model에 "example" 이름을 가진 attribute가 있다면, 해당 값이 초기값이 된다.
1.2. 동일한 이름의 세션 attribute가 있다면 해당 값으로 초기화한다.
1.3. 만약 없다면, 적절한 생성자를 통해 초기화한다.
2. example 파라미터에 값을 바인딩한다.
2.1. @ModelAttribute를 썼으므로, 파라미터의 이름과 동일한 이름을 가진 model attribute의 값이 바인딩된다.
결론적으로는 그냥 파라미터로 전달한 example2라는 값이 그냥 할당된 것 처럼 보이지만, 사실은 먼저 Model의 "example" attribute가 초기화되고 이후 이 attribute값이 할당된 것이다.
즉, 만약 기존에 동일한 이름의 attribute가 있었다면 request param의 값으로 덮어써지는 것이고, 없었다면 해당하는 이름을 가진 새로운 attribute가 생성되는 것이다. 그리고 이 attribute 값이 파라미터에 바인딩된다.

How @ModelAttribute bind parameter?

@ModelAttribute 는 request parameter를 아래의 우선순위에 따라 바인딩한다.
1. path variable
2. query parameter/form-data
3. request body
즉, 만약 동일한 이름의 데이터가 쿼리스트링과 body의 json에 있다면, 쿼리스트링의 값이 파라미터로 맵핑된다.

@RequstParam VS @ModelAttribute

RequestParam과 ModelAttribute의 차이를 설명할 때, RequestParam은 한번에 하나의 파라미터를 맵핑하고 ModelAttribute는 다수의 파라미터를 한번에 맵핑한다는 글이 많다. 나 역시 이렇게만 알고 있었는데 이는 50점 설명이다.

RequestParam은 오직 request parameter(form-data, query string)에서만 값을 맵핑한다. 반면 ModelAttribute는 이 두가지에, requestbody의 값 역시 맵핑한다.

@RequestParam just populates stand-alone variables (which may of course be fields in a @ModelAttribute class). These variables will be thrown away when the Controller is done, unless they have been fed into the model.

또한, @RequestParam은 모델을 조작하지 않는다. 의도 자체가 입력된 request parameter를 매서드의 파라미터에 바인딩하기 위한 것이다. 따라서 Model은 업데이트 되지 않으며, 호출한 매서드가 종료되면 @RequestParam 값 또한 사라진다.
반면, @ModelAttribute는 위에서 설명했듯이 주 목적은 모델 attribute의 값을 파라미터에 할당하는 것이다.

    @ModelAttribute("test")
    public String test(){
        return "example1";
    }

    @GetMapping("/model-attribute")
    public void modelAttributeTest(@ModelAttribute(value = "test") String test){
        System.out.println("test = " + test);
    }

    @GetMapping("/request-param")
    public void requestParamTest(@RequestParam(value = "test") String test){
        System.out.println("test = " + test);
    }

위의 예시에서, 만약 파라미터를 전달하지 않고 호출한다면,
/model-attribute 는 example1을 출력하지만,
/request-param 은 test가 전달되지 않았다고 에러를 내는 것을 확인할 수 있다.
즉, @ModelAttribute초기화 -> 바인딩 을 거치므로 값이 전달되지 않아도 초기화 값을 사용하는데, RequestMapping 은 오직 바인딩만 하므로 값이 전달되지 않자 에러가 나는 것이다.
Modelattribute의 맵핑 순위에서 둘의 우선순위가 request body보다 높고, 많은 경우 해당 어노테이션이 다수의 @RequestParam 을 하나로 받기 위한 용도(즉, request parameter를 맵핑하는 용도)로 쓰이기에 결과적으로는 단수/복수 차이밖에 없어보이지만 따지고 보면 그 사용 용도 자체가 다른 것이다.

What is @ModelAttribute's Role?

쭉 읽다보면 @ModelAttribute의 용도가 뭔지 좀 혼동이 온다.
이건 어디까지나 내 추측이지만, @ModelAttribute는 Spring MVC 패턴이 쓰이고 있을 때, request param으로 모델의 attribute들을 업데이트 하는데 사용되었을 것이다.
그러다보니, request parameter의 값을 DTO에 맵핑해, Service Layer로 넘겨서 사용하는 API 서버 형태에서는 조금 어색하게 느껴지는 것 같기도 하다. Model을 사용하지 않는데도 Model을 조작하고 있으니 말이다.

1. GET is recieve, not generate
get 매서드는 서버로부터 정보를 요청 하는 목적이다. 수동적으로 서버의 정보를 그냥 받겠다는 뜻이다. 그런데 POST, UPDATE 등 특정 행동을 요청하는 데 필요한 정보를 담은 requestBody를 보내는 것이 어색한다.
2. Proxy
http 통신에서는 클라이언트와 서버 사이에 프록시 등의 매개자가 있는 경우가 많은데, 서버에서 GET의 request body를 허용하더라도 중간 프록시가 허용하지 않는다면 문제가 발생한다.
3. Rejected Request / Ignored Request Body
몇몇 서버는 RequestBody를 무시하거나, RequestBody를 가진 GET 요청을 무시한다.

이러한 이유로, GET 요청에는 requestBody를 사용하는 것을 지양해야 한다.

참고자료

https://www.baeldung.com/spring-mvc-and-the-modelattribute-annotation

https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.html

https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-modelattrib-methods.html

https://docs.spring.io/spring-framework/reference/web/webflux/controller/ann-methods/modelattrib-method-args.html

https://docs.spring.io/spring-framework/reference/web/webflux/controller/ann-methods/modelattrib-method-args.html

https://stackoverflow.com/questions/29370581/spring-mvc-please-explain-difference-between-requestparam-and-modelattribute

https://www.baeldung.com/http-get-with-body~~
~~

profile
burning minor

2개의 댓글

comment-user-thumbnail
2024년 12월 31일

글의 목적이 잘 이해가 안됩니다. 제목을 봤을 땐 ModelAttribute를 Get에서 사용하는걸 지양해야한다라고 하시는 것 같은데 맞나요?
받으신 리뷰 중 "ModelAttribute는 Body이므로 get 요청에서 사용을 지양해달라"는게 잘 이해가 안되는데 Get 요청할 때 Body를 보내는게 잘못된 것이지 ModelAttribute를 쓰는 것 자체가 잘못된 행위라고 말씀하신 것 같아 조금 의아함이 드네요...?

1개의 답글