@PathVariable 과 @RequestParam — 주소에서 값을 꺼낸다
Article
3강에서 이렇게 약속했다.
GET /books/1
DELETE /books/1
그런데 우리는 아직 저 1 을 못 꺼낸다. 지금 만들 수 있는 건 /books 하나뿐이다.
이번 강의는 표식 두 개를 배운다. 주소에서 값을 꺼내는 표식이다. 어렵지 않다.
경로에서 꺼낸다
주소의 일부를 값으로 받고 싶으면 중괄호로 묶는다.
@GetMapping("/books/{index}")
public Book one(@PathVariable int index) {
return books.get(index);
}
중괄호 안의 이름(index)과 매개변수 이름(index)이 같으면 짝이 맞는다. 그게 전부다.
- $ curl http://localhost:8080/books/0
- {"title":"자바","price":20000}
- $ curl http://localhost:8080/books/2
- {"title":"HTTP","price":15000}
주소에 적은 0 과 2 가 int index 로 들어왔다. 글자였던 것이 숫자가 됐다. 우리는 아무것도 안 했다.
11강에서 본문의 글자가 Book 이 됐던 것과 같은 일이다. 경계를 넘어오면서 타입이 붙는다.
물음표 뒤에서 꺼낸다
주소 뒤에 ?이름=값 을 붙이는 방식도 있다. 이걸 쿼리 스트링이라고 부른다.
/search?minPrice=20000
이건 @RequestParam 으로 받는다.
@GetMapping("/search")
public List<Book> search(@RequestParam int minPrice) {
return books.stream()
.filter(b -> b.price() >= minPrice)
.toList();
}
자바 편 19강에서 배운 스트림 그대로다. 조건에 맞는 것만 걸러 돌려준다.
- $ curl 'http://localhost:8080/search?minPrice=20000'
- [{"title":"자바","price":20000},{"title":"스프링","price":32000}]
15000 짜리 HTTP 책이 빠졌다. 잘 걸러졌다.
무엇을 어디에 둬야 하나
여기가 처음 배울 때 제일 헷갈리는 부분이다. 규칙은 한 줄이면 된다.
경로에 둔다
@PathVariable
- 무엇을 가리키는가
- 그것 없이는 주소가 성립하지 않는다
/books/1— 1번 책/books/1/reviews— 1번 책의 후기들- 빠지면 다른 자원이 된다
물음표 뒤에 둔다
@RequestParam
- 어떻게 걸러낼까
- 없어도 주소가 성립한다
/books?minPrice=20000— 책 목록인데 비싼 것만/books?sort=price— 책 목록인데 가격순으로- 빠지면 조건만 사라진다
/books/1 에서 1 을 빼면 /books 가 되는데, 이건 책 한 권이 아니라 책 목록이다. 완전히 다른 것이다.
반면 /books?minPrice=20000 에서 조건을 빼면 그냥 /books 다. 여전히 책 목록이다. 필터만 없어졌다.
틀리게 보내본다
세 가지를 던져봤다. 전부 실제 응답이다.
| 보낸 주소 | 상태 | 왜 |
|---|---|---|
/books/abc | 400 | abc 를 int 로 못 바꾼다 |
/search (조건 없이) | 400 | minPrice 는 필수인데 없다 |
/search?minPrice=비쌈 | 400 | 숫자가 아니다 |
11강과 똑같다. 변환할 수 없으면 메서드는 아예 실행되지 않는다. 우리 코드 안에서는 index 가 항상 int 이고, minPrice 가 항상 숫자다.
로그를 켜면 이유가 보인다. 이름만 읽어도 뜻이 나온다.
- MethodArgumentTypeMismatchException:
- Method parameter 'index': Failed to convert value of type 'java.lang.String' to required type 'int'
- MissingServletRequestParameterException:
- Required request parameter 'minPrice' for method parameter type int is not present
타입이 안 맞는다, 그리고 필수 파라미터가 없다. 딱 그 말이다.
없어도 되는 값
minPrice 를 안 보내면 400 이다. 그런데 "조건 없이 전부 보여줘"도 되어야 자연스럽다.
기본값을 적어주면 된다.
@GetMapping("/search")
public List<Book> search(@RequestParam(defaultValue = "0") int minPrice) {
...
}
- $ curl http://localhost:8080/search
- [{"title":"자바","price":20000},{"title":"스프링","price":32000},{"title":"HTTP","price":15000}]
- $ curl 'http://localhost:8080/search?minPrice=30000'
- [{"title":"스프링","price":32000}]
이제 안 보내면 전부, 보내면 걸러서 준다.
둘을 함께 쓴다
같이 써도 된다. 각자 자기 자리에서 꺼내온다.
@GetMapping("/books/{index}/tag")
public String tag(@PathVariable int index, @RequestParam String label) {
return books.get(index).title() + " → " + label;
}
- $ curl 'http://localhost:8080/books/1/tag?label=추천'
- 스프링 → 추천
1 은 경로에서, 추천 은 물음표 뒤에서 왔다.
5강에서 한 약속을 여기서 지킨다
5강에서 이렇게 못박았다.
빈에는 하는 일만 담고, 다루는 값은 메서드 인자로 받는다.
9강에서 컨트롤러도 빈이고 하나뿐이라는 것을 확인했다. 두 번 요청해도 this 가 같았다. 모든 사람이 같은 컨트롤러 객체를 나눠 쓴다.
그러니 "지금 요청한 사람이 찾는 책 번호"를 필드에 담아두면 큰일 난다. 옆 사람의 요청이 덮어쓴다.
@PathVariable 과 @RequestParam 이 그 답이다. 요청마다 달라지는 값은 메서드 인자로 들어온다. 메서드 인자는 요청마다 따로 만들어지므로 섞이지 않는다.
- @PathVariable주소 한가운데서 — /books/1
- @RequestParam물음표 뒤에서 — ?minPrice=20000
- @RequestBody본문에서 — {"title":...} (11강)
그런데 문제가 하나 남았다
없는 번호를 달라고 해보자.
- $ curl http://localhost:8080/books/99
- {"timestamp":"...","status":500,"error":"Internal Server Error","path":"/books/99"}
500 이다. 3강에서 5xx 는 서버가 잘못했다는 뜻이라고 했다.
서버 로그를 보면 이유가 뻔하다.
- IndexOutOfBoundsException: Index 99 out of bounds for length 3
우리가 books.get(99) 를 불렀고 터졌다. 자바 편 18강의 예외 계보 그림에 ArrayIndexOutOfBoundsException 이 있었는데, 여기 나온 IndexOutOfBoundsException 이 그 부모다. 배열이 아니라 리스트라서 부모 쪽 이름으로 나온다.
그런데 정말 서버 잘못인가. 없는 책을 달라고 한 것은 클라이언트다. 3강에서 "그런 것 없으면 404" 라고 배웠다.
정리하면
주소에서 값을 꺼내는 표식이 둘 있고, 무엇을 어디에 두느냐가 API 의 생김새를 정한다.
- 01
경로는
@PathVariable, 물음표 뒤는@RequestParam중괄호 이름과 매개변수 이름이 같으면 짝이 맞는다.
@GetMapping("/books/{index}") public Book one(@PathVariable int index) { ... } - 02
무엇을 가리키면 경로, 어떻게 거르면 물음표 뒤
/books/1에서 1 을 빼면 다른 자원이 된다.?minPrice를 빼면 조건만 사라진다. - 03
변환에 실패하면 메서드가 안 불린다
/books/abc도, 필수 파라미터 누락도 전부400이다. 11강의@RequestBody와 같다. - 04
없어도 되는 값은
defaultValue를 준다주소에서 오는 것은 글자이므로 기본값도 글자로 적는다.
@RequestParam(defaultValue = "0") int minPrice - 05
요청마다 달라지는 값은 메서드 인자로만 받는다
컨트롤러는 빈이라 하나뿐이다. 필드에 담으면 옆 사람 요청과 섞인다.
이제 재료가 다 모였다.
| 하고 싶은 일 | 쓸 표식 | 강의 |
|---|---|---|
| 주소와 메서드를 잇는다 | @GetMapping · @PostMapping | 9강 |
| 객체를 JSON 으로 내보낸다 | @RestController (record 로) | 10강 |
| JSON 을 객체로 받는다 | @RequestBody | 11강 |
| 주소에서 값을 꺼낸다 | @PathVariable · @RequestParam | 이번 강의 |
다음 강의에서 책을 만들고, 읽고, 고치고, 지운다. 데이터베이스는 아직 안 쓴다. 자바 편 16강에서 배운 List 하나로 충분하다.
그리고 마지막에 서버를 한 번 껐다 켠다.