Study/Spring

[스프링 웹 MVC 활용] 01. HTTP Request 맵핑하기

going.yoon 2022. 3. 1. 19:38

스프링 MVC 의 핵심 기술이었던 DispatcherServlet이 핸들러를 찾아서 요청들을 수행하는지 실제로 활용을 해보자.

 

I. HTTP Method로 맵핑하기

첫번째로 HttpRequest를 처리하는 Handler를 작성하는 방법이다. Handler는 기본적으로 Controller Class 안에 메소드들이라고 생각하면 쉽다. 그리고 그 메소드에 어떤 HttpReqeust를 처리할 것인지, 어떤 값을 리턴해줄 것인지(뷰 모델인지, ResponseBody인지) 등을 설정해주면 DispatcherServlet의 HandlerAdaptor가 이를 처리해 줄 것이다.

@Controller
public class SampleController {

    @RequestMapping("/hello") 
    /*
    * #1. @RequestMapping :  기본적으로 HTTP Request가 다 Mapping이 된다.
    *  mockMvc.perform(get("/hello")) ,
    *  mockMvc.perform(put("/hello")) ,
    *  mockMvc.perform(post("/hello")) 등등 다 사용이 가능하다는 뜻.
    *
    * #2. GET만 허용하고 싶을 경우
    * @RequestMapping(value="/hello", method = RequestMethod.GET) 이렇게 한정해줘야 함.
    * @GetMapping("/hello") 이렇게 쓰거나.
    *
    * #3. @RequestMapping을 Class의 애노테이션으로도 설정 가능.
    * 하위 메소드들은 자동으로 Class 애노테이션 설정값을 따라감. 
    * */
    @ResponseBody // #4.  아래의 리턴값은 뷰모델이 아니라 응답 body안에 리턴해줄 데이터이다.라는 뜻
    public String hello(){
        return "hello";
    }
}

 

그리고 이를 테스트하는 테스트 코드를 작성해보자

@RunWith(SpringRunner.class) // test 전용 ApplicationContext를 제공
@WebMvcTest
public class SampleControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception { // 테스트를 할 때는 꼭 void형식으로 해줘야 한다.
        mockMvc.perform(get("/hello"))
                        .andDo(print())
                        .andExpect(status().isOk())
                        .andExpect(content().string("hello"));
    }

}

 

print가 호출되면서 콘솔에 찍힌 요청 및 응답값은 아래와 같다.

 

더보기

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {}
          Headers = []
             Body = null
    Session Attrs = {}

Handler: // 핸들러의 타입과 메소드가 잘 찍혔음
             Type = com.example.demowebmvc.SampleController 
           Method = com.example.demowebmvc.SampleController#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   // responseBody에 hello가 잘 셋팅되었다.
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

HTTP Method별 특징을 한번 알아보자.

1. GET

 - 클라이언트가 서버의 리소스를 요청할 때 사용한다.

 - 캐싱할 수 있다. (조건적인 GET으로 바뀔 수 있다. ex) modifiedWhen 등)

https://bohyeon-n.github.io/deploy/http/contditional-get-cache.html

 

[HTTP]conditional get과 Cache | 구보현 블로그

Conditional GET? 클라이언트는 이전에 한 번 요청해서 돌려받은 리소스에 대해 다시 한 번 요청을 할 때, 불필요한 트래픽을 줄이기 위해 해당 리소스가 변경된 경우에만 다시 보내달라고 요청할 수

bohyeon-n.github.io

 - 브라우저 기록에 남는다.

 - 북마크할 수 있다.

 - 민감한 데이터를 보낼 때 사용하면 안된다.

 - idempnent : 동일요청에 대해 동일 응답을 주어야 한다는 원칙

 

2. POST

- 리소스의 수정이나 생성에 대한 요청

- POST의 URI는 '데이터 자체'에 대한 요청이 아니라 '데이터 처리'에 대한 요청이기 때문에 idempotent하지 않음

- 캐시, 브라우징, 북마크 할 수 없음

- 데이터 길이 제한이 없음

 

3. PUT

- 리소스의 수정이나 생성에 대한 요청

