Archive/Spring Web MVC

3.1 요청 매핑하기 (handler method)

nimkoes 2021. 7. 11. 15:24
728x90

 

 

이번에 정리 할 내용은 HTTP 요청을 handler method 에 매핑하는 방법이다.

 

정리에 앞서 본 내용과 무관한 코드를 정리했다.

MyHelloController 를 제외한 Person, PersonFormatter, GreetingInterceptor, AnotherInterceptor 를 삭제하고 WebConfig 의 Interceptor, ResourceHandler, Jaxb 설정도 삭제했다.

MyHelloControllerTest 클래스의 내용도 모두 삭제 했다.

 

 

MyHelloController 에 다음과 같이 handler method 를 하나 등록 하였다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

@Controller annotation 을 사용해서 component scan 대상으로 만들고, 그 안에 @RequestMapping annotation 을 사용해서 uri 를 지정해주면 handler method 가 된다.

 

만약 11라인의 @ResponseBody 를 사용하지 않으면 return 하는 문자열에 해당하는 view 를 찾고, 사용 할 경우 반환하는 값을 HTTP response body 에 실어 응답 데이터로 보내게 된다.

 

추가로 @RequestMapping 을 사용해서 uri 만 지정할 경우 모든 종류의 HTTP method 를 허용하게 된다.

HTTP method 의 종류로는 GET, POST, HEAD, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH 등이 있다.

실제로 그런지 테스트 코드를 작성해서 확인해 봤다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {
        mockMvc.perform(get("/hello")).andDo(print()).andExpect(status().isOk());
        mockMvc.perform(post("/hello")).andDo(print()).andExpect(status().isOk());
        mockMvc.perform(head("/hello")).andDo(print()).andExpect(status().isOk());
        mockMvc.perform(put("/hello")).andDo(print()).andExpect(status().isOk());
        mockMvc.perform(delete("/hello")).andDo(print()).andExpect(status().isOk());
        mockMvc.perform(options("/hello")).andDo(print()).andExpect(status().isOk());
        mockMvc.perform(patch("/hello")).andDo(print()).andExpect(status().isOk());

    }
}

 

 

만약 특정한 요청에 대해서만 허용하게 하려면 @RequestMapping annotation 에 값을 추가하면 된다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

RequestMethod 는 enum 타입으로 HTTP request method 를 정의하고 있다.

 

/*
 * Copyright 2002-2015 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.bind.annotation;

/**
 * Java 5 enumeration of HTTP request methods. Intended for use with the
 * {@link RequestMapping#method()} attribute of the {@link RequestMapping} annotation.
 *
 * <p>Note that, by default, {@link org.springframework.web.servlet.DispatcherServlet}
 * supports GET, HEAD, POST, PUT, PATCH and DELETE only. DispatcherServlet will
 * process TRACE and OPTIONS with the default HttpServlet behavior unless explicitly
 * told to dispatch those request types as well: Check out the "dispatchOptionsRequest"
 * and "dispatchTraceRequest" properties, switching them to "true" if necessary.
 *
 * @author Juergen Hoeller
 * @since 2.5
 * @see RequestMapping
 * @see org.springframework.web.servlet.DispatcherServlet#setDispatchOptionsRequest
 * @see org.springframework.web.servlet.DispatcherServlet#setDispatchTraceRequest
 */
public enum RequestMethod {

	GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE

}

 

이제 이 handler 는 HTTP GET 요청만 처리할 수 있다.

테스트 코드를 다음과 같이 고쳐 실행해 보았다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(get("/hello"))
            .andDo(print())
            .andExpect(status().isOk())
        ;

        mockMvc.perform(post("/hello"))
            .andDo(print())
            .andExpect(status().isMethodNotAllowed())
        ;

    }

}

 

handler 가 GET 방식의 요청만 처리할 수 있게 되었으므로 POST 요청에 대한 status 로 405 Method Not Allowed 를 반환하게 된다.

만약 HTTP GET 과 POST 를 모두 허용하고 싶으면 handler 를 다음과 같이 method 를 배열 형태로 수정하면 된다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @RequestMapping(value = "/hello", method = {RequestMethod.GET, RequestMethod.POST})
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

