@PathVariable vs @RequestParam / @RequestPart

양성준·2025년 4월 3일

스프링

목록 보기
24/49

@PathVariable vs @RequestParam

  • @PathVariable
    • 오직 URL 경로(= 경로 변수)에 포함된 값을 바인딩하는 데에만 사용
    • 하나의 ID로 식별할 때 사용
    • 식별 결과가 하나일 때
    • 단건 조회, 단건 수정 API 등
  • @RequestParam
    • 다수의 조건을 통해 필터링을 할 때 사용
    • 결과가 여러건일 수도 있을 때
    • 채널 내 메시지 조회 API 등
    • 별개로, 쿼리스트링뿐만 아니라 multipart/form-data나 application/x-www-form-urlencoded 등의 form data도 받음
      • JSON이 들어올 경우, 동작하지 않으므로 그 때는 @RequestPart 사용 (JSON + multipart/form-data)

=> 이렇게 간단하게 나눠지면 정말 좋을 것 같지만, 실무에서는 생각보다 복잡한 이유가 존재한다. 몇가지의 예시를 살펴보자.

예시 1 (완전한 종속관계, 1:1)

  @PatchMapping("/{userId}/user-status")
  public ResponseEntity<UpdateUserStatusResponseDTO> updateUserStatus(
      @PathVariable("userId") UUID id) {
    UserStatus userStatus = userStatusService.updateByUserId(id);
    return ResponseEntity.ok(userStatusMapper.toUpdateUserStatusResponseDTO(userStatus));
  }
  • userStatus는 user가 없다면 존재할 수 없고, 1:1 관계가 확실함
  • 이 경우, @RequestParam을 사용하는 것보다는 @PathVariable을 사용하여
    api/users/{userId}/user-status 구조로 확실한 종속관계를 표현해주는 것이 좋다.
    + 단 하나의 userId로 식별 가능

예시 2 (애매한 종속관계, 1:N)

  @GetMapping("channels/{channelId}/messages")
  public ResponseEntity<MessageListDTO> getChannelMessages(@PathVariable("channelId") UUID id) {
    List<MessageDTO> messageDTOList = messageService.findAllByChannelId(id);
    return ResponseEntity.ok(new MessageListDTO(messageDTOList));
  }
  
  @GetMapping("/messages")
  public ResponseEntity<MessageListDTO> getChannelMessages(@RequestParam("channelId") UUID id) {
    List<MessageDTO> messageDTOList = messageService.findAllByChannelId(id);
    return ResponseEntity.ok(new MessageListDTO(messageDTOList));
  } 
  • 이 경우엔, @PathVariable과 @RequestParam을 동시에 사용할 수 있다.
  • @PathVariable의 경우, 요구사항에서 Message가 Channel에 종속적일 때 사용 가능
    • 1:N 관계더라도 '특정 채널 안의 메시지를 보고싶다'라는 종속적 요구사항이 확실할 때 사용
    • 사용하더라도, Channel 기준에서 Message를 바라보는 것이기 때문에 ChannelController에 존재해야함
      • MessageController에 존재할 경우 api/messages/channels/id/messages라는 괴상한 URI..
    • ChannelService에서 MessageService를 의존성 주입받아 조회하는 식으로 구현
  • @RequestParam의 경우, Message가 Channel이 없어도 존재할 수 있고, 1:N 관계일 때 사용 가능
    (관계보단, 종속성이 더 중요)
  @GetMapping
  public ResponseEntity<ReadStatusListDTO> getUserReadStatus(@RequestParam("userId") UUID userId) {
    List<ReadStatusDTO> readStatusDTOList = readStatusService.findAllByUserId(userId);
    return ResponseEntity.ok(new ReadStatusListDTO(readStatusDTOList));
  }
  • ReadStatus가 User에 종속적이긴하나, N:1 관계이고 / Channel에도 종속적이므로, 리소스 간 계층이 명확하지 않아 @PathVariable을 사용하기엔 애매하다.
  • @RequestParam으로 해당 userId를 가진 ReadStatus들을 필터링한다는 느낌으로 사용

예시 3 (각각 독립적인 엔티티)

  @GetMapping("/{userId}/channels")
  public ResponseEntity<ChannelListResponseDTO> getChannelsByUserId(@PathVariable("userId") UUID userId) {
    List<FindChannelDTO> findChannelDTO = channelService.findAllByUserId(userId);
    return ResponseEntity.ok(new ChannelListResponseDTO(findChannelDTO));
  }
  
    @GetMapping
  public ResponseEntity<ChannelListResponseDTO> getChannelsByUserId(@RequestParam("userId") UUID userId) {
    List<FindChannelDTO> findChannelDTO = channelService.findAllByUserId(userId);
    return ResponseEntity.ok(new ChannelListResponseDTO(findChannelDTO));
  }
  • 해당 유저가 들어가있는 채널을 구하는 API
  • 유저와 채널은 각각 독립적인 엔티티이기 때문에, userId 식별자로 channelList를 구하는 것도 이상함
  • 그러므로, 이 경우에는 @RequestParam을 통해 userId를 가진 channel을 필터링해준다는 느낌으로 사용!

정리

  • 종속적으로 표현하고 싶다면 @PathVariable, 각각을 독립적으로 바라본다면 @RequestParam 사용
  • 1:N, 1:1은 어노테이션을 나누는 기준이 아님
    • 1:N 관계더라도 "하위 리소스"라는 개념을 강조하고 싶다면 @PathVariable을 써도 상관 없다.

=> 중요한 건 데이터 모델이 아니라 API에서 리소스를 어떻게 바라보느냐(종속성)임

@RequestParam vs @RequestPart vs @RequestBody

@PostMapping("/upload")
public String upload(
    @RequestParam String name,
    @RequestParam MultipartFile file
) {
    ...
}
  • 단순 텍스트 + 파일이면 @RequestParam 사용 가능
@PostMapping("/upload")
public String upload(
    @RequestPart("info") UserDto info,      // JSON
    @RequestPart("file") MultipartFile file
) {
    ...
}
  • multipart 요청 내에 JSON 객체가 포함되어 있다면 @RequestPart를 사용해야
    내부적으로 HttpMessageConverter가 JSON → 자바 객체로 변환해줌
  • multipart/form-data의 경우, @RequestBody 사용 불가능 (@RequestBody는 JSON 요청만 변환 가능)
    • multipart/form-data에 JSON 메시지도 포함 가능, 우리가 메시지 등록할 때 메시지 + 파일을 함께 보내는데,
      이게 multipart로 하나로 묶여서 JSON과 파일데이터가 함께 포함되어있는 것임
      -> 이를 받으려면 @RequestPart로 JSON과 파일 데이터를 따로 받아줘야함
profile
백엔드 개발자를 꿈꿉니다.

0개의 댓글