요청 매핑 — @RequestMapping은 URL을 어떻게 메서드에 연결할까
브라우저가
GET /api/users/123을 요청하면 스프링은 어떻게 수백 개의 컨트롤러 메서드 중에서 정확히 맞는 하나를 찾아낼까요?
개념 정의
@RequestMapping 은 HTTP 요청의 URL, HTTP 메서드, 헤더, 파라미터 등의 조건을 기반으로 특정 컨트롤러 메서드에 매핑하는 어노테이션입니다. @GetMapping, @PostMapping 등은 이것의 축약형입니다.
왜 필요한가
서블릿 API만 사용하면 URL 라우팅을 직접 구현해야 합니다.
// 서블릿 방식
protected void service(HttpServletRequest request, HttpServletResponse response) {
String path = request.getRequestURI();
String method = request.getMethod();
if ("/users".equals(path) && "GET".equals(method)) {
listUsers(request, response);
} else if (path.matches("/users/\\d+") && "GET".equals(method)) {
getUser(request, response);
}
// ... 수십 개의 if-else
}
@RequestMapping은 이 라우팅을 선언적으로 처리합니다.
내부 동작
매핑 등록 과정
1. 애플리케이션 시작 시 RequestMappingHandlerMapping 초기화
2. 모든 빈에서 @Controller/@RestController 탐색
3. 각 클래스의 @RequestMapping 메서드 검색
4. URL 패턴, HTTP 메서드, 헤더 등 조건으로 RequestMappingInfo 생성
5. MappingRegistry에 등록 (URL → Handler 매핑 테이블)
매핑 우선순위
요청이 여러 핸들러에 매칭될 수 있을 때, 스프링은 가장 구체적인 매핑을 선택합니다.
1. 정확한 경로: /users/admin
2. 경로 변수: /users/{id}
3. 와일드카드: /users/*
4. 더블 와일드카드: /users/**
코드 예제
HTTP 메서드별 매핑
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping // GET /api/users
public List<User> list() { ... }
@GetMapping("/{id}") // GET /api/users/123
public User get(@PathVariable Long id) { ... }
@PostMapping // POST /api/users
public User create(@RequestBody CreateUserRequest request) { ... }
이어서 @PutMapping을 적용한 나머지 구현부입니다.
@PutMapping("/{id}") // PUT /api/users/123
public User update(@PathVariable Long id, @RequestBody UpdateUserRequest request) { ... }
@PatchMapping("/{id}") // PATCH /api/users/123
public User partialUpdate(@PathVariable Long id, @RequestBody Map<String, Object> fields) { ... }
@DeleteMapping("/{id}") // DELETE /api/users/123
public void delete(@PathVariable Long id) { ... }
}
@PathVariable
// 기본 사용
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) { ... }
// 변수명이 다를 때
@GetMapping("/users/{userId}")
public User getUser(@PathVariable("userId") Long id) { ... }
// 여러 경로 변수
@GetMapping("/users/{userId}/orders/{orderId}")
public Order getOrder(@PathVariable Long userId, @PathVariable Long orderId) { ... }
// 정규식 패턴
@GetMapping("/files/{filename:.+}") // 확장자 포함 매칭
public Resource getFile(@PathVariable String filename) { ... }
// Optional PathVariable
@GetMapping({"/docs", "/docs/{section}"})
public String getDocs(@PathVariable(required = false) String section) { ... }
@RequestParam
// 기본 사용
@GetMapping("/users")
public List<User> search(@RequestParam String name) { ... }
// GET /users?name=홍길동
// 기본값 설정
@GetMapping("/users")
public Page<User> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id") String sort
) { ... }
// GET /users?page=0&size=20
이어서 나머지 구현 부분입니다.
// 필수가 아닌 경우
@GetMapping("/users")
public List<User> search(
@RequestParam(required = false) String name,
@RequestParam(required = false) String email
) { ... }
// Map으로 모든 파라미터 받기
@GetMapping("/search")
public List<User> search(@RequestParam Map<String, String> params) { ... }
// 리스트 파라미터
@GetMapping("/users")
public List<User> getByIds(@RequestParam List<Long> ids) { ... }
// GET /users?ids=1,2,3 또는 GET /users?ids=1&ids=2&ids=3
@MatrixVariable
// Matrix Variable 활성화 필요
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false); // 세미콜론 유지
configurer.setUrlPathHelper(urlPathHelper);
}
}
// GET /users/filter;age=25;city=Seoul
@GetMapping("/users/{filter}")
public List<User> filter(
@MatrixVariable int age,
@MatrixVariable String city
) { ... }
콘텐트 네고시에이션 (produces/consumes)
@RestController
@RequestMapping("/api/data")
public class DataController {
// JSON 요청만 받고 JSON으로 응답
@PostMapping(
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public DataResponse processJson(@RequestBody DataRequest request) { ... }
이어서 나머지 구현 부분입니다.
// XML 요청을 받아 XML로 응답
@PostMapping(
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_XML_VALUE
)
public DataResponse processXml(@RequestBody DataRequest request) { ... }
// 여러 미디어 타입 지원
@GetMapping(produces = { MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE })
public DataResponse getData() { ... }
}
consumes는 요청의 Content-Type, produces는 응답의 Accept 헤더와 매칭됩니다.
헤더와 파라미터 조건
// 특정 헤더가 있을 때만 매칭
@GetMapping(value = "/api/data", headers = "X-Api-Version=2")
public DataV2 getDataV2() { ... }
// 특정 파라미터가 있을 때만 매칭
@GetMapping(value = "/api/users", params = "type=admin")
public List<User> getAdmins() { ... }
// 파라미터가 없을 때만 매칭
@GetMapping(value = "/api/users", params = "!type")
public List<User> getAllUsers() { ... }
URL 패턴 매칭
// 정확한 경로
@GetMapping("/users/admin")
// 경로 변수
@GetMapping("/users/{id}")
// 와일드카드 (한 단계)
@GetMapping("/users/*/profile")
// /users/abc/profile ✓
// /users/abc/def/profile ✗
// 더블 와일드카드 (여러 단계)
@GetMapping("/docs/**")
// /docs/a ✓
// /docs/a/b/c ✓
클래스 레벨과 메서드 레벨 조합
@RestController
@RequestMapping(
value = "/api/v1/orders",
produces = MediaType.APPLICATION_JSON_VALUE
)
public class OrderController {
// GET /api/v1/orders
@GetMapping
public List<Order> list() { ... }
// GET /api/v1/orders/123
@GetMapping("/{id}")
public Order get(@PathVariable Long id) { ... }
// POST /api/v1/orders
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
public Order create(@RequestBody OrderRequest request) { ... }
}
클래스 레벨의 @RequestMapping이 기본 경로가 되고, 메서드 레벨에서 추가됩니다.
주의할 점
1. @PathVariable 이름과 URI 변수명이 다르면 매핑에 실패한다
@GetMapping("/users/{userId}")에서 @PathVariable Long id로 받으면, 변수명이 불일치하여 바인딩에 실패합니다. 컴파일러 설정에 따라 파라미터 이름이 보존되지 않을 수 있으므로, @PathVariable("userId") Long id처럼 명시적으로 이름을 지정하는 것이 안전합니다.
2. 후행 슬래시(trailing slash) 처리가 Spring Boot 3에서 변경되었다
Spring Boot 3(Spring Framework 6)부터 /users와 /users/가 기본적으로 다른 경로로 처리됩니다. 이전 버전에서는 동일하게 매칭되었지만, 업그레이드 후 기존 API 클라이언트가 후행 슬래시를 붙여 호출하면 404가 반환될 수 있습니다. UrlPathHelper의 setAlwaysUseFullPath나 trailing slash 매칭 옵션을 확인하세요.
3. @RequestParam의 required 기본값이 true여서 파라미터 누락 시 400 에러가 발생한다
@RequestParam String name에서 클라이언트가 name 파라미터를 보내지 않으면 MissingServletRequestParameterException(400)이 발생합니다. 선택적 파라미터는 @RequestParam(required = false) 또는 @RequestParam(defaultValue = "")를 명시해야 합니다.
정리
@RequestMapping은 URL 패턴, HTTP 메서드, 헤더, 미디어 타입 등 다양한 조건으로 매핑합니다@GetMapping,@PostMapping등은@RequestMapping의 축약형입니다@PathVariable은 URL 경로 일부를,@RequestParam은 쿼리 파라미터를 받습니다consumes와produces로 콘텐트 네고시에이션을 제어할 수 있습니다- 여러 매핑이 겹칠 때는 가장 구체적인 것이 우선합니다