Spring Web MVC - 요청 매핑
HTTP Method
HTTP 메소드에는 GET, POST, PUT, PATCH, DELETE 등이 있다.
예제를 하나 만들어보자.
컨트롤러
@Controller
public class BookController {
@RequestMapping(value = "/bookstore")
@ResponseBody
public String bookStore() {
return "Welcome book store";
}
}
테스트 코드
@WebMvcTest
class BookControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void bookStoreTest() throws Exception {
mockMvc.perform(get("/bookstore"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("Welcome book store"))
;
}
}
테스트코드에서 get 뿐만 아니라, post, put, delete 요청 모두 가능하다.
@RequestMapping
은 기본적으로 모든 메소드를 지원하기 때문이다.
특정 메소드만 지정하려면 아래와 같이 변경한다.
예를 들어 get 만 요청하려면 method 에 GET
을 추가한다.
@RequestMapping(value = "/bookstore", method = RequestMethod.GET)
@ResponseBody
public String bookStore() {
return "Welcome book store";
}
@RequestMapping(value = "/bookstore", method = RequestMethod.GET)
는 @GetMapping("/bookstore")
로 바꿀 수 있다.
@Test
public void bookStoreTest() throws Exception {
mockMvc.perform(put("/bookstore"))
.andDo(print())
.andExpect(status().isMethodNotAllowed())
;
}
get 메소드를 받게 해놓고 예를 들어 put 요청을 하면 405
에러가 난다. 주어진 메소드 (put) 를 사용할 수 없다는 에러이다.
따라서 isMethodNotAllowed
테스트는 성공적으로 테스트가 완료된다.
여러 메소드의 요청을 허락하고 싶은 경우는 @RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
등으로 해주면 된다.
요청 | 내용 |
---|---|
GET | - SELECT, 클라이언트가 서버의 리소스를 요청할 때 사용한다. - 캐싱이 가능해서 조건적으로 GET 요청 로직이 바뀔 수 있다. 예를 들어 GET 요청을 전에 받아온 적이 있는 상태에서 다시 GET 요청을 하면 응답코드 200(정상)이 아닌 304(수정되지 않음)이 나온다. 캐시로 효율적인 자원 관리를 한 셈이다. - 브라우저 기록에 남는다. 북마크 할 수 있다. - URL에 정보가 다 보이므로 민감한 데이터를 보낼 때 사용해서는 안된다. - idemponent 하다. |
POST | - CREATE, 클라이언트가 서버의 리소스를 수정하거나 새로 만들 때 사용한다. - 서버에 보내는 데이터를 POST 요청 본문에 담는다. - 캐시할 수 없다. - 브라우저 기록에 남지 않는다. 북마크 할 수 없다. - 데이터 길이의 제한이 없다. |
PUT | - UPDATE, URI에 해당하는 데이터를 새로 만들거나 수정할 때 사용한다. - POST 의 URI 는 보내는 데이터를 처리할 리소스를 지칭한다. 반면, PUT 의 URI 는 보내는 데이터에 해당하는 리소스를 지칭한다. - idemponent 하다. |
PATCH | - UPDATE, PUT 과 비슷하다. 다른점은 기존 엔티티와 새 데이터의 차이점만 보낸다는 것이다. - idemponent 하다. |
DELETE | - DELETE, URI에 해당하는 리소스를 삭제한다. - idemponent 하다. |
idemponent
: 멱등성이라 하며 동일한 요청은 항상 동일한 결과를 반환해야 한다는 속성이다.POST vs PUT
똑같은 일련번호를 가진 게시글 2개를 올릴 때를 가정해보자. POST 의 경우 똑같은 게시글이 2개 만들어진다. 따라서 첫번째와 두번째는 다른결과가 나오므로 멱등성을 만족하지 못한다. PUT 의 경우는 첫 번째에 게시글이 생성되고, 두 번째는 같은 번호를 가진 게시글이 중복되므로 더 이상 만들지지지 않는다. 따라서 계속 반복하여도 결과는 같으므로 멱등성을 만족한다.
URI 패턴
스프링 MVC 는 여러 URI 패턴의 매핑을 지원한다.
-
요청 식별자로 매핑
@RequestMapping
에서 ?(한글자 동일), (여러글자 동일), *\(슬래시 하위 모든 패턴도 포함) 패턴을 지원한다. -
클래스위에 선언한
@RequestMapping
에서의 URI 패턴을 기본으로 하여 메소드 위에@RequestMapping
URI 를 이어 붙여 매핑이 가능하다. -
정규 표현식으로 매핑이 가능하다.
예)
/{name:[a-z]+}
는 name 으로 올 문자열이 [a-z]+ 라는 정규표현식에 맞는 요청만 처리한다.[a-z]+ 는 한 개이상의 소문자 알파벳을 의미한다.
-
패턴이 중복되는 경우, 가장 구체적으로 매핑되는 핸들러를 선택한다.
예)
/**
보다/name
이 우선순위이다. -
URL 확장자 매핑 지원
스프링 MVC 에서는
/index
라고 매핑하면,/index.html
,/index.json
,/index.xml
등의 확장자를 자동으로 매핑해준다.하지만 스프링부트에서는 이 기능을 지원하지 않는다. 보안상의 이슈가 있기 때문이다.
또한 URI 변수, Path 매개변수, URI 인코딩을 사용할 때 불명확해지므로 URL 확장자 매핑은 권장하지 않는다. 최근에는 accept 헤더에 무슨 타입으로 반환받기를 원하는지 판단하는 방식을 많이 사용한다.
그래도 사용하려면
/index.*
이런식으로 표현하여 매핑하거나 요청 파라미터를 사용한다./index?type=xml
RFD Attack (Reflected File Download Attack)
예를 들어 브라우저에서 확장자 파일을 .zip 으로 설정하면 자동으로 리소스를 다운받게 된다. (과거) 이를 확장해서, 사용자는 정상적인 URI 을 입력해서 특정 서버에 접속했는데 마치 앞선 사례처럼 되어버려 임의 파일이 다운로드 된다. 사용자가 그 파일을 열면, 특정 정보를 서버로 전송한다던가, 쿠기정보를 다 가져간다던가 등의 일이 가능해진다. 하지만 이제는 RFD 공격을 막기 위해 헤더정보에 표시를 해두므로 이전보다는 안전하다.
미디어 타입 매핑
Content-Type 매핑
특정 타입의 요청만 처리하는 핸들러는 @RequestMapping(consumes=)
옵션으로 지정한다.
테스트코드의 경우 content-type
헤더를 입력하여 요청메시지를 보낸다. content-type 이 매칭되지 않는 경우 에러 415 (Unsupported Media Type) 응답이 발생한다.
@Controller
public class BookController {
@GetMapping(value = "/bookstore",
consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String bookStore() {
return "Welcome book store";
}
}
MediaType.APPLICATION_JSON_VALUE
은 "application/json"
문자열과 같다. 스프링에서 상수로 제공된다. 자동완성으로 간편한 코딩과 동시에, 오탈자를 방지할 수 있다.
@WebMvcTest
class BookControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void bookStoreTest() throws Exception {
mockMvc.perform(get("/bookstore")
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk())
;
}
}
Accept 타입 매핑
특정 타입의 응답을 만드는 핸들러는 @RequestMapping(produces=)
를 이용한다.
테스트코드에서는 Accept 헤더로 필터링이 가능하다. 매칭되지 않는 경우에는 406 (Not Acceptable) 에러 응답이 돌아온다.
@Controller
public class BookController {
@GetMapping(value = "/bookstore",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
public String bookStore() {
return "Welcome book store";
}
}
@WebMvcTest
class BookControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void bookStoreTest() throws Exception {
mockMvc.perform(get("/bookstore")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
;
}
}
@RequestMapping
은 클래스에서 선언한 경우와 메소드에 선언한 경우가 있을 때 둘의 특성이 조합되는 것이 아니라, 메소드에 사용한 설정으로 덮여쓰인다.
헤더와 파라미터 매핑
위에서 다루었던 방법보다 좀 더 일반적 방법이다.
@RequestMapping
에서 헤더정보를 추가할 수 있다.
-
header = "key"
: 특정 헤더가 있는 요청을 처리하고 싶을 때 사용 -
header = "!key"
: 특정 헤더가 없는 요청을 처리하고 싶을 때 사용 -
header = "key=value"
: 특정 헤더 키/값이 있는 요청을 처리하고 싶을 때 사용@WebMvcTest class BookControllerTest { @Autowired MockMvc mockMvc; @Test public void bookStoreTest() throws Exception { mockMvc.perform(get("/bookstore") .header(HttpHeaders.AUTHORIZATION, "auth")) .andDo(print()) .andExpect(status().isOk()) ; } }
@Controller public class BookController { @GetMapping(value = "/bookstore", headers = HttpHeaders.AUTHORIZATION + "=auth") @ResponseBody public String bookStore() { return "Welcome book store"; } }
-
params = "a"
: 특정 요청 매개변수 키를 가진 요청을 처리하고 싶을 때 사용 -
params = "!a"
: 특정 요청 매개변수 키를 가지지 않은 요청을 처리하고 싶을 때 사용 -
params = "a=b"
: 특정 요청 매개변수 키/값을 가진 요청을 처리하고 싶을 때 사용@WebMvcTest class BookControllerTest { @Autowired MockMvc mockMvc; @Test public void bookStoreTest() throws Exception { mockMvc.perform(get("/bookstore") .param("a","b")) .andDo(print()) .andExpect(status().isOk()) ; } }
@Controller public class BookController { @GetMapping(value = "/bookstore", params = "a=b") @ResponseBody public String bookStore() { return "Welcome book store"; } }
consumes
는 headers = HttpHeaders.CONTENT_TYPE
과 동일한 옵션이며,
produces
또한 headers = HttpHeaders.ACCEPT
와 동일한 옵션이었음을 알 수 있다.
HEAD 와 OPTIONS
HEAD와 OPTIONS 는 따로 구현하지 않아도 스프링 웹 MVC 에서 자동으로 처리가 되는 HTTP method 이다.
HEAD 는 GET 요청과 동일하지만 응답 본문을 받아오지 않고 응답헤더만 받아오는 기능이다.
@Test
public void bookStoreTest() throws Exception {
mockMvc.perform(head("/bookstore"))
.andDo(print())
.andExpect(status().isOk())
;
}
응답 본문은 받아지지 않는다.
OPTIONS 는 사용할 수 있는 HTTP method 를 제공하며, 서버 또는 특정 리소스가 제공하는 기능을 확인할 수 있다. 서버는 Allow 응답 헤더에 사용할 수 있는 Http method 목록을 제공해야 한다.
같은 URL 에 아래와 같이 GET, POST 매핑이 둘 다 되어있다고 가정해보자.
@Controller
public class BookController {
@GetMapping(value = "/bookstore")
@ResponseBody
public String bookStore() {
return "Welcome book store";
}
@PostMapping("/bookstore")
@ResponseBody
public String bookStorePost() {
return "bookstore post";
}
}
테스트 코드에 options()
를 넣어준다.
@Test
public void bookStoreTest() throws Exception {
mockMvc.perform(options("/bookstore"))
.andDo(print())
.andExpect(status().isOk())
;
}
테스트를 실행시키면 응답 코드에서 아래 4가지 옵션이 나온 것을 확인할 수 있다. GET 과 POST 는 우리가 만들어준 컨트롤러에서 제공을 하고, HEAD 와 OPTIONS 는 스프링 웹 MVC 가 기본으로 제공한다.
Headers = [Allow:"POST,GET,HEAD,OPTIONS"]
테스트 코드에 이 옵션들이 모두 있는 것을 확인하는 코드를 만들어보자.
@WebMvcTest
class BookControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void bookStoreTest() throws Exception {
mockMvc.perform(options("/bookstore"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(header().stringValues(HttpHeaders.ALLOW,
hasItems(
containsString("GET"),
containsString("POST"),
containsString("HEAD"),
containsString("OPTIONS")
)))
;
}
}
코드가 조금 번거롭긴 하지만 잘 작동한다. 위처럼하면 옵션들의 순서를 지키지 않아도 된다.
커스텀 애노테이션
클래스 위에 있는 애노테이션을 메타 애노테이션이라 한다. 메타 에노테이션이 붙어있는 것은 composite 애노테이션이라 한다.
예를 들자면,
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping { ...
@GetMapping
은 composite 애노테이션이며, 이 위에 붙어있는 것들은 모두 메타 애노테이션이라 할 수 있다.
메타 애노테이션
- 애노테이션으로 사용할 수 있는 애노테이션이다. 스프링이 제공하는 대부분 애토에시녀은 메타 애노테이션이다. 그런데
@GetMapping
은 아니다. 따라서 요청 매핑 관련 애노테이션을 커스터마이징 할 때는@RequestMapping
을 사용한다.
@Retention
애노테이션은 기본값으로 RetentionPolicy.CLASS
를 가진다. 컴파일되고 클래스 파일에는 남아있을 수 있다. 하지만 클래스로더가 클래스파일 바이트코드를 읽어올 때 이 애노테이션에 대한 정보는 사라진다. 즉, 런타임에는 사라진다.
애노테이션을 언제까지 지속할지 정보를 설정할 수 있다. RetentionPolicy.RUNTIME
으로 설정하면 된다.
만약 애노테이션을 주석처럼 쓰고 싶다면, RetentionPolicy.SORUCE
로 바꾸어주면 된다. 컴파일 후에는 .class
파일에는 이 어노테이션이 남아있지 않게 된다.
@Retention(RetentionPolicy.RUNTIME)
@RequestMapping(method = RequestMethod.GET, value = "book")
public @interface GetBookMapping {
}
@Controller
public class BookController {
@GetBookMapping
@ResponseBody
public String bookStore() {
return "Welcome book store";
}
}
@WebMvcTest
class BookControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void bookStoreTest() throws Exception {
mockMvc.perform(get("/book"))
.andDo(print())
.andExpect(status().isOk())
;
}
}
위처럼 코드를 구성하면 정상적으로 작동한다.
@Target
- 애노테이션을 어디에 사용할 수 있는지 결정할 수 있다. 메소드 위에서 사용하고 싶다면
ElementType.METHOD
를 써주면 된다. 기본값이다. 여러 조건을 설정하려면 컴마 찍고 여러개 적어주면 된다. (ElementType.CONSTRUCTOR
: 생성자 위 등)
@Documented
- 애노테이션을 사용한 코드의 문서에 이 애노테이션에 대한 정보를 표기할지 설정한다.
- 백기선님 스프링 웹 MVC 참고
'Java, Kotlin, Spring > Spring Web MVC' 카테고리의 다른 글
Spring Web MVC - ModelAttribute, 폼 서브밋 (0) | 2021.02.21 |
---|---|
Spring Web MVC - Handler 아규먼트 다루기 (0) | 2021.02.17 |
Spring Web MVC - HTTP Message Converter (0) | 2021.02.13 |
Spring Web MVC - 여러가지 설정 1 (0) | 2021.02.13 |
Spring MVC 설정 (0) | 2021.02.13 |
댓글