[스프링 웹 MVC] 7. 스프링 MVC 활용 1부 (요청 맵핑하기)

165500 단어 SpringSpring

핵심 기술

  • 애노테이션 기반의 스프링 MVC
    • 💕 요청 맵핑하기
    • 💕 핸드러 메소드
    • 💕 모델과 뷰
    • 데이터 바인더 - 프로퍼티 에디터, 포매터, 컨버터
    • 예외 처리
    • 글로벌 컨트롤러 - 모든 컨트롤러에 공통으로 적용되는 컨트롤러
  • 사용할 기술
    • 스프링 부트
    • 스프링 웹 MVC
    • 타임리프
  • ✨학습할 애노테이션
    • @RequestMapping
      • @GetMapping, @PostMapping, @PutMapping
    • @ModelAttribute
    • @RequestParam, @RequestHeader
    • @PathVariable, @MatrixVatiable
    • @SessionAttribute, @RequestAttribute, @CookieValue
    • @Valid
    • @RequestBody, @ResponseBody
    • @ExceptionHandler
    • @ControllerAdvice
  • 프로젝트 생성
    • Spring Initializr
    • 의존성 - Web, thymeleaf 추가

HTTP Method

  • HTTP Method

    • GET, POST, PUT, PATCH, DELETE ...
  • SampleController

    package me.jinmin.demowebmvc;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    @Controller
    public class SampleController {
    
        //@RequestMapping(value = "/hello", method = {RequestMethod.GET, RequestMethod.PUT})
        //@RequestMapping(value = "/hello", method = RequestMethod.GET)
        //@GetMapping("/hello") //위와 같음
        @RequestMapping("/hello") // url을 통해 맵핑
        @ResponseBody // 문자를 응답으로 나타내고자 할 때.
        public String hello(){
            return "hello"; //해당 String에 해당하는 뷰 이름을 찾아간다.
        }
    }
    • (1 테스트) 일반적으로 Method를 지정하지 않고 @RequestMapping만 명시하면 모든 HTTP Method를 다 허용한다.
    • (2 테스트) @RequestMapping(value = "/hello", method = RequestMethod.GET) : GET 방식만 허용한다
    • (3) @RequestMapping(value = "/hello", method = {RequestMethod.GET, RequestMethod.PUT}) : GET, PUT 방식을 허용한다. (테스트는 안 한다!)
  • (1) 테스트 SampleControllerTest

    package me.jinmin.demowebmvc;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.test.web.servlet.MockMvc;
    
    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;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    @AutoConfigureMockMvc
    public class SampleControllerTest {
    
        @Autowired
        MockMvc mockMvc; //스프링 부트 테스트 객체
    
        @Test
        public void helloTest() throws Exception {
            mockMvc.perform(get("/hello")) //GET 방식으로 요청 (post, put, delete 등 모두 가능)
                    .andDo(print()) //Log 내용
                    .andExpect(status().isOk()) //상태 이상 체크
                    .andExpect(content().string("hello")); //내용에 "hello"가 있는지 유무
        }
    }
    • 테스트 결과 (Log)

      MockHttpServletRequest:
            **HTTP Method = GET
            Request URI = /hello**
             Parameters = {}
                Headers = []
                   Body = null
          Session Attrs = {}
      
      Handler:
                   Type = me.jinmin.demowebmvc.SampleController
                 Method = me.jinmin.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
          Forwarded URL = null
         Redirected URL = null
                Cookies = []
  • (2) 테스트

    package me.jinmin.demowebmvc;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.test.web.servlet.MockMvc;
    
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    @AutoConfigureMockMvc
    public class SampleControllerTest {
    
        @Autowired
        MockMvc mockMvc;
    
        @Test
        public void helloTest() throws Exception {
            mockMvc.perform(post("/hello"))
                    .andDo(print())
                    .andExpect(status().isMethodNotAllowed());
        }
    }
    • 테스트 결과 (Log)

      MockHttpServletRequest:
            HTTP Method = POST
            Request URI = /hello
             Parameters = {}
                Headers = []
                   Body = null
          Session Attrs = {}
      
      Handler:
                   Type = null
      
      Async:
          Async started = false
           Async result = null
      
      Resolved Exception:
                   Type = org.springframework.web.HttpRequestMethodNotSupportedException
      
      ModelAndView:
              View name = null
                   View = null
                  Model = null
      
      FlashMap:
             Attributes = null
      
      MockHttpServletResponse:
                 Status = 405
          Error message = Request method 'POST' not supported
                Headers = [Allow:"GET"]
           Content type = null
                   Body = 
          Forwarded URL = null
         Redirected URL = null
                Cookies = []
  • ⚾ GET (조회)

    • C → S 리소스 요청
    • 캐싱 가능(조건적인 GET)
    • 브라우저 기록 남음
    • 북마크 가능
    • 민감한 데이터 전송 x (URL에 표기)
    • idempotent : 멱등, 동일한 GET 요청은 동일한 응답을 리턴
  • ⚾ POST (생성)

    • C → S 리소스 새로 생성, 수정
    • 서버에 보내는 데이터를 POST 요청 본문에 담는다.
    • 캐싱 x
    • 브라우저 기록 x
    • 북마크 x
    • 데이터 길이 제한 x
  • ⚾ PUT (수정)

    • URI에 해당하는 데이터를 새로 만들거나 수정
    • POST와 차이점
      • POST의 URI는 보내는 데이터를 처리할 리소스를 지칭 : 같은 요청을 2번 보내면 매 번 새로운 2번의 응답 생성
      • PUT의 URI는 보내는 데이터에 해당하는 리소스를 지칭 : 같은 요청을 2번 보내면 새로운 응답 1번 생성하고 그 뒤로 응답 x
    • Idempotent
  • ⚾ PATCH

    • PUT과 비슷, 기존 엔티티와 새로운 데이터의 차이점만 보낸다
    • Idempotent
  • ⚾DELETE

    • URI에 해당하는 리소스를 삭제
    • Idempotent

