학습 자료

@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}

주소에 적은 02int 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 — 책 목록인데 가격순으로
  • 빠지면 조건만 사라진다
경로는 명사, 물음표 뒤는 조건. 3강에서 "경로는 명사"라고 했던 그 말의 실제 모습이다.

/books/1 에서 1 을 빼면 /books 가 되는데, 이건 책 한 권이 아니라 책 목록이다. 완전히 다른 것이다.

반면 /books?minPrice=20000 에서 조건을 빼면 그냥 /books 다. 여전히 책 목록이다. 필터만 없어졌다.

틀리게 보내본다

세 가지를 던져봤다. 전부 실제 응답이다.

틀리면 무엇이 오나
보낸 주소상태
/books/abc400abcint 로 못 바꾼다
/search (조건 없이)400minPrice 는 필수인데 없다
/search?minPrice=비쌈400숫자가 아니다
셋 다 4xx. 3강에서 배운 대로 "요청한 쪽이 잘못했다"는 뜻이다.

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 이 그 답이다. 요청마다 달라지는 값은 메서드 인자로 들어온다. 메서드 인자는 요청마다 따로 만들어지므로 섞이지 않는다.

값이 들어오는 세 갈래
  1. @PathVariable주소 한가운데서 — /books/1
  2. @RequestParam물음표 뒤에서 — ?minPrice=20000
  3. @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 의 생김새를 정한다.

다시 짚어보기
  1. 01

    경로는 @PathVariable, 물음표 뒤는 @RequestParam

    중괄호 이름과 매개변수 이름이 같으면 짝이 맞는다.

    @GetMapping("/books/{index}")
    public Book one(@PathVariable int index) { ... }
  2. 02

    무엇을 가리키면 경로, 어떻게 거르면 물음표 뒤

    /books/1 에서 1 을 빼면 다른 자원이 된다. ?minPrice 를 빼면 조건만 사라진다.

  3. 03

    변환에 실패하면 메서드가 안 불린다

    /books/abc 도, 필수 파라미터 누락도 전부 400 이다. 11강의 @RequestBody 와 같다.

  4. 04

    없어도 되는 값은 defaultValue 를 준다

    주소에서 오는 것은 글자이므로 기본값도 글자로 적는다.

    @RequestParam(defaultValue = "0") int minPrice
  5. 05

    요청마다 달라지는 값은 메서드 인자로만 받는다

    컨트롤러는 빈이라 하나뿐이다. 필드에 담으면 옆 사람 요청과 섞인다.

이제 재료가 다 모였다.

Part 3 에서 배운 것
하고 싶은 일쓸 표식강의
주소와 메서드를 잇는다@GetMapping · @PostMapping9강
객체를 JSON 으로 내보낸다@RestController (record 로)10강
JSON 을 객체로 받는다@RequestBody11강
주소에서 값을 꺼낸다@PathVariable · @RequestParam이번 강의
네 개면 CRUD 를 만들 수 있다.

다음 강의에서 책을 만들고, 읽고, 고치고, 지운다. 데이터베이스는 아직 안 쓴다. 자바 편 16강에서 배운 List 하나로 충분하다.

그리고 마지막에 서버를 한 번 껐다 켠다.

@PathVariable 과 @RequestParam — 주소에서 값을 꺼낸다 — 디코드랩(DCODELAB)