티스토리 뷰

이번 글에서는 스프링(Spring)에서 제공하는 HTTP 요청 방법에 관련하여 개발할 때 편리성을 제공하는 어노테이션들에 대해 알아보도록 하겠습니다.

 

단순하게 어노테이션 사용방법 뿐만 '뿅' 하고 설명하는 것이 아닌, 예전에는 어떤 방법을 사용하였고, 어떤 방법들이 존재하는지 하나씩 리팩토링 해나가는 식으로 코드를 분석하고 설명하여 완벽하게 자신의 것으로 만드는 것이 이 글의 목적입니다.

 

이 글을 읽기 전 서블릿(Servlet)과 HttpServletRequest/Response에 대하여 기초 지식이 부족하신 분들은 아래 링크를 통해 공부하고 오시는 것이 이해하기 훨씬 쉽기 때문에 추천드립니다.

 

서블릿이란?

 

HttpServletRequest란?

 

HttpServletResponse란?


 

HTTP 요청 - 기본 헤더 조회

어노테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원합니다.

 

예전 제 블로그의 글 중에 HttpServletRequest를 통해 요청 메시지의 헤더 정보를 읽는 방법에 대해 설명했던 적이 있다.

 

스프링 어노테이션을 사용하지 않고 헤더 값을 조회하려고 할 때, HttpServletRequest 객체가 제공하는 편의 메서드를 사용하여 아래 예제 코드와 같이 헤더 값을 조회할 수 있었습니다.

 

 

스프링 어노테이션 사용 전 예시 코드

@RequestMapping("/headers")
    public String headers(HttpServletRequest request, HttpServletResponse response) {                         
        log.info("method={}", request.getMethod());
        log.info("locale={}", request.getLocale());
        log.info("header host={}", request.getHost());
    }

 

 

스프링 어노테이션 사용 후 예시 코드

@RequestMapping("/headers")
    public String headers(HttpServletRequest request, HttpServletResponse response,
                          HttpMethod httpMethod,
                          Locale locale,
                          @RequestHeader MultiValueMap<String, String> headerMap,
                          @RequestHeader("host") String host,
                          @CookieValue(value = "myCookie", required = false) String cookie) {
        log.info("request={}", request);
        log.info("response={}", response);
        log.info("httpMethod={}", httpMethod);
        log.info("locale={}", locale);
        log.info("headerMap={}", headerMap);
        log.info("header host={}", host);
        log.info("myCookie={}", cookie);
    }

 

먼저 코드를 분석해보자면, 현재 header() 메서드에 많은 파라미터가 들어가 있는 것을 볼 수 있습니다.

 

내용은 다음과 같습니다. (HttpServletRequest, HttpServletResponse 생략)

 

  • HttpMethod : HTTP 메서드를 조회합니다.
  • Locale : Locale 정보를 조회합니다.
  • @RequestHeader MultiValueMap <String, String> headerMap : 모든 HTTP 헤더를 MultiValueMap 형식으로 조회합니다.
  • @RequestHeader("host") String host : 특정 HTTP 헤더를 조회합니다. 대표적인 속성으로는 필수 값 여부를 설정하는 required와 기본 값을 설정하는 defaultValue가 있습니다.
  • @CookieValue(value="myCookie", required=false) String cookie : 특정 쿠키를 조회합니다. 대표적인 속성으로는 필수 값 여부를 설정하는 required와 기본 값을 설정하는 defaultValue가 있습니다.

 


 

MultiValueMap?

Map과 유사하며, 하나의 키에 여러 값을 받을 수 있습니다.

 

HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용합니다.

MultiValueMap<String, String> map = new LinkedMultiValueMap();
map.add("username", "Baek");
map.add("username", "Kim");

//[Baek, Kim]
List<String> values = map.get("username");

 


 

어노테이션을 사용하니까 사용하지 않는 코드보다 더 복잡하고 길어보이는데...?

 

스프링 어노테이션을 사용하여 헤더 정보를 조회하는 것은 몇 가지 중요한 이유가 있습니다.

 

1. 간결성과 가독성

어노테이션을 사용하면 메서드 시그니처에 명확하게 의도를 나타낼 수 있습니다.

예로 @RequestHeader, @CookieValue를 사용한다면 해당 파라미터가 HTTP 요청의 특정 헤더나 쿠키 값을 구하는 것임을 명확히 알 수 있습니다. 이는 코드의 가독성을 높여줍니다.

 

 

2. 편리한 바인딩 