지금은 @RequestMapping annotation 을 사용해서 HTTP 의 어떤 타입의 요청을 처리할지 mehotd 값을 지정해 주었지만 한가지 타입의 요청만 처리하도록 한다면 @GetMapping 과 같은 다른 annotation 을 사용해서 코드를 간결하게 작성할 수 있다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

@GetMapping 을 포함하여 @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping 등이 있다.

그리고 이 모든 HTTP request method 에 매칭하는 annotation 은 @RequestMapping annotation 을 meta annotation 으로 사용하고 있다.

@GetMapping 을 대표로 문서를 보면 @RequestMapping(method = RequestMethod.GET) 의 축약 표현이라고 정의되어 있다.

 

추가로 @RequstMapping 은 method 가 아닌 class 레벨에 작성할 수도 있다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping(method = RequestMethod.GET)
public class MyHelloController {

    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello";
    }

    @PostMapping
    @ResponseBody
    public String hi() {
        return "hi";
    }

}

 

class 레벨에 @ReauestMapping 으로 GET 요청만 받을 수 있도록 정의 했기 때문에 이 안에 작성된 모든 handler 는 GET 요청만 처리할 수 있게 된다.

실제로 그런지 궁금해서 일부러 @PostMapping 을 추가하고 테스트 코드를 작성해봤다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(get("/hello")).andExpect(status().isOk());                 // 200
        mockMvc.perform(post("/hello")).andExpect(status().isMethodNotAllowed());  // 405
        mockMvc.perform(get("/hi")).andExpect(status().isNotFound());              // 404
        mockMvc.perform(post("/hi")).andExpect(status().isNotFound());             // 404

    }

}

 

특이했던건 @PostMapping 을 정의한 handler 에 대해서 405 가 아닌 404 응답을 받았다는 점이다.

 

 


 

 

HTTP request 를 handler 에 매핑할 때 uri 가 완전히 동일한 handler 만 사용했는데 패턴을 사용하는 방법이 있다.

패턴은 크게 다음 세 가지를 사용할 수 있다.

 

? 길이 1의 문자 필수 /hello? /hello?? /hello/? /hello/?? /hello?/?/? /hello/?test?
* 길이 0 이상의 문자열 /hello* /hello/* /hello/*/test /hello/*/te*st
** 하위 모든 path /hello/** /hello/**/test

 

사용하기에 따라 굉장히 복잡한 규칙을 만들 수 있겠지만 그런 경우가 얼마나 있을까 싶은 생각이다.

 

package me.nimkoes;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyHelloController {

    @GetMapping("/hello?")
    public String hello_case_1() { return "hello case 1"; }

    @GetMapping("/hello/?")
    public String hello_case_2() { return "hello case 2"; }

    @GetMapping("/hello/??")
    public String hello_case_3() { return "hello case 3"; }

    @GetMapping("/hello/?/?")
    public String hello_case_4() { return "hello case 4"; }

    @GetMapping("/hello*")
    public String hello_case_5() { return "hello case 5"; }

    @GetMapping("/hello/*/test")
    public String hello_case_6() { return "hello case 6"; }

    @GetMapping("/hello/*/te*st")
    public String hello_case_7() { return "hello case 7"; }

    @GetMapping("/hello/**")
    public String hello_case_8() { return "hello case 8"; }

    @GetMapping("/hello/**/test")
    public String hello_case_9() { return "hello case 9"; }

}

 

