@RequestMapping
annotation을 집중적으로 다룬다.@RequestMapping
Basics그동안 몇개의 글에서 잠깐 다뤄서 알고 있겠지만, 웹 애플리케이션에 온 요청을 적절한 controller의 메서드에 mapping하는 것이 주된 역할이다.
...라고 소개하고 있지만 사실 정확히 말하면 HTTP request만 처리가 가능하다. 다른 프로토콜 형태의 request에 대한 mapping은 다른 annotation을 사용해야 하는데 (@MessageMapping
등) 어쨌든 @RequestMapping
은 HTTP request만 처리가 가능하니 이 경우에 대해서만 생각하겠다.
우리가 만든 애플리케이션이 기본적으로 /
, 즉 root context path에서 콘텐츠를 제공한다고 해보자. 이는 Spring Boot의 기본 설정이기도 하다.
저 기본 root 경로를 바꾸는 방법을 알고 싶으면 이 글 참고.
@RequestMapping
- by Path@RequestMapping(value = "/ex/foos", method = RequestMethod.GET)
@ResponseBody
public String getFoosBySimplePath() {
return "Get some Foos";
}
value
value
, 그러니까 attribute를 지정하지 않고 설정하는 element 혹은 value
를 써서 설정하는 element는 해당 메서드가 담당하는 경로(URI)를 나타낸다.method
method
는 해당 메서드가 담당하는 요청 종류를 나타낸다. 지원하는 HTTP 요청 종류는 밑과 같다.curl
command위 예제가 잘 작동하는지 실험 할 때 밑과 같은 command를 쓰라고 하고 있다.
curl -i http://localhost:8080/ex/foos
이는 리눅스 커맨드로, 서버에서 data를 전달하거나 받기 위한 요청을 보낼 때 사용하는 command이다. 위에 입력한 주소는 요청을 보내는 서버 및 관련 경로이며, 출력은 옵션으로 변경한게 아니면 서버로부터 받은 reply 결과물이다. 여러 option이 있는데 여기서 사용하는 옵션만 얘기해보자면
-i
: HTTP header 정보도 출력하도록 한다. 기본은 내용물만 출력.-X
: 무슨 HTTP request를 할건지 명시할 때 쓰인다. 참고로 명시가 안되면 GET
다.-H
: HTTP request때 추가로 더할 header을 명시할 때 쓰인다. 추가 header 개수만큼 -H
를 쓴다.-d
: parameter 전달에 사용.위 command는 로컬상 돌아가는 웹애플리케이션의 /ex/foos
에다가 GET 요청을 하는 것이다.
@RequestMapping
- the HTTP Methodmethod
부분을 각 controller method에다가 다르게 지정하면 된다. 밑처럼 메서드를 지정하면 위의 GET
메서드를 처리하는 메서드와 밑의 POST
메서드를 처리하는 메서드가 동일 경로에 대해 공존하게 된다.@RequestMapping(value = "/ex/foos", method = POST)
@ResponseBody
public String postFoos() {
return "Post some Foos";
}
curl -i -X POST http://localhost:8080/ex/foos
RequestMapping
and HTTP Headers@RequestMapping
With the headers
AttributeHTTP 요청에 header이 추가로 들어오는 경우는 자주 있다. 이것까지 고려해서 어떤 메서드가 요청을 처리할지 분기를 나누는 것이 가능하다. 이 때 사용하는 annotation의 element가 headers
이다.
밑의 경우 /ex/foos
에 key:val
형태의 header이 추가된 GET
요청이 오는 경우에 이를 처리하는 controller method를 정의한 것이다.
@RequestMapping(value = "/ex/foos", headers = "key=val", method = GET)
@ResponseBody
public String getFoosWithHeader() {
return "Get some Foos with Header";
}
curl -i -H "key:val" http://localhost:8080/ex/foos
@RequestMapping(
value = "/ex/foos",
headers = { "key1=val1", "key2=val2" }, method = GET)
@ResponseBody
public String getFoosWithHeaders() {
return "Get some Foos with Header";
}
curl -i -H "key1:val1" -H "key2:val2" http://localhost:8080/ex/foos
:
으로 나누도록 설정되어 있지만 Spring에서는 =
을 사용한다는 점 유의headers
는 해당 header이 request에 '포함되어'있냐를 가지고 mapping을 한다. 즉 앞의 첫번째 예시 코드의 경우 key:val
이라는 header이 포함되어 있으면 다른 header이 포함된 경우에도 저 controller method가 담당을 한다. 단, 현 요청의 header로 성립되는 더 구체적으로 조건이 지정된 controller method가 없다는 가정하에 말이다.
또 !=
을 사용해서, 해당 header이 존재하고 해당 값을 가지지 '않아야' 한다는 조건을 넣는것도 가능하다. 예를들어 key!=val
을 하면 val
값을 가지지 않는 key
header이 있는 경우에 그 controller method에 mapping한다는 것을 의미한다.
거기에 header이름만 넣어서 해당 header이 존재해야 한다는 조건을 넣는것도 가능하다. 예를들어 key
를 사용하면 key
header이 뭔 값이든 존재만 하면 그 controller method에 mapping한다는 것을 의미한다.
여기에 header 앞에 !
을 사용해서 해당 header이 존재하지 않아야 한다는 조건을 넣는것도 가능하다. 예를들어 !key
를 하면 key
라는 header이 request에 존재하지 않는 경우에 그 controller method에 mapping한다는 것을 의미한다.
마지막으로, type에도 이 annotation 사용이 가능하며 이 경우 메서드들이 전부 해당 조건을 계승하게 된다.
@RequestMapping
Consumes and ProducesHeader에 거의 무조건 있는 정보중의 하나가 바로 Accept
header이다. 혹시나 모를까봐 설명하자면 요청한 측에서 받을 수 있는 형식들을 전부 명시한 것이다.
때문에 이를 고려해가지고 controller method가 적절한 형식의 파일을 반환하도록 하는 것이 매우 중요하다. 이는 앞의 headers
를 활용해서구현할 수 있다. 예제는 json
받는게 가능한 요청에 대해 처리하는 controller method를 정의한 것이다.
@RequestMapping(
value = "/ex/foos",
method = GET,
headers = "Accept=application/json")
@ResponseBody
public String getFoosAsJsonFromBrowser() {
return "Get some Foos with Header Old";
}
Accept
의 경우 위 값을 '포함'하는 header이면 대응을 시켜주기 때문에 밑과 같은 request도 위의 controller method에 대응된다.curl -H "Accept:application/json,text/html" http://localhost:8080/ex/foos
Accept
랑 Content-Type
header 등의 미디어 관련 header에 한해서) wildcard pattern 사용이 가능하다. 모든 text형식에 대해서 mapping한다면 text/*
으로 한다든가 등.produces
/ consumes
produces
, consumes
가 이 역할을 한다. 밑은 위와 동일한 효과를 내는 코드.@RequestMapping(
value = "/ex/foos",
method = RequestMethod.GET,
produces = "application/json"
)
@ResponseBody
public String getFoosAsJsonFromREST() {
return "Get some Foos with Header New";
}
사실 앞의 옛날 코드를 Spring 3.1부터는 위와 같은 코드로 자동으로 변환시켜서 실행한다는 것도 유의.
consumes
는POST
와 같이 본인이 data를 '받는' 경우에 사용하는 element라는 것도 유의.
produces
와 consumes
에 (headers
와 마찬가지로) 여러개의 값을 넣는 것도 가능하다.@RequestMapping(
value = "/ex/foos",
method = GET,
produces = { "application/json", "application/xml" }
)
...
produces
/ consumes
를 Accept
header을 넣은 headers
와 같이 쓸 수 없다는 점 유의. 이유는 Spring 3.1 이후에서 이 둘을 사실상 같은 것으로 취급하기 때문이다.RequestMapping
With Path Variables@PathVariable
이라는 annotation을 활용해 URI의 일부를 변수에 대응시키는 것이 가능하다.
@PathVariable
@RequestMapping(value = "/ex/foos/{id}", method = GET)
@ResponseBody
public String getFoosBySimplePathWithPathVariable(
@PathVariable("id") long id) {
return "Get a specific Foo with id=" + id;
}
curl http://localhost:8080/ex/foos/1
@PathVariable
에 value
element를 지정할 필요가 없다. 그래서 밑은 위와 같은 효과를 낸다.@RequestMapping(value = "/ex/foos/{id}", method = GET)
@ResponseBody
public String getFoosBySimplePathWithPathVariable(
@PathVariable String id) {
return "Get a specific Foo with id=" + id;
}
id
의 type을 정해도 된다.@PathVariable long id
@PathVariable
@PathVariable
을 사용해도 된다.@RequestMapping(value = "/ex/foos/{fooid}/bar/{barid}", method = GET)
@ResponseBody
public String getFoosBySimplePathWithPathVariables
(@PathVariable long fooid, @PathVariable long barid) {
return "Get a specific Bar with id=" + barid +
" from a Foo with id=" + fooid;
}
curl http://localhost:8080/ex/foos/1/bar/2
@PathVariable
With RegexnumericId
부분이 숫자인 경우에만 controller method가 mapping되도록 설정한다.@RequestMapping(value = "/ex/bars/{numericId:[\\d]+}", method = GET)
@ResponseBody
public String getBarsBySimplePathWithPathVariable(
@PathVariable long numericId) {
return "Get a specific Bar with id=" + numericId;
}
http://localhost:8080/ex/bars/1
는 위에 mapping되고, http://localhost:8080/ex/bars/abc
는 안된다는 것이다.RequestMapping
With Request ParametersURL의 parameter을 쉽게 추출해주도록 돕는 annotation으로 @RequestParam
이 있다.
사용법은 그냥 특정 이름의 request parameter이 어디에 연결되어야 하는지를 표기하면 된다.
@RequestMapping(value = "/ex/bars", method = GET)
@ResponseBody
public String getBarBySimplePathWithRequestParam(
@RequestParam("id") long id) {
return "Get a specific Bar with id=" + id;
}
curl -i -d id=100 http://localhost:8080/ex/bars
params
여기서 더 나아가 params
를 사용할 경우, 해당 method가 해당 parameter을 가진 request를 받는 전용이라고 표시하는 것이 가능하다.
이렇게 표기하면 해당 parameter을 가진 request만 controller method에서 처리한다는 것을 표기하는게 가능하다. 예를들어 밑의 경우 /ex/bars
에 request하고, GET
이고, id
라는 parameter이 존재해야만 해당 controller method가 선택된다.
@RequestMapping(value = "/ex/bars", params = "id", method = GET)
@ResponseBody
public String getBarBySimplePathWithExplicitRequestParam(
@RequestParam("id") long id) {
return "Get a specific Bar with id=" + id;
}
headers
와 마찬가지로 여러개의 param을 설정하는 것도 가능하다. 이 경우에도 headers
와 마찬가지로 하나라도 해당되면 선택되는 후보군이 된다.@RequestMapping(
value = "/ex/bars",
params = { "id", "second" },
method = GET)
@ResponseBody
public String getBarBySimplePathWithExplicitRequestParams(
@RequestParam("id") long id) {
return "Narrow Get a specific Bar with id=" + id;
}
http://localhost:8080/ex/bars?id=100&second=something
RequestMapping
Corner Cases@RequestMapping
- Multiple Paths Mapped to the Same Controller Method@RequestMapping
의 path 부분은 하나의 controller method당 하나만 쓰는게 좋은 편이다. 다만 꼭 그게 좋다는건 아니고, 그 경우 여러개를 사용해도 가능하다.@RequestMapping(
value = { "/ex/advanced/bars", "/ex/advanced/foos" },
method = GET)
@ResponseBody
public String getFoosOrBarsByPath() {
return "Advanced - Get some Foos or Bars";
}
@RequestMapping
- Multiple HTTP Request Methods to the Same Controller Method@RequestMapping(
value = "/ex/foos/multiple",
method = { RequestMethod.PUT, RequestMethod.POST }
)
@ResponseBody
public String putAndPostFoos() {
return "Advanced - PUT and POST within single method";
}
@RequestMapping
- a Fallback for All Requestsvalue
, 즉 경로는 wildcard 설정이 가능하며, method
의 경우 유한하기 대문에 이를 전부 적으면 모든 request를 처리하는 controller method(...?)를 만드는 것이 가능하다.@RequestMapping(
value = "*",
method = { RequestMethod.GET, RequestMethod.POST ... })
@ResponseBody
public String allFallback() {
return "Fallback for All Requests";
}
5번까지의 예제들을 보면서 조금 더 생각해보면 이래저래 짜다보면 하나의 request에 대해 두가지의 controller method가 동시에 처리할 수 있는 경우를 만드는 것이 가능하다.
이 경우 보통은 더 많은 조건을 만족하는 controller method가 처리한다. 예를들어 밑의 경우 Accepts
header을 기반으로 어떤 controller method를 사용할지 정하게 된다.
@GetMapping(value = "foos/duplicate", produces = MediaType.APPLICATION_XML_VALUE)
public String duplicateXml() {
return "<message>Duplicate</message>";
}
@GetMapping(value = "foos/duplicate", produces = MediaType.APPLICATION_JSON_VALUE)
public String duplicateJson() {
return "{\"message\":\"Duplicate\"}";
}
duplicate
와 관련된 runtime error이 발생하게 된다.@GetMapping(value = "foos/duplicate" )
public String duplicate() {
return "Duplicate";
}
@GetMapping(value = "foos/duplicate" )
public String duplicateEx() {
return "Duplicate";
}
그러면 첫번째 경우에서 그 method들이 정의된 모든 controller method이고 Accepts
header이 없는 foos/duplicate
request가 들어오게 되면 어떻게 되냐고 할 수 있다. 오류? 아니다. Configuration에서 설정된 default media type에 지정된 type을 handle하는 controller method로 mapping된다. 구체적인 원리를 파악하는건 좀 복잡한데 이 글 참고
다만 위 Accept
가 아닌 다른 영역에서 약간 차이가 있는데 다른 element가 동일한 경우 보통은 runtime error을 일으킨다. 가끔 버전에 따라 아닌 경우가 있긴 하지만 일반적으로 그렇다. 그래서 가급적 중복되는 처리 조건이 나오지 않도록 path 등등을 잘 설정해주는게 좋다. 위의 produce
, consume
의 경우만 좀 예외가 있는거고.
@RequestMapping
을 기반으로 새로운 몇개의 HTTP mapping annotation을 만들었다. @GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping
method
element가 지정되어 있는것이다. 나머지는 동일. 이를 활용해 CRUD operation을 지원하는 RESTful API를 간단하게 다음과 같이 만들 수 있다.@GetMapping("/{id}")
public ResponseEntity<?> getBazz(@PathVariable String id){
return new ResponseEntity<>(new Bazz(id, "Bazz"+id), HttpStatus.OK);
}
@PostMapping
public ResponseEntity<?> newBazz(@RequestParam("name") String name){
return new ResponseEntity<>(new Bazz("5", name), HttpStatus.OK);
}
@PutMapping("/{id}")
public ResponseEntity<?> updateBazz(
@PathVariable String id,
@RequestParam("name") String name) {
return new ResponseEntity<>(new Bazz(id, name), HttpStatus.OK);
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteBazz(@PathVariable String id){
return new ResponseEntity<>(new Bazz(id), HttpStatus.OK);
}
여기서
ResponseEntity
는HttpEntity
중HttpStatusCode
가 추가된 녀석이며, 보통 Spring MVC의@Controller
의 return value로 자주 사용된다.new
로 생성하는 객체 부분은 HTTP response의 body 부분을 담당하게 된다.HttpStatusCode
는HttpStatus
enum class를 활용하면 된다.
@Controller
달린 component를 web application의 controller로 활용하기 위해서는 해당 controller이 위치한 package에 대해 @ComponentScan
을 하는것과, @EnableWebMvc
를 해야 한다는 것을 잊지 말자.@Configuration
@EnableWebMvc
@ComponentScan({ "org.baeldung.spring.web.controller" })
public class MvcConfig {
//
}