URI 패턴

  • URI, URL, URN

    • URI : Uniform Resource Identifier
    • URL : Uniform Resource Location
    • URN : Uniform Resource Name
  • SampleController

    package me.jinmin.demowebmvc;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    @Controller
    public class SampleController {
    
        @GetMapping({"/hello", "/hi"})
        @ResponseBody
        public String hello(){
            return "hello";
        }
    }
  • SampleControllerTest

    package me.jinmin.demowebmvc;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.test.web.servlet.MockMvc;
    
    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.status;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    @AutoConfigureMockMvc
    public class SampleControllerTest {
    
        @Autowired
        MockMvc mockMvc;
    
        @Test
        public void helloTest() throws Exception {
            mockMvc.perform(get("/hello"))
                    .andDo(print())
                    .andExpect(status().isOk());
    
            mockMvc.perform(get("/hi"))
                    .andDo(print())
                    .andExpect(status().isOk());
        }
    }
    • 테스트 성공
  • 요청 식별자로 맵핑하기

    • @RequestMapping은 아래와 같은 패턴 지원
      • ? : 한 글자 (“/author/???” ⇒ “/author/123”)
      • * : 여러 글자 (“/author/*” ⇒ “/author/jinmin”)
      • ** : 여러 패스 (“/author/** ⇒ “/author/jinmin/book”)
  • 클래스에 선언한 @RequestMapping과 조합

    • 클래스에 선언한 URI 패턴뒤에 이어 붙여서 맵핑

      package me.jinmin.demowebmvc;
      
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.ResponseBody;
      
      @Controller
      @RequestMapping("/hi")
      public class SampleController {
      
          @GetMapping("/hello")
          @ResponseBody
          public String hello(){
              return "hello";
          }
          
          @GetMapping("/hello2")
          @ResponseBody
          public String hello2(){
              return "hello2";
          }
      }
      서버주소/hi/hello
      서버주소/hi/hello2
      (가능)
  • 정규 표현식 맵핑 : @PathVariable 사용

    • SampleController

      package me.jinmin.demowebmvc;
      
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.PathVariable;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.ResponseBody;
      
      @Controller
      @RequestMapping("/hello")
      public class SampleController {
      
          @GetMapping("/{name:[a-z]+}")
          @ResponseBody
          public String hello(@PathVariable String name){
              return "hello " + name;
          }
      }
    • SampleControllerTest

      package me.jinmin.demowebmvc;
      
      import org.junit.Test;
      import org.junit.runner.RunWith;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.test.context.junit4.SpringRunner;
      import org.springframework.test.web.servlet.MockMvc;
      
      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;
      
      @RunWith(SpringRunner.class)
      @SpringBootTest
      @AutoConfigureMockMvc
      public class SampleControllerTest {
      
          @Autowired
          MockMvc mockMvc;
      
          @Test
          public void helloTest() throws Exception {
              mockMvc.perform(get("/hello/jinmin"))
                      .andDo(print())
                      .andExpect(status().isOk())
                      .andExpect(content().string("hello jinmin"));
          }
      }
      • 테스트 성공
      • IF, mockMvc.perform(get("/hello/123456")) ⇒ 실패, 문자가 아닌 숫자가 왔기 때문에.
  • 중복 패턴?

    package me.jinmin.demowebmvc;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    @Controller
    @RequestMapping("/hello")
    public class SampleController {
    
        @GetMapping("/jinmin")
        @ResponseBody
        public String helloJinmin(){
            return "hello jinmin!";
        }
    
        @GetMapping("/*")
        @ResponseBody
        public String hello(){
            return "hello jinmin";
        }
    }
    • 서버 주소/hello/jinmin 확인, 결과는 "hello jinmin!"
    • 가장 구체적으로 맵핑되는 핸들러를 선택
  • URI 확장자 맵핑 지원

    • 권장 x (스프링 부트에서 기본적으로 해당 기능을 사용하지 않도록 설정)
      • 보안 이슈 (Reflected File Download Attack)
      • URI 변수, Path 매개변수, URI 인코딩 사용 시 불명확

미디어 타입

  • 특정한 타입의 데이터를 담고 있는 요청만 처리하는 핸들러

    • Json을 보내는 요청만 처리하겠다. = @RequestMapping(consumes=MediaType.APPLICATION_JSON_UTF8_VALUE)

    • Content-Type 헤더로 필터링

    • 오류 예제)

      • SampleController

        package me.jinmin.demowebmvc;
        
        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 SampleController {
        
            @GetMapping(value = "/hello", consumes = MediaType.APPLICATION_JSON_VALUE)
            @ResponseBody
            public String hello(){
                return "hello";
            }
        }
      • SampleControllerTest

        package me.jinmin.demowebmvc;
        
        import org.junit.Test;
        import org.junit.runner.RunWith;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
        import org.springframework.boot.test.context.SpringBootTest;
        import org.springframework.test.context.junit4.SpringRunner;
        import org.springframework.test.web.servlet.MockMvc;
        
        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;
        
        @RunWith(SpringRunner.class)
        @SpringBootTest
        @AutoConfigureMockMvc
        public class SampleControllerTest {
        
            @Autowired
            MockMvc mockMvc;
        
            @Test
            public void helloTest() throws Exception {
                mockMvc.perform(get("/hello"))
                        .andDo(print())
                        .andExpect(status().isOk())
                        .andExpect(content().string("hello"));
            }
        }
      • 결과

        java.lang.AssertionError: Status 
        Expected :200
        Actual   :415 (Unsupported Media Type)
    • 성공 예제)

      • SampleControllerTest (테스트만 변경)

        package me.jinmin.demowebmvc;
        
        import org.junit.Test;
        import org.junit.runner.RunWith;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
        import org.springframework.boot.test.context.SpringBootTest;
        import org.springframework.http.MediaType;
        import org.springframework.test.context.junit4.SpringRunner;
        import org.springframework.test.web.servlet.MockMvc;
        
        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;
        
        @RunWith(SpringRunner.class)
        @SpringBootTest
        @AutoConfigureMockMvc
        public class SampleControllerTest {
        
            @Autowired
            MockMvc mockMvc;
        
            @Test
            public void helloTest() throws Exception {
                mockMvc.perform(get("/hello")
                        .contentType(MediaType.APPLICATION_JSON_VALUE))
                            .andDo(print())
                            .andExpect(status().isOk())
                            .andExpect(content().string("hello"));
            }
        }
      • 결과 : 성공

  • 특정한 타입의 응답을 만드는 핸들러

    • Json만을 받는 응답 처리 = @RequestMapping(produces = "application/json")

    • Accept 헤더로 필터링 (⇒ Accept 헤더가 없더라도 테스트는 성공. Why? "어떠한 타입의 핸들러도 받아들인다" 라는 의미로 간주된다. 다만, Accept 헤더를 틀리게 명시하면 오류)

    • 오류 예제)

      • SampleController

        package me.jinmin.demowebmvc;
        
        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 SampleController {
        
            @GetMapping(value = "/hello", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
            @ResponseBody
            public String hello(){
                return "hello";
            }
        }
      • SampleControllerTest

        package me.jinmin.demowebmvc;
        
        import org.junit.Test;
        import org.junit.runner.RunWith;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
        import org.springframework.boot.test.context.SpringBootTest;
        import org.springframework.http.MediaType;
        import org.springframework.test.context.junit4.SpringRunner;
        import org.springframework.test.web.servlet.MockMvc;
        
        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;
        
        @RunWith(SpringRunner.class)
        @SpringBootTest
        @AutoConfigureMockMvc
        public class SampleControllerTest {
        
            @Autowired
            MockMvc mockMvc;
        
            @Test
            public void helloTest() throws Exception {
                mockMvc.perform(get("/hello")
                        .contentType(MediaType.APPLICATION_JSON_VALUE)
                        .accept(MediaType.APPLICATION_JSON_VALUE))
                            .andDo(print())
                            .andExpect(status().isOk())
                            .andExpect(content().string("hello"));
            }
        }
      • 결과

        java.lang.AssertionError: Status 
        Expected :200
        Actual   :406 (Not Acceptable)
    • 성공 예제)

      • SampleControllerTest

        package me.jinmin.demowebmvc;
        
        import org.junit.Test;
        import org.junit.runner.RunWith;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
        import org.springframework.boot.test.context.SpringBootTest;
        import org.springframework.http.MediaType;
        import org.springframework.test.context.junit4.SpringRunner;
        import org.springframework.test.web.servlet.MockMvc;
        
        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;
        
        @RunWith(SpringRunner.class)
        @SpringBootTest
        @AutoConfigureMockMvc
        public class SampleControllerTest {
        
            @Autowired
            MockMvc mockMvc;
        
            @Test
            public void helloTest() throws Exception {
                mockMvc.perform(get("/hello")
                        .contentType(MediaType.APPLICATION_JSON_VALUE)
                        .accept(MediaType.TEXT_PLAIN_VALUE))
                            .andDo(print())
                            .andExpect(status().isOk())
                            .andExpect(content().string("hello"));
            }
        }
  • 클래스에 선언한 consumes, produces 등은 메소드에서 잡아먹는다. (⇒ 메소드가 설정한 것이 실행된다. 오버라이딩)

  • 🔔내 식대로 결론

    • Content-Type : 요청 형식이 이렇다
    • Accept : 응답 형식이 이렇다.
    • 문자열 말고 MediaType을 이용하자. (편하니까)

헤더와 매개변수

  • 특정 헤더가 있는 요청을 처리하고 싶을 때 (headers)

    • 예제)

      package me.jinmin.demowebmvc;
      
      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 SampleController {
      
          @GetMapping(value = "/hello", headers = HttpHeaders.FROM)
          //@GetMapping(value = "/hello", headers = HttpHeaders.AUTHORIZATION) //오류 : HttpHeaders.AUTHORIZATION 헤더가 없음
          //@GetMapping(value = "/hello", headers = "!" + HttpHeaders.FROM) //오류 : HttpHeaders.FROM 헤더가 존재
          //@GetMapping(value = "/hello", headers = HttpHeaders.FROM + "=" + "111") //오류 : 요청 HttpHeaders.FROM 헤더의 값이 "localhost"이기 때문에
          @ResponseBody
          public String hello(){
              return "hello";
          }
      }
      package me.jinmin.demowebmvc;
      
      import org.junit.Test;
      import org.junit.runner.RunWith;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.http.HttpHeaders;
      import org.springframework.test.context.junit4.SpringRunner;
      import org.springframework.test.web.servlet.MockMvc;
      
      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;
      
      @RunWith(SpringRunner.class)
      @SpringBootTest
      @AutoConfigureMockMvc
      public class SampleControllerTest {
      
          @Autowired
          MockMvc mockMvc;
      
          @Test
          public void helloTest() throws Exception {
              mockMvc.perform(get("/hello")
                          .header(HttpHeaders.FROM, "localhost"))
                          .andDo(print())
                          .andExpect(status().isOk())
                          .andExpect(content().string("hello"));
          }
      }
  • 특정 요청 매개변수를 갖는 요청을 처리하고 싶을 때 (params)

    • 예제)

      package me.jinmin.demowebmvc;
      
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.ResponseBody;
      
      @Controller
      public class SampleController {
      
          @GetMapping(value = "/hello", params = "name")
          //@GetMapping(value = "/hello", params = "name=jinmin") //오류 : value 값이 맵핑이 안된다.
          //@GetMapping(value = "/hello", params = "!name") //오류 : name이라는 param이 있기 때문에
          @ResponseBody
          public String hello(){
              return "hello";
          }
      }
      package me.jinmin.demowebmvc;
      
      import org.junit.Test;
      import org.junit.runner.RunWith;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.test.context.junit4.SpringRunner;
      import org.springframework.test.web.servlet.MockMvc;
      
      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;
      
      @RunWith(SpringRunner.class)
      @SpringBootTest
      @AutoConfigureMockMvc
      public class SampleControllerTest {
      
          @Autowired
          MockMvc mockMvc;
      
          @Test
          public void helloTest() throws Exception {
              mockMvc.perform(get("/hello")
                          .param("name","jinmin"))
                          .andDo(print())
                          .andExpect(status().isOk())
                          .andExpect(content().string("hello"));
          }
      }

HEAD와 OPTIONS

  • 개발자가 구현하지 않아도 스프링 웹 MVC에서 자동으로 처리하는 HTTP Method

    • HEAD
    • OPTIONS
  • HEAD

    • GET과 동일

      • But, 응답 본문을 받아오지 않고 응답 헤더만 받아온다.
    • 공통 (SampleController)

      package me.jinmin.demowebmvc;
      
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.ResponseBody;
      
      @Controller
      public class SampleController {
      
          @GetMapping("/hello")
          @ResponseBody
          public String hello(){
              return "hello";
          }
      }
    • 테스트 - GET

      package me.jinmin.demowebmvc;
      
      import org.junit.Test;
      import org.junit.runner.RunWith;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.test.context.junit4.SpringRunner;
      import org.springframework.test.web.servlet.MockMvc;
      
      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;
      
      @RunWith(SpringRunner.class)
      @SpringBootTest
      @AutoConfigureMockMvc
      public class SampleControllerTest {
      
          @Autowired
          MockMvc mockMvc;
      
          @Test
          public void helloTest() throws Exception {
              mockMvc.perform(get("/hello"))
                          .andDo(print())
                          .andExpect(status().isOk())
                          .andExpect(content().string("hello"));
          }
      }
      • 결과 (응답 본문)

        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 = []
    • 테스트 - HEAD

      package me.jinmin.demowebmvc;
      
      import org.junit.Test;
      import org.junit.runner.RunWith;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.test.context.junit4.SpringRunner;
      import org.springframework.test.web.servlet.MockMvc;
      
      import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.head;
      import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
      import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
      
      @RunWith(SpringRunner.class)
      @SpringBootTest
      @AutoConfigureMockMvc
      public class SampleControllerTest {
      
          @Autowired
          MockMvc mockMvc;
      
          @Test
          public void helloTest() throws Exception {
              mockMvc.perform(head("/hello"))
                          .andDo(print())
                          .andExpect(status().isOk());
          }
      }
      • 결과 (응답 본문)

        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 = []
      • Body의 내용이 다르다. Why? 헤더 정보만 가져오기 때문에

  • OPTIONS

    • 서버 또는 특정 리소스가 제공하는 기능을 확인할 수 있다.

    • 서버는 Allow 응답 헤더에 사용할 수 있는 HTTP Method 목록을 제공해야 한다.

    • SampleController (GET, POST 방식의 /hello)

      @Controller
      public class SampleController {
      
          @GetMapping("/hello")
          @ResponseBody
          public String hello(){
              return "hello";
          }
      
          @PostMapping("/hello")
          @ResponseBody
          public String helloPost(){
              return "hello";
          }
      }
    • SampleControllerTest

      @RunWith(SpringRunner.class)
      @SpringBootTest
      @AutoConfigureMockMvc
      public class SampleControllerTest {
      
          @Autowired
          MockMvc mockMvc;
      
          @Test
          public void helloTest() throws Exception {
              mockMvc.perform(options("/hello"))
                          .andDo(print())
                          .andExpect(status().isOk());
          }
      }
    • 결과

      MockHttpServletResponse:
                 Status = 200
          Error message = null
                Headers = [Allow:"GET,HEAD,POST,OPTIONS"]
           Content type = null
                   Body = 
          Forwarded URL = null
         Redirected URL = null
                Cookies = []
      • 직접 구성 : GET, POST
      • 스프링 웹 MVC 기본 제공 : HEAD, OPTIONS
  • 테스트 코드를 통해 특정 Header를 확인하는 여러 방법

    @RunWith(SpringRunner.class)
    @SpringBootTest
    @AutoConfigureMockMvc
    public class SampleControllerTest {
    
        @Autowired
        MockMvc mockMvc;
    
        @Test
        public void helloTest() throws Exception {
            mockMvc.perform(options("/hello"))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(header().exists(HttpHeaders.ALLOW))// Allow 헤더가 있는지
                    .andExpect(header().stringValues(HttpHeaders.ALLOW,
                            hasItems(
                                    containsString("GET"),
                                    containsString("POST"),
                                    containsString("HEAD"),
                                    containsString("OPTIONS")
                            ))); //Allow 헤더에 위와 같은 값들이 들어있는지(순서 상관없이)
        }
    }

커스텀 애노테이션

  • 메타 애노테이션

    • 애노테이션에 사용 가능한 애노테이션(@ 위의 @)
    • 스프링이 제공하는 대부분의 애노테이션은 메타 @
  • 조합(Composed) 애노테이션

    • 한 개 혹은 여러 애노테이션의 조합
    • 코드 간결화
    • 구체적 의미 부여
  • 예제)

    • @GetHelloMapping

      package me.jinmin.demowebmvc;
      
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RequestMethod;
      
      @RequestMapping(method = RequestMethod.GET, value = "/hello")
      public @interface GetHelloMapping {
      
      }
    • SampleController

      package me.jinmin.demowebmvc;
      
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.PostMapping;
      import org.springframework.web.bind.annotation.ResponseBody;
      
      @Controller
      public class SampleController {
      
          @GetHelloMapping
          @ResponseBody
          public String hello(){
              return "hello";
          }
      }
    • SampleControllerTest

      package me.jinmin.demowebmvc;
      
      import org.junit.Test;
      import org.junit.runner.RunWith;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.test.context.junit4.SpringRunner;
      import org.springframework.test.web.servlet.MockMvc;
      
      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.status;
      
      @RunWith(SpringRunner.class)
      @SpringBootTest
      @AutoConfigureMockMvc
      public class SampleControllerTest {
      
          @Autowired
          MockMvc mockMvc;
      
          @Test
          public void helloTest() throws Exception {
              mockMvc.perform(get("/hello"))
                      .andDo(print())
                      .andExpect(status().isOk());
          }
      }
      • 오류가 난다 Why??? 해결은 아래 내용부터‼
  • 🎈 @Retention

    • 해당 애노테이션 정보를 언제까지 유지할 것인가

      • Source : 소스 코드까지만 유지. (⇒ 컴파일 시 정보가 사라짐, 그저 // (주석))
      • Class(기본값) : 컴파일 한 .class 파일에도 유지 (⇒ 런타임 시, 클래스를 메모리로 읽어오면 사라짐)
      • Runtime : 클래스를 메모리로 읽어올 때까지 유지 (⇒ 코드에서 해당 정보를 바탕으로 특정 로직 수행, 애플리케이션이 구동될 때도 유지)
    • @GetHelloMapping

      package me.jinmin.demowebmvc;
      
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RequestMethod;
      
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      
      @Retention(RetentionPolicy.RUNTIME)
      @RequestMapping(method = RequestMethod.GET, value = "/hello")
      public @interface GetHelloMapping {
      
      }
      • 테스트 구동 성공
  • @Target

    • 해당 애노테이션을 어디에 사용할 수 있는지

      package me.jinmin.demowebmvc;
      
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RequestMethod;
      
      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
      
      @Target(ElementType.METHOD) // 배열로 여러 곳을 지정할 수 있다.
      @Retention(RetentionPolicy.RUNTIME)
      @RequestMapping(method = RequestMethod.GET, value = "/hello")
      public @interface GetHelloMapping {
      
      }
  • @Documented

    • 해당 애노테이션을 사용한 코드의 문서에 그 애노테이션에 대한 정보를 표기할지 결정

좋은 웹페이지 즐겨찾기