어떻게 사용할 수 있는지 테스트 해보기 위해 몇가지 경우만 만들어 보았다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(get("/hello1"))                    .andExpect(content().string("hello case 1"));
        mockMvc.perform(get("/hello!"))                    .andExpect(content().string("hello case 1"));
        mockMvc.perform(get("/hello/z"))                   .andExpect(content().string("hello case 2"));
        mockMvc.perform(get("/hello/nk"))                  .andExpect(content().string("hello case 3"));
        mockMvc.perform(get("/hello/n/k"))                 .andExpect(content().string("hello case 4"));
        mockMvc.perform(get("/hello"))                     .andExpect(content().string("hello case 5"));
        mockMvc.perform(get("/helloTest"))                 .andExpect(content().string("hello case 5"));
        mockMvc.perform(get("/hello/nimkoes/test"))        .andExpect(content().string("hello case 6"));
        mockMvc.perform(get("/hello/nk/te_some_st"))       .andExpect(content().string("hello case 7"));
        mockMvc.perform(get("/hello/nk/some"))             .andExpect(content().string("hello case 8"));
        mockMvc.perform(get("/hello/nk/some/another"))     .andExpect(content().string("hello case 8"));
        mockMvc.perform(get("/hello/nk/some/another/test")).andExpect(content().string("hello case 9"));

    }

}

 

uri 패턴 매핑 방법 중에 확장자를 매핑하는 방법도 있다.

이 방법은 요청 uri 가 특정 파일의 확장자를 포함하는 경우인데 /hello.* 와 같은 uri 패턴을 사용할 경우 /hello.json, /hello.zip 등의 uri 를 사용할 수 있는 것을 뜻한다.

 

하지만 확장자를 사용한 uri 패턴을 사용할 경우 RFD 공격과 같은 보안 이슈가 있기 때문에 권장하는 방법이 아니며, Spring Boot 에서는 이런 uri 의 요청을 사용하지 못하도록 막고 있다.

 

 


 

@RequestMapping 또는 @RequestMapping 을 meta annotation 으로 사용하는 @GetMapping 과 같은 annotation 을 조금 더 자세히 보면 consumes 와 produces 라는 값을 설정할 수 있다.

 

// 생략...

package org.springframework.web.bind.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

// 생략...

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {

    // 생략...

    /**
     * The consumable media types of the mapped request, narrowing the primary mapping.
     * <p>The format is a single media type or a sequence of media types,
     * with a request only mapped if the {@code Content-Type} matches one of these media types.
     * Examples:
     * <pre class="code">
     * consumes = "text/plain"
     * consumes = {"text/plain", "application/*"}
     * </pre>
     * Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
     * all requests with a {@code Content-Type} other than "text/plain".
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings override
     * this consumes restriction.
     * @see org.springframework.http.MediaType
     * @see javax.servlet.http.HttpServletRequest#getContentType()
     */
    String[] consumes() default {};

    /**
     * The producible media types of the mapped request, narrowing the primary mapping.
     * <p>The format is a single media type or a sequence of media types,
     * with a request only mapped if the {@code Accept} matches one of these media types.
     * Examples:
     * <pre class="code">
     * produces = "text/plain"
     * produces = {"text/plain", "application/*"}
     * produces = MediaType.APPLICATION_JSON_UTF8_VALUE
     * </pre>
     * <p>It affects the actual content type written, for example to produce a JSON response
     * with UTF-8 encoding, {@link org.springframework.http.MediaType#APPLICATION_JSON_UTF8_VALUE} should be used.
     * <p>Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
     * all requests with a {@code Accept} other than "text/plain".
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings override
     * this produces restriction.
     * @see org.springframework.http.MediaType
     */
    String[] produces() default {};

}

 

consumes 와 produces 모두 header 의 Mediatype 을 참조해서 handler 를 매핑하는데 사용한다.

차이가 있다면 consumes 는 HTTP 요청의 Content-Tpye 을 사용하고 produces 는 Accept 를 사용 한다.

 

package me.nimkoes;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping(value = "/hello", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

HTTP GET 요청을 받는 /hello handler method 는 consumes 로 APPLICATION_JSON_UTF8_VALUE 를 사용하고 있다.

사실 MediaType 타입의 값 대신 문자열을 사용해도 되지만 오타가 있을 경우 찾아내기 매우 어려울 수 있기 때문에 되도록 정의 된 상수를 사용하는걸 권장한다.

 

APPLICATION_JSON_UTF8_VALUE 는 "application/json;charset=UTF-8" 값이 할당되어 있다.

 

그래서 이 handler 는 HTTP GET 방식의 uri 가 /hello 여야 하기도 하지만, Content-Type 이 json 형식이라고 명시되어 있어야 매핑할 수 있다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(
                get("/hello")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("hello"))
        ;

    }

}

 