- PUT의 URI는 '데이터 자체'에 대한 요청이기 때문에 idempotent함.

 

4. PATCH

- PUT과 비슷하지만, 기존 엔티티와 새 엔티티의 차이점만 송신

- PATCH의 URI는 '데이터 자체'에 대한 요청이기 때문에 idempotent함.

 

5. DELETE

- idempotent

 

 

 

II. URI 패턴 맵핑하기

두번째로는 URI 패턴을 맵핑하는 방법을 알아보자. URI는 URL을 포함하는 개념으로 여기서는 요청에서 사용되는 문자열정도..라고 생각하면 된다.

 

첫번째로, uri 는 아래와 같이 다양하게 지정해줄 수 있다.

@Controller
public class SampleController {

    //@GetMapping({"/hello" , "/hi"})  #1. 여러개의 uri 사용 가능
    //@GetMapping("/hello/?") #2. pattern으로 uri 지정 가능 ( 하나의 문자 )
    //@GetMapping("/hello/*") #2. pattern으로 uri 지정 가능 ( 여러개의 문자 )
    //@GetMapping("/hello/**") #2. pattern으로 uri 지정 가능 ( 뒤에 path depth가 몇개던 상관 없음 )
    //@GetMapping("/{name:[a-z]+}") #3. 정규식으로 uri 지정 가능
    @ResponseBody
    public String hello(){
        return "hello";
    }

}

 

그렇다면 요청에 해당하는 Mapping이 중복되는 경우에 어떤 Handler를 먼저 처리할까? 가장 비슷한 handler를 찾아온다.

@RunWith(SpringRunner.class) // test 전용 ApplicationContext를 제공
@WebMvcTest
public class SampleControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception { // 테스트를 할 때는 꼭 void형식으로 해줘야 한다.
        mockMvc.perform(get("/hello/gayoung"))
                        .andDo(print())
                        .andExpect(status().isOk())
                        .andExpect(handler().handlerType(SampleController.class))
        ;
    }

}


@Controller
@RequestMapping("/hello")
public class SampleController {

    @GetMapping("/gayoung") // 여기에 Mapping됨.
    @ResponseBody
    public String helloGY()  { return "hello Gayoung"; }

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

 

 

III. 미디어 타입 맵핑하기

세번째로 특정 타입에 대한 요청만 처리하는 핸들러를 알아보자. 

// #1. HTTP Request 를 생성하는 TestCode
@RunWith(SpringRunner.class) 
@WebMvcTest
public class SampleControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception { 
        mockMvc.perform(get("/hello")
        				
                        /** #1.1 요청과 응답시 원하는 미디어 타입을 지정해서 호출한다.*/
        
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        // contentType : 나는 어떤 형식으로 요청을 보낼꺼다
                        .accept(MediaType.APPLICATION_JSON))
                        // accept : 나는 어떤 형식의 응답을 원한다.
                        .andDo(print())
                        .andExpect(status().isOk())
                        .andExpect(handler().handlerType(SampleController.class))
        ;
    }

}

// #2. Handler가 구현되어있는 Controller
@Controller
public class SampleController {

    @RequestMapping(value="/hello"
            
             /** #2.1 Request의 contentType과 맵핑되어야 하는 consumes*/
            , consumes = MediaType.APPLICATION_JSON_UTF8_VALUE
            // consumes =  HTTP Request Header 안에 content-type = application/json;charset=UTF-8 이 포함된 요청만 핸들링하겠다.
            
            /** #2.2 Request의 accept와 맵핑되어야 하는 produces*/
            , produces = MediaType.TEXT_PLAIN_VALUE
            // produces = plainType의 Text만 리턴받는 요청만 처리하겠다.
            // 단 요청시 AcceptType지정을 안해주면 produce에서 뭘 지정해주던 다 요청 받음.
    )

    // # cf ) APPLICATION_JSON_UTF8는 return MediaType 객체를,
    // APPLICATION_JSON_UTF8_VALUE는 application/json;charset=UTF-8 문자열을 return한다.
   
   @ResponseBody
    public String helloGY()  { return "hello Gayoung"; }


}

 

IV. 헤더와 파라미터 맵핑

