본문 바로가기
Spring/스프링 MVC 활용

Spring ArgumentResolver

by 히포파타마스 2022. 8. 19.

Spring ArgumentResolver

 

 

 

1. Servlet

HTTP 요청 시 WAS는 Request, Response 객체를 새로 생성하고, 서블릿 객체를 호출한다.

 

[Servlet 생성]

 

 

개발자는 Request 객체에서 HTTP 요청 정보를 사용하고 Response 객체에 HTTP 응답 정보를 입력한다.

서블릿이 종료되면 WAS는 Response 객체에 담겨있는 내용으로 HTTP 응답 정보를 생성한다.

 

즉, 생성된 Request, Response 객체를 받아 하나의 스레드에서 서블릿 객체가 실행되면,

그 시점부터 개발자는 Request의 정보를 사용해 Response에 적절한 응답을 담아 서블릿을 종료하는 것으로 요청을 처리할 수 있다. 

※ 서블릿 컨테이너는 서블릿을 지원하는 WAS를 뜻한다.

 

 

 

 

 

 

 

 

 

2. Spring MVC

Request와 Response 객체를 받아 서블릿이 생성되면 다음과 같이 Spring의 dispatcherServlet로 Request와 Response가 넘어가게 된다.

 

[Request, Response의 흐름]

 

 

 

1. Servlet 객체가 Request와 Response를 받아 생성된다.

2. Servlet 객체는 내부의 service() 메서드를 실행시켜, Request와 Response를 스프링의 dispatcher Servlet으로 전달한다.

3. dispatcherServlet은 request의 URL을 확인하고 매핑되는 핸들러와 어댑터가 있는지 조회한다.

4. 매핑되는 핸들러와 어댑터가 존재한다면 핸들러 어댑터를 호출하고 Request와 Response를 넘긴다.

5. 핸들러 어댑터는 호출해야 할 핸들러(컨트롤러)를 호출하고, Request와 Response를 넘긴다.

 

 

중요한 것은 다음과 같다.

 

서블릿 Request와 Response 객체를 받아 개발자는 요청을 처리하고 응답할 수 있다.

스프링에서는 위 그림과 같은 흐름을 통해, Request와 Response가 요청의 URL과 매핑되는 컨트롤러에 넘겨지게 된다.

따라서 개발자는 컨트롤러에서 Request와 Response를 사용할 수 있고, 컨트롤러에서 요청을 처리하고 응답할 수 있다.

 

 

 

 

 

2.1 servlet request, response 사용 예

다음과 같이 사용자를 나타내는 User,

User를 저장하는 UserRepository,

User에 대한 서비스를 제공하는 UserService가 있다고 하자.

 

[User]

@Data
public class User {

    long id;
    String name;
    String password;

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

 

 

 

[UserRepository]

@Repository
public class UserRepository {

    public final HashMap<Long, User> userRepo = new HashMap<>();

    public void save(User user) {
        userRepo.put(user.id, user);
    }

    public User findByUserId(long userId) {
        User user = userRepo.get(userId);
        return user;
    }
}

 

 

 

[UserService]

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    public void signUp(User user) {
        userRepository.save(user);
    }

    public User getUser(long userId) {
        User user = userRepository.findByUserId(userId);
        return user;
    }
}

 

 

 

 

 

클라이언트가 user를 등록하기 위해 쿼리 파라미터로 user의 정보를 보낼 경우 Controller는 다음과 같이 서블릿 Request와 Response를 사용해서 요청을 처리하고 응답할 수 있다.

 

[Servlet으로 요청 처리]

@PostMapping("/user/signUpByServlet")
public void signUpByServlet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    String id = request.getParameter("id");
    String name = request.getParameter("name");
    String password = request.getParameter("password");

    User user = new User(Long.parseLong(id), name, password);
    userService.signUp(user);

    response.getWriter().write("ok");
}

 

앞서 설명한 것처럼 컨트롤러로 서블릿 Request(HttpServletRequest)와 Response(HttpServletResponse)가 넘어오기 때문에 두 객체를 파라미터로 받을 수 있다.

 

request는 HTTP 요청 정보를 기반으로 만들어졌기 때문에 관련 정보와 파라미터들이 들어있다.

따라서 사용자가 User의 정보를 쿼리 파라미터로 요청하면 위 코드와 같이 request에 해당 파라미터를 조회할 수 있다.

개발자는 조회된 값들로 User를 생성하고 등록해주면 된다.

 

 

 

 

사용자가 다음과 같이 URL을 작성해서 요청하면 User가 생성돼서 저장된다.

그리고 사용자는 개발자가 지정한 응답을 받는다(예제에서는 "ok")

 

[{{host}}/user/signUpByServlet?id=0&name=park&password=1234]

 

 

 

 

 

 