contentType 을 지정해주지 않거나 json 형식이 아닌 경우 테스트는 실패한다.

지금은 contentType 을 올바르게 설정 했기 때문에 테스트는 성공한다.

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {}
          Headers = {Content-Type=[application/json;charset=UTF-8]}
             Body = null
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public java.lang.String me.nimkoes.MyHelloController.hello()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]}
     Content type = text/plain;charset=UTF-8
             Body = hello
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

HTTP request 를 보낼 때 header 에 내가 보내는 데이터의 형식이 무엇인지 알려주는 Content-Type 뿐만 아니라, 요청에 대한 응답 데이터의 형식이 어떤 형식이기를 바란다는 의미로 Accept 값을 설정할 수 있다.

그리고 handler 에도 HTTP request 의 Accept 에 매칭하는 값으로 produces 값을 설정할 수 있는데, 이 handler 의 응답 데이터 형식이 무엇인지 명시할 수 있다.

 

package me.nimkoes;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping(
        value = "/hello",
        consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,
        produces = MediaType.TEXT_PLAIN_VALUE
    )
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

이 handler 의 응답 데이터 형식을 "text/plain" 이라고 명시해 주었기 때문에 HTTP request 의 Accept 값이 다른 값인 경우 오류가 발생 한다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(
                get("/hello")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("hello"))
        ;

    }

}

 

28라인에서 차라리 accept 를 설정하지 않았다면 테스트는 오히려 성공 했을 텐데, 이 요청에 대한 응답 결과 json 형식의 데이터를 기대한다고 명시했기 때문에 406 Not Acceptable 오류가 발생 한다.

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {}
          Headers = {Content-Type=[application/json]Accept=[application/json]}
             Body = <no character encoding set>
    Session Attrs = {}

생략...

MockHttpServletResponse:
           Status = 406
    Error message = null
          Headers = {}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

생략...

java.lang.AssertionError: Status 
Expected :200
Actual   :406

 

정리하면 @RequestMapping 또는 이 annotation 을 meta annotation 으로 사용하는 @GetMapping, @PostMapping 등을 사용할 때 consumes 와 produces 값을 추가로 설정할 수 있는데,

consumes 는 HTTP request header 의 Content-Type 을 참조하고, produces 는 HTTP request header 의 Accept 값을 참조 한다.

 

Content-Type 이 일치하지 않는 경우 415 Unsupported Media Type 오류가 발생하고

Accept 값이 일치하지 않는 경우 406 Not Acceptable 오류가 발생 한다.

 

 


 

@RequestMapping 에 설정할 수 있는 값으로 headers 가 있다.

 

// 생략...

package org.springframework.web.bind.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

// 생략...

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {

    // 생략...

    /**
     * The headers of the mapped request, narrowing the primary mapping.
     * <p>Same format for any environment: a sequence of "My-Header=myValue" style
     * expressions, with a request only mapped if each such header is found
     * to have the given value. Expressions can be negated by using the "!=" operator,
     * as in "My-Header!=myValue". "My-Header" style expressions are also supported,
     * with such headers having to be present in the request (allowed to have
     * any value). Finally, "!My-Header" style expressions indicate that the
     * specified header is <i>not</i> supposed to be present in the request.
     * <p>Also supports media type wildcards (*), for headers such as Accept
     * and Content-Type. For instance,
     * <pre class="code">
     * &#064;RequestMapping(value = "/something", headers = "content-type=text/*")
     * </pre>
     * will match requests with a Content-Type of "text/html", "text/plain", etc.
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings inherit
     * this header restriction (i.e. the type-level restriction
     * gets checked before the handler method is even resolved).
     * @see org.springframework.http.MediaType
     */
    String[] headers() default {};

    // 생략...

}

 

이 값은 HTTP request 에 특정한 header 가 있는 경우 또는 그 header 의 값이 특정한 값인 경우에만 handler 를 매핑해주고 싶을 때 사용할 수 있다.

 

package me.nimkoes;

import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping(value = "/hello", headers = HttpHeaders.AUTHORIZATION)
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

