1. URL 값은 왜 따로 받아야 할까?
REST API를 만들다 보면 URL 안에 값이 들어가는 경우가 많습니다.
GET /api/posts/1
GET /api/posts?keyword=spring&page=1두 요청은 모두 게시글 API처럼 보이지만, URL 안의 값이 의미하는 바는 다릅니다.
/api/posts/1
└── 1번 게시글이라는 특정 리소스
/api/posts?keyword=spring&page=1
└── 게시글 목록을 조회할 때 사용하는 검색 조건Spring MVC에서는 이런 값을 컨트롤러 메서드 파라미터로 받을 때 @PathVariable과 @RequestParam을 사용합니다.
@GetMapping("/api/posts/{postId}")
public PostResponse getPost(@PathVariable Long postId) {
return postService.getPost(postId);
}@GetMapping("/api/posts")
public List<PostResponse> getPosts(@RequestParam String keyword) {
return postService.search(keyword);
}이전 글인 HTTP 메서드와 매핑 어노테이션에서 HTTP 메서드별로 요청을 매핑하는 방법을 살펴봤습니다. 이번 글에서는 매핑된 요청의 URL 안에 들어있는 값을 어떻게 꺼내 쓰는지 알아보겠습니다.
2. @PathVariable과 @RequestParam 한눈에 보기
먼저 두 어노테이션의 차이를 간단히 정리해보겠습니다.
| 구분 | 사용 위치 | 주 용도 | 예시 |
|---|---|---|---|
@PathVariable | URL 경로 | 특정 리소스 식별 | /api/posts/1 |
@RequestParam | query string, form parameter | 검색, 필터, 정렬, 페이지 조건 | /api/posts?keyword=spring&page=1 |
URL을 기준으로 보면 더 분명합니다.
GET /api/posts/1?keyword=spring&page=1
│ └───────────────┬───────┘
│ @RequestParam
│
@PathVariable@PathVariable은 경로 자체의 일부를 변수로 받습니다. @RequestParam은 ? 뒤에 붙는 query string 값을 받습니다.
3. @PathVariable 기본 사용법
@PathVariable은 URL 경로에 들어간 값을 받을 때 사용합니다.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PostController {
@GetMapping("/api/posts/{postId}")
public PostResponse getPost(@PathVariable Long postId) {
return new PostResponse(postId, "Spring REST API");
}
}요청이 이렇게 들어오면,
GET /api/posts/1 HTTP/1.1Spring은 {postId} 자리에 있는 1을 postId 파라미터에 넣어줍니다.
/api/posts/{postId}
/api/posts/1
↓
postId = 1@PathVariable은 보통 단건 조회, 수정, 삭제처럼 특정 리소스를 가리킬 때 사용합니다.
@GetMapping("/api/posts/{postId}")
public PostResponse getPost(@PathVariable Long postId) {
return postService.getPost(postId);
}
@DeleteMapping("/api/posts/{postId}")
public void deletePost(@PathVariable Long postId) {
postService.deletePost(postId);
}4. 경로 변수 이름과 파라미터 이름
경로 변수 이름과 메서드 파라미터 이름이 같으면 @PathVariable에 이름을 생략할 수 있습니다.
@GetMapping("/api/posts/{postId}")
public PostResponse getPost(@PathVariable Long postId) {
return postService.getPost(postId);
}하지만 이름이 다르면 명시해야 합니다.
@GetMapping("/api/posts/{postId}")
public PostResponse getPost(@PathVariable("postId") Long id) {
return postService.getPost(id);
}이 경우 URL의 {postId} 값을 Java 파라미터 id로 받습니다.
실무에서는 가능하면 경로 변수 이름과 파라미터 이름을 맞추는 편이 읽기 쉽습니다.
@PathVariable Long postId이렇게 맞춰두면 컨트롤러 코드를 읽는 사람이 URL과 파라미터의 관계를 바로 이해할 수 있습니다.
5. 여러 개의 @PathVariable 받기
경로 변수는 여러 개 사용할 수도 있습니다.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CommentController {
@GetMapping("/api/posts/{postId}/comments/{commentId}")
public CommentResponse getComment(
@PathVariable Long postId,
@PathVariable Long commentId
) {
return new CommentResponse(postId, commentId, "좋은 글입니다.");
}
}요청 예시는 다음과 같습니다.
GET /api/posts/1/comments/10 HTTP/1.1값은 다음처럼 매핑됩니다.
postId = 1
commentId = 10이 구조는 “1번 게시글에 속한 10번 댓글”처럼 상위 리소스와 하위 리소스 관계를 표현할 때 자주 사용합니다.
다만 경로가 너무 깊어지면 API가 읽기 어려워질 수 있습니다.
/api/users/1/posts/2/comments/3/replies/4이런 경로가 계속 늘어난다면 정말 계층 구조로 표현해야 하는지, 또는 별도 리소스로 분리하는 것이 나은지 고민해봐야 합니다.
6. @RequestParam 기본 사용법
@RequestParam은 query string 값을 받을 때 사용합니다.
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PostController {
@GetMapping("/api/posts")
public List<PostResponse> getPosts(@RequestParam String keyword) {
return postService.search(keyword);
}
}요청이 이렇게 들어오면,
GET /api/posts?keyword=spring HTTP/1.1Spring은 query string의 keyword 값을 메서드 파라미터에 넣어줍니다.
?keyword=spring
↓
keyword = "spring"@RequestParam은 검색어, 페이지 번호, 정렬 조건, 필터 조건처럼 목록 조회 조건을 받을 때 자주 사용합니다.
@GetMapping("/api/posts")
public List<PostResponse> getPosts(
@RequestParam String keyword,
@RequestParam int page,
@RequestParam String sort
) {
return postService.search(keyword, page, sort);
}요청 예시는 다음과 같습니다.
GET /api/posts?keyword=spring&page=1&sort=latest HTTP/1.17. required와 defaultValue
@RequestParam은 기본적으로 필수 값입니다.
@GetMapping("/api/posts")
public List<PostResponse> getPosts(@RequestParam String keyword) {
return postService.search(keyword);
}이 상태에서 keyword 없이 요청하면 Spring은 요청이 잘못되었다고 판단합니다.
GET /api/posts HTTP/1.1검색어가 없어도 전체 목록을 보여주고 싶다면 required = false를 사용할 수 있습니다.
@GetMapping("/api/posts")
public List<PostResponse> getPosts(
@RequestParam(required = false) String keyword
) {
return postService.search(keyword);
}또는 기본값을 지정할 수 있습니다.
@GetMapping("/api/posts")
public List<PostResponse> getPosts(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size
) {
return postService.getPosts(page, size);
}요청에서 page와 size를 보내지 않으면 각각 1, 20이 사용됩니다.
GET /api/posts HTTP/1.1page = 1
size = 20페이징 값처럼 기본값이 자연스러운 값은 defaultValue를 사용하는 편이 컨트롤러 코드가 단순합니다.
8. Optional로 선택 파라미터 표현하기
선택 값이라는 의도를 더 드러내고 싶다면 Optional을 사용할 수도 있습니다.
import java.util.Optional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@GetMapping("/api/posts")
public List<PostResponse> getPosts(
@RequestParam Optional<String> keyword
) {
return postService.search(keyword.orElse(null));
}Spring은 @RequestParam Optional<String>을 선택 파라미터로 처리합니다.
다만 실무에서는 모든 선택 파라미터를 Optional로 감싸기보다, 팀 컨벤션에 맞춰 required = false, defaultValue, 별도 검색 조건 객체 중 하나를 일관되게 사용하는 것이 좋습니다.
간단한 값 하나라면 required = false도 충분합니다. 기본값이 명확하다면 defaultValue가 더 읽기 쉽습니다.
9. 타입 변환
@RequestParam과 @PathVariable은 문자열로 들어온 값을 메서드 파라미터 타입에 맞게 변환해줍니다.
@GetMapping("/api/posts/{postId}")
public PostResponse getPost(@PathVariable Long postId) {
return postService.getPost(postId);
}요청 경로의 1은 문자열처럼 들어오지만, Spring이 Long으로 변환해줍니다.
GET /api/posts/1 HTTP/1.1@RequestParam도 마찬가지입니다.
@GetMapping("/api/posts")
public List<PostResponse> getPosts(
@RequestParam int page,
@RequestParam boolean published
) {
return postService.getPosts(page, published);
}GET /api/posts?page=1&published=true HTTP/1.1값이 변환할 수 없는 형태라면 요청 오류가 발생합니다.
GET /api/posts?page=abc HTTP/1.1page를 int로 받을 수 없기 때문입니다. 이런 오류는 보통 400 Bad Request로 처리됩니다.
그래서 외부에 공개되는 API라면 타입 변환 실패나 필수 파라미터 누락에 대한 에러 응답 형식도 함께 정리하는 것이 좋습니다. 에러 처리 방식은 뒤쪽 글에서 별도로 다루겠습니다.
10. @RequestParam과 @PathVariable 선택 기준
가장 중요한 기준은 값이 “리소스 식별자”인지 “조회 조건”인지입니다.
| 상황 | 추천 |
|---|---|
| 특정 게시글 조회 | GET /api/posts/{postId} |
| 특정 회원 조회 | GET /api/users/{userId} |
| 게시글 검색 | GET /api/posts?keyword=spring |
| 페이지 번호 | GET /api/posts?page=1 |
| 정렬 조건 | GET /api/posts?sort=latest |
| 카테고리 필터 | GET /api/posts?category=spring |
특정 리소스를 가리키는 값이라면 @PathVariable이 자연스럽습니다.
GET /api/posts/1목록을 조회하면서 조건을 바꾸는 값이라면 @RequestParam이 자연스럽습니다.
GET /api/posts?keyword=spring&page=1&sort=latest다르게 말하면, 그 값이 없으면 URL이 가리키는 대상 자체가 달라지는지 생각해보면 됩니다.
/api/posts/1
└── 1번 게시글이라는 대상 자체를 가리킴
/api/posts?page=1
└── 게시글 목록이라는 대상은 같고, 조회 조건만 바뀜11. 실무에서 선택하는 기준
실무에서는 URL을 클라이언트와 서버가 함께 쓰는 계약으로 봅니다. 그래서 값의 위치가 일관적이어야 합니다.
저는 보통 아래 기준을 사용합니다.
| 값의 성격 | 위치 |
|---|---|
| 리소스 ID | path variable |
| 계층 관계 | path variable |
| 검색어 | request parameter |
| 필터 조건 | request parameter |
| 정렬 조건 | request parameter |
| 페이징 조건 | request parameter |
예를 들어 게시글 목록을 페이지 단위로 조회하는 API는 다음처럼 설계할 수 있습니다.
GET /api/posts?page=1&size=20&sort=latest게시글 하나를 조회하는 API는 다음처럼 설계합니다.
GET /api/posts/11번 게시글의 댓글 목록을 조회한다면 이렇게 표현할 수 있습니다.
GET /api/posts/1/comments?page=1&size=20여기서 1은 어떤 게시글의 댓글인지 결정하므로 path variable이고, page, size는 댓글 목록을 어떻게 조회할지 정하는 조건이므로 request parameter입니다.
12. 자주 하는 실수
12.1. 검색 조건을 path variable로 계속 늘리는 경우
검색 조건이 많아질수록 path variable로 표현하면 URL이 어색해집니다.
GET /api/posts/spring/latest/1/20이 URL만 보고는 spring, latest, 1, 20이 각각 무엇을 의미하는지 알기 어렵습니다.
검색, 정렬, 페이징 조건은 request parameter로 표현하는 편이 더 명확합니다.
GET /api/posts?keyword=spring&sort=latest&page=1&size=2012.2. 특정 리소스 ID를 request parameter로만 받는 경우
특정 게시글 하나를 조회하는 API를 이렇게 만들 수도 있습니다.
GET /api/posts?id=1동작은 가능하지만 REST API에서는 특정 리소스를 가리키는 ID를 경로에 두는 편이 더 자연스럽습니다.
GET /api/posts/1다만 여러 조건 중 하나로 ID를 필터링하는 검색 API라면 request parameter도 어색하지 않습니다.
GET /api/posts?authorId=1&category=spring중요한 것은 ID라는 이름만 보고 무조건 path variable로 넣는 것이 아니라, 그 값이 “대상 자체”인지 “조회 조건 중 하나”인지 구분하는 것입니다.
12.3. required 기본값을 모르고 오류를 만나는 경우
@RequestParam은 기본적으로 필수입니다.
@GetMapping("/api/posts")
public List<PostResponse> getPosts(@RequestParam String keyword) {
return postService.search(keyword);
}아래 요청은 keyword가 없으므로 오류가 납니다.
GET /api/posts HTTP/1.1검색어가 없어도 되는 API라면 required = false나 defaultValue를 명확히 지정해야 합니다.
@GetMapping("/api/posts")
public List<PostResponse> getPosts(
@RequestParam(required = false) String keyword
) {
return postService.search(keyword);
}12.4. 민감한 값을 query string에 넣는 경우
query string은 브라우저 주소창, 서버 로그, 프록시 로그, 분석 도구 등에 남을 수 있습니다.
GET /api/users?password=secret
GET /api/token?accessToken=sample-token비밀번호, 인증 토큰, 주민등록번호 같은 민감한 값은 query string에 넣지 않는 것이 좋습니다. 인증 정보는 보통 HTTP 헤더나 안전한 인증 흐름을 통해 전달합니다.
예시 값도 실제 값처럼 보이지 않게 작성하는 습관이 중요합니다.
13. 참고 자료
- Spring Framework - Method Arguments
- Spring Framework - @RequestParam
- Spring Framework API - PathVariable
14. 정리
@PathVariable과 @RequestParam은 URL 안의 값을 컨트롤러 메서드 파라미터로 받기 위한 어노테이션입니다.
@PathVariable은 URL 경로에 포함된 값을 받을 때 사용합니다.@RequestParam은 query string이나 form parameter 값을 받을 때 사용합니다.- 특정 리소스를 식별하는 값은 보통 path variable로 표현합니다.
- 검색, 필터, 정렬, 페이징 조건은 request parameter로 표현하는 편이 자연스럽습니다.
@RequestParam은 기본적으로 필수 값이므로 선택 값이라면required = false,defaultValue,Optional등을 고려해야 합니다.- query string에는 민감한 값을 넣지 않는 것이 좋습니다.
이 두 어노테이션을 구분할 수 있으면 REST API URL 설계가 훨씬 선명해집니다. 다음 글에서는 요청 본문에 담긴 JSON 데이터를 Java 객체로 받는 @RequestBody를 살펴보겠습니다.