컨트롤러에서 Request와 Response를 받을 수 있어서 개발자는 요청의 처리와 응답을 할 수 있다.

그러나 서블릿이 제공하는 Request와 Response는 사용하기 불편한 점이 있다.

 

우선 위 예제에서 내가 필요한 것은 User를 생성하기 위한 정보들이다.

그런데 서블릿 Request를 사용해서 사용자가 보낸 정보를 받으려면 각 정보의 이름과 매칭 되는 파라미터를 조회하고, 심지어 User 생성에 맞는 타입으로 각각 변환하는 것도 고려해야 한다.

 

게다가 만약 쿼리 파라미터가 아니라 JSON으로 User의 정보가 온다고 해보자,

일단은 HttpServletRequest를 통해서 Http의 body의 정보도 가져올 수 있긴 하다.

 

[body]

@PostMapping("/user/signUpByServletJson")
    public void signUpByServletJson(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        System.out.println(messageBody);

//        body를 하나씩 파싱해서 객체를 만들기는 어렵다..
//        User user = new User(Long.parseLong(id), name, password);
//        userService.signUp(user);

        response.getWriter().write("ok");
    }

 

 

 

 

그러나 결국 HTTP의 Body는  다음과 같은 String, 즉 문자의 덩어리이다.

 

[HTTP_Body_Json]

{
    "id":"0",
    "name":"park",
    "password":"1234"
}

 

이 문자 덩어리를 파싱 해서 User의 정보(id, name, password)를 가져오는 것은 상당히 힘들고 비효율적이다.

 

 

 

 

 

 

 

 

3. ArgumentResolver

HttpServletRequest에서 정보들을 우리가 사용할 형태로 가져오는 일은 그 자체만으로 비효율적이고 고된 작업이다.

결국 우리는 요청에서 오는 정보들을 한 번에 알맞은 파라미터로 받아서 처리하고 싶다.

 

 

이 역할을 Spring의 ArgumentResolver가 해준다.

 

[ArgumentReslover 개입]

 

핸들러 어댑터는 핸들러를 호출하기 전에 ArgumentResolver를 호출한다.

ArgumentResolver는 호출할 컨트롤러를 알고 있기 때문에 컨트롤러의 파라미터나 붙여진 애노테이션의 정보를 기반으로 컨트롤러에 전달한 데이터를 생성한다.

생성된 데이터는 컨트롤러에 파라미터로 전달된다.

 

예를들어, 앞선 예제에서 User정보를 HttpServletRequest에서 하나씩 빼서 데이터를 생성했던 작업을 ArgumentResolver을 사용해서 대신 처리할 수 있고, 생성된 데이터를 컨트롤러에서 직접 받을 수 있다.

 

[ArgumentResolver 예]

@PostMapping("/user/signUpParam")
public String signUpByParam(@RequestParam long id, @RequestParam String name, @RequestParam String password) {
    User user = new User(id, name, password);
    userService.signUp(user);

    return "ok";
}

 

HttpServletRequest에서 User정보를 각각 조회하고 생성하지 않고 바로 컨트롤러의 파라미터로 받을 수 있다.

 

이처럼 ArgumentResolver를 통해 사용자는 컨트롤러에서 실제로 사용할 데이터들을 바로 받을 수 있다.

스프링은 30여 가지의 ArgumentResolver가 기본으로 제공된다,

따라서 스프링의 컨트롤러는 아주 다양한 파라미터를 받을 수 있다.

 

 

 

 

 

3.1 ArgumentResolver의 종류

스프링은 여러 ArgumentResolver를 지원하고 컨트롤러는 요청에 따라 다양한 파라미터를 사용할 수 있다.

이번에는 어떤 요청을 받았을 때, ArgumentResolver를 적용해서 어떤 파라미터를 사용해야 하는지를 살펴본다.

 

 

3.1.1 쿼리 파라미터로 요청을 받는 경우

3.1.1.1 @RequestParam

컨트롤러의 파라미터에 @RequestParam("name")이 붙으면 쿼리 파라미터에서 name과 일치하는 값을 바로 받을 수 있다.

 

[@RequestParam]

@PostMapping("/user/signUpParam")
public String signUpByParam(@RequestParam long id, @RequestParam String name, @RequestParam String password) {
    User user = new User(id, name, password);
    userService.signUp(user);

    return "ok";
}

 

파라미터의 변수명과 쿼리 파라미터의 key값이 같다면 위와 같이 @RequestParam("name")의 ("name")을 생략할 수 있다.

 

 

 

사용자는 다음과 같이 쿼리 파라미터로 User정보를 요청하면 된다.

[@RequestParam 요청 처리]

 

 

 

 

 

3.1.1.2 @ModelAttribute

@ModelAttribute 애노테이션을 사용하면 쿼리 파라미터를 객체로 바로 받을 수 도 있다.

 