이 경우 HTTP request header 에 AUTHORIZATION 이 있는 경우 매핑될 수 있다.

HttpHeaders 는 header 로 사용할 수 있는 문자열을 정의 한 클래스이다.

 

 

테스트 코드를 작성할 때 보면 headers 로 key-value 쌍으로 설정할 수 있다.

현재 AUTHORIZATION 이 있을 경우에 handler 에 매핑될 수 있기 때문에 없는 경우와 있는 경우 모두 테스트 해보았다.

 

headers 를 아무것도 설정하지 않았을 때는 404 오류가 발생했다.

 

java.lang.AssertionError: Status 
Expected :200
Actual   :404

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(
                get("/hello")
                .header(HttpHeaders.AUTHORIZATION,"1234")
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("hello"))
        ;

    }

}

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {}
          Headers = {Authorization=[1234]}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public java.lang.String me.nimkoes.MyHelloController.hello()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]}
     Content type = text/plain;charset=UTF-8
             Body = hello
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

HTTP request header 에 특정한 key 가 있는 경우에만 매핑할 뿐만 아니라 그 값에 대해서도 검사할 수 있다.

예를 들어 지금 AUTHORIZATION 값으로 "1234" 를 설정 했는데, @GetMapping 에서도 이 값이 올바른 경우에만 매핑해줄 수 있다.

 

package me.nimkoes;

import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping(value = "/hello", headers = HttpHeaders.AUTHORIZATION + "=" + "4321")
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

handler 에 설정한 headers 를 보면 HttpHeaders.AUTHORIZATION 의 값이 "4321" 인 경우에만 매핑하도록 설정했기 때문에 테스트를 다시 실행해보면 실패한다.

이 경우에도 앞서 AUTHORIZATION 이 없었을 때와 마찬가지로 404 오류를 발생한다.

테스트 코드에서 AUTHORIZATION 값을 "4321" 로 수정한 다음 실행하면 테스트가 성공한다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(
                get("/hello")
                .header(HttpHeaders.AUTHORIZATION,"4321")
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("hello"))
        ;

    }

}

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {}
          Headers = {Authorization=[4321]}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public java.lang.String me.nimkoes.MyHelloController.hello()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]}
     Content type = text/plain;charset=UTF-8
             Body = hello
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

headers 설정과 관련하여 특정 header 가 있는 경우와 그 header 의 값이 특정한 값인 경우에 매핑하는 방법에 대해 알아보았다.

반대로 특정 header 가 없는 경우에 매핑하고 싶은 경우 ! 연산자를 사용해서 설정할 수 있다.

 

package me.nimkoes;

import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping(value = "/hello", headers = HttpHeaders.AUTHORIZATION + "=" + "4321")
    @ResponseBody
    public String hello() {
        return "hello";
    }

    @GetMapping(value = "/hi", headers = "!" + HttpHeaders.AUTHORIZATION)
    @ResponseBody
    public String hi() {
        return "hi";
    }

}

 

새로운 handler 를 정의 하였고, 이 handler 는 HttpHeaders.AUTHORIZATION 이 없는 경우 매핑되도록 설정 했다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(
                get("/hello")
                .header(HttpHeaders.AUTHORIZATION,"4321")
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("hello"))
        ;

        mockMvc.perform(
                get("/hi")
                .header(HttpHeaders.AUTHORIZATION,"1234")
            )
            .andDo(print())
            .andExpect(status().isNotFound())
        ;

        mockMvc.perform(get("/hi"))
            .andDo(print())
            .andExpect(status().isOk())
        ;

    }

}

 

지금까지 header 에 특정한 key 가 있어야 하는 경우, 있을 때 특정한 값이어야 하는 경우, 특정한 key 가 없어야 하는 경우에 대해 정리해 보았다.

추가로 parameter 에 대해서도 header 를 설정한 것과 매우 유사하게 사용할 수 있는데, 단순히 headers 대신 params 를 사용하면 된다.

 

    @GetMapping(value = "/helloParam", params = "test")
    @ResponseBody
    public String helloParam() {
        return "hello";
    }

 