네번째로 특정한 헤더, 헤더 키, 매개변수, 매개변수 키값의 유무에 따른 헤더 맵핑방법을 알아보자.

@Controller
public class SampleController {

    //@GetMapping(value="/hello", headers= HttpHeaders.ACCEPT_LANGUAGE) : 특정 헤더가 있는 요청을 처리하고 싶은 경우
    //@GetMapping(value="/hello", headers= "!" + HttpHeaders.ACCEPT_LANGUAGE) : 특정 헤더가 없는 요청을 처리하고 싶은 경우
    //@GetMapping(value="/hello", headers=HttpHeaders.FROM + "=111") : 특정 헤더/키값이 있는 요청을 처리하고 싶은 경우
    //@GetMapping(value="/hello", params = "name") : 특정 파라미터가 있는 요청을 처리하고 싶은 경우
    //@GetMapping(value="/hello", params = "name=spring") : 특정 파라미터/키값이 있는 요청을 처리하고 싶은 경우
    @ResponseBody
    public String helloGY()  { return "hello Gayoung"; }


}

 

 

V. HEAD와 OPTIONS처리

스프링 MVC에서는 Get이나 Post와 같은 HTTP Method 중 우리가 구현하지 않아도 기본적으로 제공하는 메소드들이 있다. Head와 Options가 바로 그것이다. Head는 Get과 비슷하지만 응답 본문을 받아오지 않고 응답 헤더만을 가져오는 요청이다. 그렇기 때문에 Get만 구현해놓는다면 Head만 따로 구현하지 않아도 된다. Options는 해당 handler가 제공하는 기능을 보여준다. 

 

더보기

test 코드를 통해 options 요청을 보내보자

 

@Test
public void helloTest() throws Exception { // 테스트를 할 때는 꼭 void형식으로 해줘야 한다.
    mockMvc.perform(options("/hello"))
                    .andDo(print())
                    .andExpect(status().isOk())
    ;
}

 

위의 테스트 코드를 실행하면 콘솔창에 나타난 response값은 아래와 같다.

 

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Allow:"GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS", Accept-Patch:""]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

 

 

 

 

VI. 커스텀 애노테이션

맨 처음 살펴볼 때 @RequestMapping 애노테이션은 모든 HTTP Request를 처리할 수 있고, @GetMapping 애노테이션은 그 중에서도 Get Request만 처리한다고 했다. 이 @GetMapping은 RequestMapping을 커스터마이징한 조합(Composed) 애노테이션이라고 할 수 있다.

@GetMapping = @RequestMapping(method = RequestMethod.GET)

이런식으로 한개 혹은 여러개의 애노테이션을 조합해서 조합애노테이션을 생성할 수 있다.

아래의 코드를 살펴보자. GetHelloMapping이라는 애노테이션을 만들어주고, Controller에서 해당 애노테이션을 붙여주었다.

그런데 테스트 결과는? 404에러 발생.

// #1. GetHelloMapping이라는 애노테이션 클래스를 만들어준다.
@RequestMapping(method= RequestMethod.GET , value= "/hello")
@Retention(RetentionPolicy.CLASS)
/********Retention Policy***********/
public @interface GetHelloMapping {
}


// #2. Controller에서 해당 애노테이션을 사용해준다.
@Controller
public class SampleController {

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

 

이러한 에러가 발생하는 이유는 바로 애노테이션의 Retention 정책이 기본적으로 CLASS로 설정되어 있어 DispatcherServlet이 해당 애노테이션을 찾아오지 못하기 때문이다. 이는 자바가 컴파일할 때까지는 애노테이션이 유효하지만, 런타임시에 해당 애노테이션의 유효성이 사라지게 된다. 따라서 테스트가 실행되면서 해당 애노테이션을 찾지 못해 404가 발생하게 되는 것이다.

 

Retention Policy의 유형은 다음과 같다.

1. Source : 유효범위가 소스코드까지. 컴파일시 해당 애노테이션 정보 무효화

2. Class(Default) : 유효범위가 컴파일시점까지. 런타임시 클래스가 메모리에 로드되면 해당 애노테이션 정보 무효화

3. Runtime : 클래스가 메모리에 로드 된 이후에도 유효함.