어노테이션을 사용하면 스프링이 자동으로 HTTP 요청의 특정 부분을 메서드 파라미터로 바인딩해 줍니다.

이는 수동으로 HttpServletRequest 객체에서 값을 추출하는 과정을 줄여줍니다.

public String headers(
    @RequestHeader("host") String host, 
    @CookieValue(value = "myCookie", required = false) String cookie) 
    { 
        // 'host'와 'cookie' 변수는 자동으로 바인딩 된다.
    }

 

이를 통해 중복 코드를 줄일 수 있고, 코드의 유지보수성을 높일 수 있습니다.

 

 

3. 스프링의 풍부한 기능 활용

스프링 프레임워크는 어노테이션 기반 프로그래밍을 지원하여 다양한 기능을 제공하고, 확장성을 높여줍니다.

예로 @RequestHeader 나 @CookieValue는 추가적인 속성을 지원하여 기본값 설정, 필수 여부 지정 등을 할 수 있습니다.

 

이외에도 의존성 주입 및 테스트 코드 작성이 용이해지는 장점도 있으며, 어노테이션을 선언적 프로그래밍을 할 수 있다는 장점이 있습니다.

 


 

HTTP 요청 파라미터

이전에 HttpServletRequest 객체의 getParameter()를 사용하여 GET-쿼리 파라미터 전송과 POST-HTML Form 전송, 이 두 가지 요청 파라미터를 조회할 수 있었습니다.

 

GET 쿼리 파라미터 전송 방식이든, POST HTML Form 전송 방식이든 둘 다 형식이 같으므로 구분 없이 조회할 수 있습니다.

 

이것을 간단히 요청 파라미터(request parameter) 조회라고 합니다.

 

getParameter() 사용하여 파라미터를 조회하는 방법 이전 글에 설명했기에, 지금부터는 스프링으로 요청 파라미터를 조회하는 방법을 단계적으로 알아보도록 하겠습니다.

 

 

HTTP 요청 파라미터 - @RequestParam

스프링이 제공하는 @RequestParam을 사용하면 요청 파라미터를 매우 편리하게 사용할 수 있습니다.

@ResponseBody
@RequestMapping("/request-param")
public String requestParamTest(
        @RequestParam("username") String memberName, 
        @RequestParam("age") int memberAge) {
        
    log.info("username={}, age={}", memberName, memberAge);
    return "ok";
}

 

  • @RequestParam : 파라미터 이름으로 바인딩합니다.
  • @ResponseBody : View 조회를 무시하고, HTTP message body에 직접 해당 내용을 입력합니다. (현재 내용에서는 벗어나기 때문에 추후 다른 포스팅에서 설명 예정입니다.)

HttpServletRequest의 getParameter("username")와 @RequestParam("username")은 동일하다고 생각하시면 되겠습니다. @RequestParma의 name(value) 속성이 파라미터 이름으로 사용됩니다.

 

만약 HTTP 파라미터 이름이 변수 이름과 같다면, @RequestParam("username")을 @RequestParam으로 생략이 가능합니다.

@ResponseBody
@RequestMapping("/request-param")
public String requestParamTest(
        // 변수 명이 파라미터 이름과 동일해야 생략 가능
        @RequestParam String username, 
        @RequestParam int age) { 
        
    log.info("username={}, age={}", username, age);
    return "ok";
}

 

 

여기서 추가로 String, int, Integer 등의 단순 타입이라면 @RequestParam도 생략이 가능합니다.

 

이렇게 어노테이션을 완전히 생략해도 되지만, 너무 없다면 명시성이 떨어지기 때문에 개인적으로 비추천합니다.

코드를 읽으려고/분석하려 하면 더 복잡해질 것 같고, 헷갈릴 거 같기 때문입니다.


 

주의! 스프링 부트 3.2 파라미터 인식 문제

 

스프링부트 3.2부터 자바 컴파일러에 -parameters 옵션을 넣어주어야 어노테이션에 적는 이름을 생략할 수 있습니다.

 

주로 @RequestParam, @PathVariable 어노테이션에서 문제가 발생한다.

java.lang.IllegalArgumentException 에러가 발생한다.

 

 

해결 방안으로는 첫 번째로 어노테이션에 이름을 생략하지 않고 다음과 같이 이름을 항상 적어준다. (권장)

  • @RequestParam("username") String username
  • @PathVariable("userId") String userId

 