HTTP request 를 이 handler 에 매핑하기 위해서는 "test" 라는 param 이 있어야 한다.

 

        mockMvc.perform(
                get("/helloParam")
                .param("test", "1234")
            )
            .andDo(print())
            .andExpect(status().isOk())
        ;

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /helloParam
       Parameters = {test=[1234]}
          Headers = {}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public java.lang.String me.nimkoes.MyHelloController.helloParam()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]}
     Content type = text/plain;charset=UTF-8
             Body = hello
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

headers 에서와 마찬가지로 params 도 특정 param 이 없어야 하거나, 특정 값이어야 하는 경우 설정 방법이 동일하기 때문에 굳이 반복해서 정리하지 않기로 했다.

 

 


 

 

Spring Web MVC 를 사용하면 만들지 않아도 HTTP HEAD 와 OPTIONS 요청을 처리할 수 있다.

HTTP HEAD 명세HTTP OPTIONS 명세를 한 번 보면 좋을것 같다.

 

MyHelloController 에 GET 과 POST 요청을 받을 수 있도록 handler 를 수정했다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @RequestMapping(value = "/hello", method = {RequestMethod.GET, RequestMethod.POST})
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

 

HTTP HEAD 와 OPTIONS 요청을 처리하도록 명시하지 않았지만, Spring Web MVC 를 사용하기 때문에 기본적으로 사용할 수 있다.

정말로 그런지 HEAD 와 OPTIONS 요청에 대한 결과를 테스트 코드를 통해 확인해 봤다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.head;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(head("/hello"))
            .andDo(print())
            .andExpect(status().isOk())
        ;

        mockMvc.perform(options("/hello"))
            .andDo(print())
            .andExpect(status().isOk())
        ;

    }
}

 

 

MockHttpServletRequest:
      HTTP Method = HEAD
      Request URI = /hello
       Parameters = {}
          Headers = {}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public java.lang.String me.nimkoes.MyHelloController.hello()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]}
     Content type = text/plain;charset=UTF-8
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

MockHttpServletRequest:
      HTTP Method = OPTIONS
      Request URI = /hello
       Parameters = {}
          Headers = {}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping$HttpOptionsHandler
           Method = public org.springframework.http.HttpHeaders org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping$HttpOptionsHandler.handle()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Allow=[GET,HEAD,POST,OPTIONS]}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

이곳을 통해 각 HTTP method 에 대한 명세를 확인할 수 있다.

 

 

 


 

 

마지막으로 커스텀 애노테이션을 만들어서 요청을 매핑하는 방법이 있다.

annotation 중에 다른 annotation 들을 조합해서 만든 annotation 을 composed annotation 이라고 한다.

@GetMapping 역시 composed annotation 이다.

 

// 생략...

package org.springframework.web.bind.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

// 생략...

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {

    // 생략...

}

 

 

그리고 @Target, @Retention, @Documented, @RequestMapping 과 같이 다른 annotation 에 정의해서 사용할 수 있는 것을 meta annotation 이라고 한다.

 

uri 가 /hello 인 HTTP GET 요청을 처리하는 handler 라고 알려줄 수 있는 annotation 을 만들어 봤다.

 

 

GetHelloMapping 이라는 이름으로 annotation 을 하나 만들었다.

 

 

package me.nimkoes;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public @interface GetHelloMapping {
}

 

 

 

 

 

새로 만든 annotation 을 controller 의 handler 에 적용했다.

 

package me.nimkoes;

import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyHelloController {

    @GetHelloMapping
    public String hello() {
        return "hello";
    }

}

 

새로 만든 annotation 을 사용해서 handler 매핑이 잘 되는지 확인해보기 위해 테스트 코드를 만들어 실행해 봤다.

 

package me.nimkoes;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {

        mockMvc.perform(get("/hello"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("hello"))
        ;

    }
}

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {}
          Headers = {}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public java.lang.String me.nimkoes.MyHelloController.hello()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]}
     Content type = text/plain;charset=UTF-8
             Body = hello
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

Java annotation 에 대해 예전에 정리했던 글을 링크로 남겨둔다.

 

 

 

 

728x90