전체 목록
SpringEasy#74

@RequestParam, @PathVariable, @RequestBody의 차이를 설명해주세요.

#Spring#MVC#HTTP#어노테이션
힌트

URL 쿼리스트링, URL 경로 변수, HTTP Body에서 각각 데이터를 바인딩합니다.

정답 및 해설

@RequestParam, @PathVariable, @RequestBody의 차이를 설명해주세요.

Spring MVC에서 클라이언트의 HTTP 요청 데이터를 컨트롤러 메서드의 파라미터로 바인딩하는 방법은 데이터가 어디에 위치하느냐에 따라 달라집니다. @RequestParam, @PathVariable, @RequestBody는 각각 쿼리 파라미터, URL 경로 변수, 요청 본문에서 데이터를 추출합니다. 이 차이를 명확히 이해하면 RESTful API를 올바르게 설계할 수 있습니다.

@RequestParam

개요

@RequestParamURL 쿼리 파라미터(Query String)를 컨트롤러 메서드의 파라미터에 바인딩합니다.

GET /users?page=1&size=20&keyword=spring
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                쿼리 파라미터 영역

기본 사용법

@RestController
@RequestMapping("/users")
public class UserController {

    // GET /users?page=1&size=20
    @GetMapping
    public Page<UserDto> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        return userService.findAll(page, size);
    }

    // GET /users/search?keyword=spring&active=true
    @GetMapping("/search")
    public List<UserDto> searchUsers(
            @RequestParam String keyword,
            @RequestParam(required = false) Boolean active) {
        return userService.search(keyword, active);
    }
}

주요 속성

@RequestParam(
    value = "q",         // 파라미터 이름 (생략 시 변수명 사용)
    required = false,    // 필수 여부 (기본값: true)
    defaultValue = ""    // 기본값 (required=false와 함께 사용)
)
String keyword

Map으로 받기

파라미터 이름을 미리 알 수 없을 때 Map으로 전체를 수신할 수 있습니다.

// GET /search?name=John&age=30&city=Seoul
@GetMapping("/search")
public List<UserDto> dynamicSearch(@RequestParam Map<String, String> params) {
    // params = {"name": "John", "age": "30", "city": "Seoul"}
    return userService.dynamicSearch(params);
}

여러 값 받기 (다중 값)

// GET /items?ids=1&ids=2&ids=3
@GetMapping("/items")
public List<ItemDto> getItems(@RequestParam List<Long> ids) {
    return itemService.findByIds(ids);
}

@PathVariable

개요

@PathVariable은 **URL 경로(Path)**의 일부를 변수로 추출합니다. RESTful API에서 리소스를 식별하는 데 주로 사용됩니다.

GET /users/42/orders/7
          ^^         ^
     userId=42   orderId=7

기본 사용법

@RestController
@RequestMapping("/users")
public class UserController {

    // GET /users/42
    @GetMapping("/{id}")
    public UserDto getUser(@PathVariable Long id) {
        return userService.findById(id);
    }

    // GET /users/42/orders/7
    @GetMapping("/{userId}/orders/{orderId}")
    public OrderDto getUserOrder(
            @PathVariable Long userId,
            @PathVariable Long orderId) {
        return orderService.findByUserIdAndOrderId(userId, orderId);
    }
}

변수명 불일치 시

URL 템플릿 변수명과 파라미터명이 다를 경우 value 속성으로 명시합니다.

// GET /users/42
@GetMapping("/{user-id}")
public UserDto getUser(@PathVariable("user-id") Long userId) {
    return userService.findById(userId);
}

required 속성

// 선택적 경로 변수 (Spring 4.3.3+)
@GetMapping({"/users", "/users/{id}"})
public Object getUsers(@PathVariable(required = false) Long id) {
    if (id == null) {
        return userService.findAll();
    }
    return userService.findById(id);
}

@RequestBody

개요

@RequestBody는 HTTP 요청의 **본문(Body)**을 Java 객체로 역직렬화합니다. 주로 Content-Type: application/json인 POST, PUT, PATCH 요청에서 사용됩니다. 내부적으로 HttpMessageConverter가 JSON → Java 객체 변환을 담당합니다.

POST /users
Content-Type: application/json

{
    "name": "홍길동",
    "email": "hong@example.com",
    "age": 30
}

기본 사용법

// Request DTO
public class CreateUserRequest {
    @NotBlank
    private String name;

    @Email
    private String email;

    @Min(1)
    private int age;

    // getter/setter 또는 @Data (Lombok)
}

@RestController
@RequestMapping("/users")
public class UserController {

