Controller Restful 관련

2024. 11. 11. 16:05Java

Restful에 신경 써서 코드를 작성하는 중 보통 내가 하는 방식과는 다르게 작성하기에 궁금한 부분과 이런 방식도 있다는 것을 남기기 위해

해당 본문을 작성했습니다.

첫 번째

@RestController
@RequestMapping("/v1/errands")
@RequiredArgsConstructor
public class ErrandController {

    private static final String BASE_URI = "/v1/errands/";
    private final ErrandService errandService;

    @PostMapping("/orders/{orderId}/errands")
    public ResponseEntity<Void> registerErrands(@PathVariable(value = "orderId") final Long orderId,
                                                @RequestBody final RegisterErrandRequest registerErrandRequest,
                                                @LoginUser final Long memberId) {
        RegisterErrandCommand registerErrandCommand = RegisterErrandCommand.of(orderId, memberId, registerErrandRequest.estimateMinutes());
        Long errandId = errandService.registerErrand(registerErrandCommand);
        URI location = URI.create(BASE_URI + errandId);
        return ResponseEntity.created(location).build();
    }

위 코드에서 보통 저의 경우에는 @RequestMapping을 지정한 이후 BASE_URI를 지정하지 않고
최대한 간단명료하게 사용하려고 합니다.

하지만 위 코드는 BASE_URI를 사용하고

URI location = URI.create(BASE_URI + errandId);
이런 식으로 접근하는데

한 가지 든 생각이 이렇게 되면
@RequestMapping의 "/v1/errands"가 한번 더 호출되어서 API가 "/v1/errands/v1/errands/orders/{orderId}/errands" 이런 식으로
호출되지 않을까?라는 생각이 문득 들었습니다.

Claude AI의 답변은 이렇게 나왔습니다.

아니요, 그렇지 않습니다. URI.create(BASE_URI + errandId)에서 BASE_URI는 단순히 Location 헤더에 들어갈 URI를 만들기 위한 문자열일 뿐입니다.
실제 API 엔드포인트는 @RequestMapping과 @PostMapping에 의해 결정되며, 이 경우:

@RequestMapping("/v1/errands")
@PostMapping("/orders/{orderId}/errands")

따라서 실제 API 엔드포인트는:
CopyPOST /v1/errands/orders/{orderId}/errands
이렇게 됩니다.
Location 헤더는 새로 생성된 리소스의 URI를 나타내는 용도로, 201 Created 응답에서 사용됩니다. 이 경우 생성된 심부름(errand)의 위치를 나타내게 됩니다.
예를 들어 orderId가 123일 때 요청을 보내면:

요청 URL: POST /v1/errands/orders/123/errands
응답의 Location 헤더: /v1/errands/{생성된 errandId}

이렇게 됩니다.

이렇게 하나만 보면 이해가 쉽지 않습니다

Location 헤더는 새로 생성된 리소스의 URI를 나타내는 용도로, 201 Created 응답에서 사용됩니다. 이 경우 생성된 심부름(errand)의 위치를 나타내게 됩니다.
예를 들어 orderId가 123일 때 요청을 보내면:
요청 URL: POST /v1/errands/orders/123/errands
응답의 Location 헤더: /v1/errands/{생성된 errandId}

// 1. 먼저 심부름을 등록하고 ID를 받아옴
Long errandId = errandService.registerErrand(registerErrandCommand);

// 2. 받아온 ID로 새로운 리소스의 위치를 생성
URI location = URI.create(BASE_URI + errandId);

// 3. 201 Created 응답과 함께 Location 헤더에 새 리소스 위치를 포함
return ResponseEntity.created(location).build();
  1. 클라이언트가 /v1/errands/orders/123/errands로 POST 요청
  2. 서버에서 심부름이 생성되고 ID가 456이 할당됨
  3. Location 헤더에는 /v1/errands/456이 설정됨
  4. 클라이언트는 이 Location 헤더를 통해 생성된 리소스에 바로 접근 가능

이는 REST의 HATEOAS(Hypertext As The Engine Of Application State) 원칙을 따르는 것으로, 클라이언트가 생성된 리소스를 쉽게 찾을 수 있도록 하는 설계입니다.

@PostMapping
public ResponseEntity<CreateOrderResponse> createOrder(@Valid @RequestBody final CreateOrderRequest createOrderRequest,
                                                       @LoginUser final Long memberId) {
    CreateOrdersCommand createOrdersCommand = CreateOrdersCommand.of(memberId, createOrderRequest);

    return ResponseEntity.status(HttpStatus.CREATED).body(orderService.createOrder(createOrdersCommand));
}
  1. Order 생성
  2. // OrderController에서 @PostMapping public ResponseEntity <CreateOrderResponse> createOrder(...) { // 주문 생성 후 orderId 반환 return ResponseEntity.status(HttpStatus.CREATED). body(orderService.createOrder(createOrdersCommand)); }
  3. 생성된 OrderID를 사용해 Errand 등록
  4. // ErrandController에서 @PostMapping("/orders/{orderId}/errands") public ResponseEntity <Void> registerErrands(@PathVariable(value = "orderId") final Long orderId,...) { // 앞서 생성된 orderId를 사용해서 심부름 등록 Long errandId = errandService.registerErrand(registerErrandCommand); ... }

이렇게 진행됩니다 물론 Order 와 Errand Entity는 @OneToOne으로 일대일 매핑된 상태

두번째

@PatchMapping("/{errandId}/pickup")
public ResponseEntity<Void> startErrand(@PathVariable final Long errandId,
                                        @RequestBody @Valid StartErrandRequest startErrandRequest,
                                        @LoginUser final Long youngerId) {
    StartErrandCommand startErrandCommand = StartErrandCommand.of(errandId, startErrandRequest.errandEstimateMinutes(), youngerId);
    errandService.startErrand(startErrandCommand);
    return ResponseEntity.noContent().build();
}

@PatchMapping("/{errandId}/complete")
public ResponseEntity<Void> completeErrand(@PathVariable final Long errandId,
                                           @LoginUser final Long youngerId) {
    CompleteErrandCommand completeErrandCommand = CompleteErrandCommand.of(errandId, youngerId);
    errandService.completeErrand(completeErrandCommand);
    return ResponseEntity.noContent().build();
}

두번째 코드를 보며 궁금했던 것은
여길 보면 return ResponseEntity.noContent().build()로 끝나는 상황입니다.
@PatchMapping인데 OK로도 충분할텐데 왜 이렇게 구성한걸까? 라는 부분으로 글을 쓰는데

AI의 답변은

REST API에서 응답 코드를 선택할 때는 "의미"에 초점을 맞추는 것이 좋습니다.
204 No Content와 200 OK의 주요 차이점은:
200 OK:

"요청이 성공했고, 응답 본문에 결과가 있다"는 의미
보통 응답 본문(response body)에 데이터를 포함할 때 사용

204 No Content:

"요청이 성공했고, 응답 본문이 없다"는 의미
특히 UPDATE/PATCH/DELETE 작업 후 클라이언트에게 돌려줄 데이터가 없을 때 적합

이 코드에서 204를 사용한 이유는:

PATCH 요청 후 변경된 리소스를 반환할 필요가 없음
클라이언트는 단순히 작업 성공 여부만 알면 됨
불필요한 응답 본문을 줄여 네트워크 대역폭 절약

따라서 이 경우 204 No Content가 더 정확한 의미를 전달합니다. 물론 200 OK를 사용해도 틀린 것은 아니지만, REST의 의미론적 관점에서는 204가 더 적절한 선택이라고 볼 수 있습니다.

// 현재 코드: 더 명확한 의미 전달
return ResponseEntity.noContent().build();  // 204 응답

// 대안: 작동은 하지만 덜 정확한 의미
return ResponseEntity.ok().build();  // 200 응답

PATCH 요청 후 변경된 리소스를 반환할 필요가 없음
클라이언트는 단순히 작업 성공 여부만 알면 됨
불필요한 응답 본문을 줄여 네트워크 대역폭 절약

 

생각해보니 어플리케이션 특성상 간단 명료하게 전달만 해주면 되는 부분이니

ok() 보다는 noContent()가 더 맞는 방향이라고 생각하게 되었습니다.

앞으로 참고해서 프로젝트를 진행 해야겠습니다.

 

'Java' 카테고리의 다른 글

RedisTemplate 의 객체지향  (1) 2024.11.20
Kafka 적응기  (1) 2024.11.13
JWT 그런데 OAuth2.0 곁들인 (2)  (1) 2024.11.10
JWT 그런데 OAuth2.0 곁들인  (1) 2024.11.08
SSE(Server-Sent Events)  (0) 2024.11.06