[쿼리 파라미터 정보로 객체를 생성하는 ArgumentResolver]

@PostMapping("/user/signUpByParamToObj")
public String signUpByParamToObj(@ModelAttribute User user) {
    userService.signUp(user);

    return "ok";
}

 

ArgumentResolver는 쿼리 파라미터로 전달받은 정보들을 사용해서 @ModelAttribute가 붙은 객체를 생성하고 컨트롤러에 넘겨준다.

 

※ @ModelAttribute는 생략할 수 있으나 애노테이션이 없으면 해당 컨트롤러가 어떤 요청을 받는지 알기 어렵기 때문에 잘 생략되지 않는다.

 

@RequestParam은  사용자가 구현하지 않은 Integer, String, int, ... 등과 같은 자바의 기본형에 적용되고 @ModelAttribute는 사용자가 구현한 객체에 적용된다.

 

 

 

 

 

3.1.2 form-data로 요청이 오는 경우

3.1.2.1 @RequestParam

@RequestParam은  사용자가 구현하지 않은 Integer, String, int, ... 등과 같은 기본형이 form 데이터로 오는 경우에도 해당 데이터를 파라미터로 전달한다.

 

즉, 앞의 @RequestParam 예제와 동일한 컨트롤러로 사용자가 다음과 같이 form 데이터로 요청을 보내도 처리가 된다.

 

[@RequestParam_form-data 요청 처리]

 

 

 

 

 

3.1.2.2 @ModelAttribute

컨트롤러의 파라미터에 @ModelAttribute가 붙으면 ArgumentResolver는 전달받은 form 데이터 정보로 해당 파라미터를 생성하고 컨트롤러에 전달한다.

 

[@ModelAttribute]

@PostMapping("/user/signUpByForm")
public String signUpByForm(@ModelAttribute User user) {
    userService.signUp(user);

    return "ok";
}

 

 

 

 

사용자가 다음과 같이 form 데이터로 User 정보를 보내면 요청이 처리된다.

 

[@ModelAttribute로 요청 처리]

 

이때도 @ModelAttribute는 생략할 수 있으나 위에서 쿼리파라미터를 받을 때와 같은 이유로 생략하는 것이 권장되진 않는다.

 

 

 

 

 

 

 

3.1.3 Json으로 요청이 오는 경우

3.1.3.1 @RequestBody

컨트롤러의 파라미터에 @RequestBody가 붙어있으면 ArgumentResolver는 전달받은 Json을 파싱해서 파라미터를 생성하고 컨트롤러에 전달해준다.

 

[@RequestBody]

@PostMapping("/user/signUpByJson")
public String signUpByJson(@RequestBody User user) {
    userService.signUp(user);

    return "ok";
}

 

굉장히 편리하지만 Json 데이터를 담을 객체가 반드시 필요하다.

 

 

 

[@RequestBody로 Json 요청 처리]

 

 

 

 

 

 

 

3.1.4 경로 변수를 포함해서 요청이 오는 경우

요청에 경로 변수가 포함돼서 온다면, @PathVariable("name")을 사용해서 ("name")의 경로 변수를 받을 수 있다.

 

[@PathVariable]

@GetMapping("/user/{userId}")
public User getUser(@PathVariable long userId) {
    User user = userService.getUser(userId);
    return user;
}

 

경로 변수와 컨트롤러의 파라미터 변수명이 같다면 @PathVariable("name")에서 ("name")을 생략할 수 있다.

 

 

 

[@PathVariable로 요청 처리]

 

 

 

 

 

참고 : 

https://velog.io/@rmswjdtn/Spring-Servlet-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B3%A0-%EB%84%98%EC%96%B4%EA%B0%80%EC%9E%90

 

[Spring] Servlet 정리하고 넘어가자!

스프링 부트를 쓰다보면 잘 모르고 넘어가는 Spring MVC Flow! 이번기회에 한번 정리해보려고 합니다~JSP : HTML문서에서 내부적으로 Java문법을 사용할 수 있게 해주는 Java Server Page언어Servlet : client 의

velog.io

사용자의 요청부터 서블릿 생성, 스프링 까지의 흐름이 아주 잘 설명되어있다! 

특히 서블릿 부분은 어떻게 생성되서 무슨 메서드를 호출하는지 애매했는데 이부분도 굉장히 자세히 설명되어있다.

'Spring > 스프링 MVC 활용' 카테고리의 다른 글

#11 파일 업로드  (0) 2021.08.03
#10 스프링 타입 컨버터  (0) 2021.08.03
#9 API 예외 처리  (0) 2021.08.03
#8 예외 처리와 오류 페이지  (0) 2021.08.02
#7 로그인 처리 - 필터, 인터셉터  (0) 2021.08.01

댓글