    // POST /users
    @PostMapping
    public ResponseEntity<UserDto> createUser(
            @Valid @RequestBody CreateUserRequest request) {
        UserDto created = userService.create(request);
        return ResponseEntity
                .status(HttpStatus.CREATED)
                .body(created);
    }

    // PUT /users/42
    @PutMapping("/{id}")
    public UserDto updateUser(
            @PathVariable Long id,
            @RequestBody UpdateUserRequest request) {
        return userService.update(id, request);
    }
}

@Valid와 함께 사용

public class CreateUserRequest {
    @NotBlank(message = "이름은 필수입니다")
    private String name;

    @Email(message = "올바른 이메일 형식이어야 합니다")
    @NotNull
    private String email;

    @Min(value = 1, message = "나이는 1 이상이어야 합니다")
    @Max(value = 150, message = "나이는 150 이하이어야 합니다")
    private int age;
}

@PostMapping
public ResponseEntity<UserDto> createUser(
        @Valid @RequestBody CreateUserRequest request,
        BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        throw new ValidationException(bindingResult);
    }
    return ResponseEntity.ok(userService.create(request));
}

@RequestHeader

HTTP 헤더 값을 파라미터로 바인딩합니다.

@GetMapping("/protected")
public ResponseEntity<String> protectedEndpoint(
        @RequestHeader("Authorization") String authHeader,
        @RequestHeader(value = "Accept-Language", defaultValue = "ko") String language) {

    // authHeader = "Bearer eyJhbGciOiJIUzI1NiJ9..."
    // language = "ko"

    return ResponseEntity.ok("접근 성공");
}

@ModelAttribute

폼 데이터(application/x-www-form-urlencoded) 또는 쿼리 파라미터를 객체에 한 번에 바인딩합니다.

public class SearchFilter {
    private String keyword;
    private String category;
    private int minPrice;
    private int maxPrice;
    // getter/setter
}

// GET /products?keyword=노트북&category=IT&minPrice=100000&maxPrice=2000000
@GetMapping("/products")
public List<ProductDto> searchProducts(@ModelAttribute SearchFilter filter) {
    // filter.keyword = "노트북"
    // filter.category = "IT"
    // filter.minPrice = 100000
    return productService.search(filter);
}

@ModelAttribute@RequestParam을 하나씩 받는 것과 달리, 여러 파라미터를 하나의 객체로 묶어서 받을 수 있어 편리합니다.


비교 요약

데이터 위치별 어노테이션 선택

HTTP 요청
├── URL 경로:    GET /users/42          → @PathVariable
├── 쿼리 파라미터: GET /users?page=1     → @RequestParam
├── 요청 본문:    POST /users (JSON)    → @RequestBody
├── 요청 헤더:    Authorization: Bearer → @RequestHeader
└── 폼 데이터:    application/x-www-... → @ModelAttribute

실전 REST API 설계 예시

@RestController
@RequestMapping("/api/v1")
public class ProductController {

    // 단일 상품 조회: 경로 변수로 ID 전달
    // GET /api/v1/products/123
    @GetMapping("/products/{id}")
    public ProductDto getProduct(@PathVariable Long id) {
        return productService.findById(id);
    }

    // 상품 목록 검색: 쿼리 파라미터로 필터/페이징 전달
    // GET /api/v1/products?keyword=노트북&page=0&size=20
    @GetMapping("/products")
    public Page<ProductDto> searchProducts(
            @RequestParam(required = false) String keyword,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return productService.search(keyword, page, size);
    }

    // 상품 생성: 요청 본문으로 데이터 전달
    // POST /api/v1/products
    @PostMapping("/products")
    public ResponseEntity<ProductDto> createProduct(
            @Valid @RequestBody CreateProductRequest request,
            @RequestHeader("X-User-Id") Long userId) {
        ProductDto created = productService.create(request, userId);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }

    // 상품 수정: 경로 변수(ID) + 요청 본문(수정 내용)
    // PUT /api/v1/products/123
    @PutMapping("/products/{id}")
    public ProductDto updateProduct(
            @PathVariable Long id,
            @RequestBody UpdateProductRequest request) {
        return productService.update(id, request);
    }
}

요약 표

어노테이션데이터 위치HTTP 메서드Content-Type주요 용도
@RequestParamURL 쿼리 스트링주로 GET무관검색 필터, 페이징, 정렬
@PathVariableURL 경로GET/PUT/DELETE 등무관리소스 식별 (ID)
@RequestBodyHTTP 요청 본문POST/PUT/PATCHapplication/json객체 생성/수정
@RequestHeaderHTTP 헤더모든 메서드무관인증 토큰, 언어 설정
@ModelAttribute쿼리 파라미터/폼GET/POSTapplication/x-www-form-urlencoded폼 제출, 검색 조건 객체