해결방안 두 번째는 컴파일 시점에 -parameters 옵션을 적용하는 것이다. (인텔리J 기준)

  1. IntelliJ IDEA에서 File -> Setting을 연다. (Mac은 IntelliJ IDEA -> Settings)
  2. Bulid, Execution, Deployment -> Compiler -> Java Comiler로 이동한다.
  3. Additional command line parameters라는 항목에 '-parameters'를 추가한다.
  4. out 폴더를 삭제하고 다시 실행한다. 꼭 out 폴더를 삭제해야 다시 컴파일이 일어난다.

 

세 번째 해결방안으로는 Gradle을 사용해서 빌드하고 실행하면 된다.

 

필자는 주로 첫 번째 방법인 어노테이션에 이름을 생략하지 않고 작성하는 방법을 사용한다.

명시적이어서 보기 편하고 쉽기 때문이다.


 

파라미터 필수 여부 및 기본 값 적용 - required, defaultValue 속성

@RequestParam 어노테이션에는 파라미터 필수 여부를 설정할 수 있고 기본 값을 적용할 수 있게 도와주는 required 속성과 default 속성이 있다.

 

@requestParam 어노테이션의 required 속성 기본 값은 true(파라미터 필수이다.)

@ResponseBody
@RequestMapping("/request-param")
public String requestParamTest(
        @RequestParam(required = true) String username, 
        @RequestParam(required = false) Integer age) { 
        
    log.info("username={}, age={}", username, age);
    return "ok";
}

 

 

위 코드를 보면 username에는 true, age 에는 false 가 설정되어 있다.

username 에 대한 파라미터는 필수로 요청이 들어와야 하고, age에 대한 파라미터는 요청이 들어오지 않아도 된다는 뜻으로 사용된다.

 

만약 '/request-param? age=10'으로 요청이 들어온다면 에러가 발생한다.

username에 대한 값이 들어오지 않았기 때문이다.

 

여기서 주의할 점은, '/request-param? username=' 이렇게 파라미터 이름만 있고 값이 없는 경우는 빈문자로 값이 들어가게 되어 에러가 발생하지 않는다.

 

추가로 기본형(primitive)에 null을 입력하는 것은 불가능하기 때문에(500 에러) null을 받을 수 있는 Integer 타입으로 변경하거나 defaultValue 속성을 사용하도록 하자.

@ResponseBody
@RequestMapping("/request-param")
public String requestParamTest(
        @RequestParam(required = true, defaultValue = "guest") String username, 
        @RequestParam(required = false, defaultValue = "-1") int age) { 
        
    log.info("username={}, age={}", username, age);
    return "ok";
}

 

파라미터에 값이 없는 경우 defaultValue에 설정된 값으로 기본 값을 적용할 수 있다.

이럴 때는 이미 기본 값이 있기 때문에 required 속성이 의미가 없다.

 

defaultValue는 빈 문자의 경우에도 설정한 기본 값이 적용됩니다.

 

 

HTTP 요청 파라미터 - @ModelAttriute

실제 개발을 하면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 합니다.

보통은 다음과 같은 코드를 작성할 수 있습니다.

@RequestParam("username") String username;
@RequestParam("age") String age;

HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);

 

필드가 두 개니까 그렇기 정말 많은 필드가 존재한다면 코드도 길어지고 개발하기 힘들 것이다.

 

스프링은 이 과정을 완전히 자동화해 주는 @ModelAttribute 어노테이션을 제공한다.

// 요청 파라미터를 바인딩 받을 객체 생성
@Data
public class HelloData {
    private String username;
    private int age;
}

@ResponseBody
@RequestMapping("model-attribute-test")
public String modelAttribute(@ModelAttribute HelloData helloData) {
    log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
    return "ok";
}

 

바인딩받을 객체 앞에 @ModelAttribute 어노테이션을 적어주면 username, age 요청 파라미터가 HelloData 객체에 알아서 바인딩되어 값이 들어가게 된다.

 

스프링 MVC의 @ModelAttribute의 실행 과정은 다음과 같다.

  1. HelloData 객체를 생성한다.
  2. 요청 파라미터 이름으로 HelloData 객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다. (예) 파라미터 이름이 username이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력한다.

 

HelloData 클래스에 롬복 라이브러리를 사용하여 @Data 어노테이션을 사용하면 알아서 Getter, Setter 등등을 만들어준다.

롬복에 대해 잘 모른다면 아래 링크를 참고하는 것을 추천합니다.

 

