브라우저가 GET /api/users/123을 요청하면 스프링은 어떻게 수백 개의 컨트롤러 메서드 중에서 정확히 맞는 하나를 찾아낼까요?

개념 정의

@RequestMapping 은 HTTP 요청의 URL, HTTP 메서드, 헤더, 파라미터 등의 조건을 기반으로 특정 컨트롤러 메서드에 매핑하는 어노테이션입니다. @GetMapping, @PostMapping 등은 이것의 축약형입니다.

왜 필요한가

서블릿 API만 사용하면 URL 라우팅을 직접 구현해야 합니다.

JAVA
// 서블릿 방식
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은 이 라우팅을 선언적으로 처리합니다.

내부 동작

매핑 등록 과정

PLAINTEXT
1. 애플리케이션 시작 시 RequestMappingHandlerMapping 초기화
2. 모든 빈에서 @Controller/@RestController 탐색
3. 각 클래스의 @RequestMapping 메서드 검색
4. URL 패턴, HTTP 메서드, 헤더 등 조건으로 RequestMappingInfo 생성
5. MappingRegistry에 등록 (URL → Handler 매핑 테이블)

매핑 우선순위

요청이 여러 핸들러에 매칭될 수 있을 때, 스프링은 가장 구체적인 매핑을 선택합니다.

PLAINTEXT
1. 정확한 경로: /users/admin
2. 경로 변수: /users/{id}
3. 와일드카드: /users/*
4. 더블 와일드카드: /users/**

코드 예제

HTTP 메서드별 매핑

JAVA
@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을 적용한 나머지 구현부입니다.

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

JAVA
// 기본 사용
@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

JAVA
// 기본 사용
@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

이어서 나머지 구현 부분입니다.

JAVA
// 필수가 아닌 경우
@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

JAVA
// 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)

JAVA
@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) { ... }

이어서 나머지 구현 부분입니다.

JAVA
    // 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 헤더와 매칭됩니다.

헤더와 파라미터 조건

JAVA
// 특정 헤더가 있을 때만 매칭
@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 패턴 매칭

JAVA
// 정확한 경로
@GetMapping("/users/admin")

// 경로 변수
@GetMapping("/users/{id}")

// 와일드카드 (한 단계)
@GetMapping("/users/*/profile")
// /users/abc/profile ✓
// /users/abc/def/profile ✗

// 더블 와일드카드 (여러 단계)
@GetMapping("/docs/**")
// /docs/a ✓
// /docs/a/b/c ✓

클래스 레벨과 메서드 레벨 조합

JAVA
@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가 반환될 수 있습니다. UrlPathHelpersetAlwaysUseFullPath나 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은 쿼리 파라미터를 받습니다
  • consumesproduces로 콘텐트 네고시에이션을 제어할 수 있습니다
  • 여러 매핑이 겹칠 때는 가장 구체적인 것이 우선합니다
댓글 로딩 중...