[JAVA] Lombok(롬복) 사용 - IntelliJ에서 설치 및 적용방법

Lombok(롬복) 이란? Lombok 은 자바의 Annotation processsor 라는 기능을 이용하여 컴파일 시점에 Lombok의 어노테이션을 읽어서, 다양한 메서드와 생성자를 자동으로 생성해 주는 라이브러리이다. (Getter, Se

developshrimp.com

 

 

@ModelAttribute 어노테이션도 @RequestParam과 동일하게 어노테이션을 생략할 수 있다.

하지만 혼란을 야기할 수 있기 때문에 생략하지 않는 걸 권장한다.

@ResponseBody
@RequestMapping("model-attribute-test")
public String modelAttribute(HelloData helloData) {
    log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
    return "ok";
}

 

스프링은 해당 생략 시 다음과 같은 규칙을 적용한다.

  • String, int, Integer 같은 단순 타입 = @RequestParam
  • 나머지 = @ModelAttribute (argument resolver로 지정해 둔 타입 외)

argument resolver는 추후 다른 포스팅에서 설명 예정이다.

 


 

HTTP 요청 메시지 - 단순 텍스트

여기까지 공부했다면, 서블릿 정도는 다 알고 있다고 가정을 하겠다.

 

서블릿에서 HTTP 요청 메시지를 받는 방법을 생각해보자.

  • HTTP API에서 주로 사용, JSON, XML, TEXT
  • 데이터 형식은 주로 JSON 사용
  • POST, PUT, PATCH

 

우리가 위에서 공부한 요청 파라미터와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam, @ModelAttribute를 사용할 수 없다.

(물론 HTML Form 형식으로 전달되는 경우는 요청 파라미터로 인정된다.)

 

 

먼저 HttpServletRequest를 통해 가장 단순한 텍스트 메시지를 읽을 때, HTTP 메시지 바디의 데이터를 InputStream을 사용해서 직접 읽을 수 있었다.

@PostMapping("/request-body-string")
public void requestBodyStringTest(HttpServletRequest request, HttpServletResponse response) throws IOException {
    SertvletInputStream inputStream = request.getInputStream();
    String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
    
    log.info("messageBody={}", messageBody);
    response.getWriter().write("ok");
}

 

이미 이전에 다 해봤고 단순하기 때문에 테스트는 넘어가도록 하겠다.

 

스프링 MVC는 더 편리한 기능을 위해 다음 파라미터를 지원한다.

  • InputStream(Reader) : HTTP 요청 메시지 바디의 내용을 직접 조회
  • OutputStream(Writer) : HTTP 응답 메시지의 바디에 직접 결과 출력
// InputStream(Reader) : HTTP 요청 메시지 바디의 내용을 직접 조회
// OutputStream(Writer) : HTTP 응답 메시지의 바디에 직접 결과 출력
@PostMapping("/request-body-string-v2")
public void requestBodyStringTestV2(InputStream reader, Writer writer) throws IOException {
    String messageBody = StreamUtils.copyToString(reader, StandardCharsets.UTF_8);
    log.info("messageBody={}", messageBody);
    writer.write("ok");
}

 

 

스프링 MVC는 이뿐만 아니라 편의성을 위해 다음 파라미터들도 지원한다.

  • HttpEntity : HTTP header, body 정보를 편리하게 조회
    • 메시지 바디 정보를 직접 조회
    • 요청 파라미터를 조회하는 기능과 관계없음 (@ReqeustParam, @ModelAttribute X)
  • HttpEntity는 응답에도 사용 가능
    • 메시지 바디 정보 직접 반환
    • 헤더 정보 포함 가능
    • view 조회 X

 

HttpEntity를 상속받은 다음 객체들도 같은 기능을 제공합니다.

  • RequestEntity
    • HttpMethod, url 정보가 추가, 요청에서 사용
  • ResponseEntity
    • HTTP 상태 코드 설정 가능, 응답에서 사용

 

@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringTestV3(HttpEntity<String> httpEntity) {
    String messageBody = httpEntity.getBody();
    log.info("messageBody={}", messageBody);
    
    return new HttpEntity<>("ok");
}

=== 또는 ===

@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringTestV3(RequestEntity<String> httpEntity) {
    String messageBody = httpEntity.getBody();
    log.info("messageBody={}", messageBody);

    // message body에 "ok" 문자 전달 후 상태코드 201(CREATED) 반환
    return new ResponseEntity<>("ok", HttpStatus.CREATED);
}

 

 

 

마지막으로 스프링은 HTTP 메시지 바디 정보를 더욱더 편리하게 조회하기 위한 @RequestBody을 제공한다.

 

메시지 바디 정보를 편리하게 조회하는 기능을 제공하기 때문에, 만약에 헤더 정보가 필요하다면 위에서 설명한 HttpEntity 혹은 @RequestHeader 기능을 사용하면 된다.

@ResponseBody
@PostMapping("/request-body-string-v4")
public HttpEntity<String> requestBodyStringTestV4(@RequestBody String messageBody) {
    log.info("messageBody={}", messageBody);
    
    return "ok";
}

 

@ResponseBody를 사용하면 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달할 수 있습니다. (view 사용 X)

 

 

HTTP 요청 메시지 - JSON

이번에는 HTTP API에서 주로 사용하는 JSON 데이터 형식을 조회해 보자.

 

아래 예제는 기존 서블릿에서 사용했던 방식이다.

@Slf4j
@Controller
public class RequestBodyJsonController {

    private ObjectMapper objectMapper = new ObjectMapper();
    
    @PostMapping("/request-body-json-v1")
    public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
    	ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        
        log.info("messageBody={}", messageBody);
        
        HelloData data = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", data.getUsername(), data.getAge());

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

 

위 코드는 HttpServletRequest를 사용해서 직접 HTTP 메시지 바디에서 데이터를 읽어와 문자로 변환한다.

 

문자로 된 JSON 데이터를 Jackson 라이브러리인 objectMapper를 사용해서 자바 객체로 변환한다.

 

 

위의 방식에 문제점으로는, 문자로 변환하고 다시 JSON으로 변환하는 과정이 불편할 것입니다.

+ 코드가 복잡하고 길다.

 

HTTP 요청 메시지- 단순 데이터에서와 마찬가지로 스프링은 이런 문제점을 해결하기 위해 @RequestBody 어노테이션을 제공합니다.

@ResponseBody
@PostMapping("/request-body-json-v2")
public void requestBodyJsonV2(@ReqeustBody HelloData data) {
    log.info("username={}, age={}", data.getUsername(), data.getAge());

    return "ok";
}

 

@RequestBody 하나로 너무나도 간결하게 코드가 줄어드는 것을 확인할 수 있습니다.

분명히 @RequestBody 어노테이션은 message body에 단순 텍스트를 읽는 역할을 수행했는데, 코에 걸면 코걸이 귀에 걸면 귀걸이인 것처럼 객체 앞에 작성하면 JSON 객체로 알아서 변환해 주는 것이 신기하실 겁니다.

 

HttpEntity, @RequestBody를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해 줍니다. 

 

HTTP 메시지 컨버터는 문자뿐만 아니라 JSON도 객체로 변환해 주는데, 우리가 방금 문자열로 바꾸고 다시 JSON 객체로 바꾸는 귀찮고 복잡한 작업을 대신 처리해 주는 것이죠.

(HTTP 메시지 컨버터에 대해서는 추후에 포스팅 예정입니다. 지금 여기서는 그냥 스프링이 기본적으로 제공해 주는 편리한 기능이라고 생각하시면 될 것 같습니다.)

 

 

여기서 주의!

HTTP 요청 시에 content-type이 application/json인지 꼭! 확인해야 합니다.

그래야 JSON을 처리할 수 있는 HTTP 메시지 컨버터가 실행됩니다.

 

 

물론 앞서 배운 것과 같이 HttpEntity를 사용하여 JSON으로 바꿔줄 수도 있습니다.

@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(HttpEntity<HelloData> httpEntity) {
    HelloData data = httpEntity.getBody();
    log.info("username={}, age={}", data.getUsername(), data.getAge());

    return "ok";
}

 

추가로 응답의 경우에 @ResponseBody를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있습니다.

@ResponseBody
@PostMapping("/request-body-json-v4")
public HelloData requestBodyJsonV4(@RequestBody HelloData data) {
    log.info("username={}, age={}", data.getUsername(), data.getAge());

    return data;
}

 

 

응답의 자세한 내용은 다음 글을 참고해 주시면 도움이 될 것입니다.

 


 

참고(출처)

이 글은 인프런에서 김영한님의 스프링 MVC 1편 강의를 토대로 정리한 공부 글입니다.

혹시나 정리 간에 틀린 내용이 있다면 댓글 부탁드립니다.

감사합니다.

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 | 김영한 - 인프런

김영한 | 웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습

www.inflearn.com