Spring Boot (+ RESTful)

RESTful 웹 서비스 구축 - @PutMapping / @DeleteMapping

wy-family 2025. 1. 1. 20:03

PutMapping 은 수정하기 인데, 왜 CREATED 가 있는것인지,

여기서 @Valid 는 @RequestBody 만 @Valid 하는 것인지, 아니면 @PathVariable 까지 포함해서 @Valid 하는 것인지.

book.set~~() 을 해서 수정되는 데이터를 교체해줬는데, 왜 book=bookService.save(book); 을 해주는것인지,
그리고 save 를 해주게 되면, id 값은 새롭게 부여되는것인지,
아니면 id 값은 그대로 있고 @PrePersist 인 createdAt 의 값만 LocalDateTime.now() 를 받아서
새롭게 부여되는것인지가 궁금해.

 

질문에 대한 답변:

1. @PutMapping과 HttpStatus.CREATED의 조합:

  • **@PutMapping**은 일반적으로 리소스를 수정할 때 사용됩니다.
  • 그러나 여기서 HttpStatus.CREATED를 사용하는 것은 약간 이상합니다. **HttpStatus.CREATED**는 새 리소스가 생성되었음을 나타내기 때문에 일반적으로 @PostMapping과 함께 사용됩니다.
  • 이 코드에서는 수정 작업을 수행하므로, HttpStatus.OK나 HttpStatus.NO_CONTENT가 더 적합할 것입니다.

2. @Valid의 적용 범위:

  • @Valid는 @RequestBody에만 적용됩니다.
    • 즉, BookPayloadDTO 클래스의 유효성 검사를 수행합니다.
    • @PathVariable에는 별도로 유효성 검사를 적용하지 않습니다.
    • 만약 @PathVariable에도 유효성 검사가 필요하다면, 커스텀 Validator나 다른 유효성 검증 메커니즘을 추가로 구현해야 합니다.

3. book.set~~() 후 book = bookService.save(book);을 호출하는 이유:

  • book.set~~()으로 데이터 값을 변경했지만, 변경된 엔티티를 영속화(Persistence) 계층에 저장하려면 save()를 호출해야 합니다.
    • JPA의 영속성 컨텍스트는 엔티티의 상태를 추적합니다.
    • save()를 호출하면 변경된 엔티티가 데이터베이스에 반영됩니다.
    • save()는 다음과 같은 작업을 수행합니다:
      • 새로운 엔티티: 데이터베이스에 새로 삽입.
      • 기존 엔티티: 데이터베이스에서 수정(Update).

4. save()를 호출하면 id 값은 어떻게 되는지:

  • id 값은 유지됩니다:
    • JPA는 id 필드를 기준으로 엔티티가 새로운 것인지 기존 것인지 판별합니다.
    • id가 이미 존재하는 경우, 해당 엔티티는 "수정" 작업으로 처리되며, 새로운 id가 부여되지 않습니다.
    • 그러나 @PrePersist가 아닌 **@PreUpdate**가 적용되면 수정 시 createdAt이 변경되지 않고, updatedAt 필드를 별도로 관리하는 것이 일반적입니다.

5. @PrePersist와 createdAt의 동작:

  • **@PrePersist**는 새 엔티티가 처음으로 저장되기 전에 호출됩니다.
  • 수정 시에는 **@PreUpdate**를 사용하는 것이 일반적입니다.
  • 따라서, 수정 작업에서 createdAt 필드가 변경되지 않고 유지되려면, 수정 시에는 @PrePersist가 작동하지 않도록 해야 합니다.
    • 수정 시 LocalDateTime.now()로 createdAt이 갱신된다면 이는 잘못된 동작입니다.
    • 수정 시점의 시간을 기록하려면 별도의 updatedAt 필드를 추가하고, @PreUpdate를 사용해야 합니다.

개선 제안:

  1. HttpStatus.CREATED를 HttpStatus.NO_CONTENT 또는 HttpStatus.OK로 변경:
    • @PutMapping에 맞는 상태 코드를 사용하는 것이 RESTful 원칙에 적합합니다.
  2. createdAt 필드 유지:
    • @PrePersist 대신 수정 전에는 @PreUpdate를 사용하고, createdAt을 변경하지 않도록 로직을 수정하세요.
  3. updatedAt 필드 추가:
    • 수정 시점을 저장하려면 updatedAt 필드를 추가하고 @PreUpdate로 관리하는 것을 권장합니다.
  4. 유효성 검증:
    • @PathVariable에도 유효성 검증이 필요하다면 커스텀 유효성 검증 로직을 추가하거나, 별도의 어노테이션을 사용하는 것을 고려하세요.

DeleteMapping 은 삭제하기 인데, 왜 CREATED 가 있는것인지,

그리고 id 값을 받았으면 그걸로 해당 id 값의 정보가 있는 isPresent 로 확인을 했으면,
그리고 나서는 id 값으로 delete를 하면 되는데, 여기서는 굳이 왜 Book book;,book=optionalBook.get(); 이 있는걸까?

 

// 내 생각에는,
@DeleteMapping(value = "/books/{id}")
@ApiResponse(responseCode = "202", description = "Book deleted")
@Operation(summary = "Book delete")
public ResponseEntity<?> delete_Book(@PathVariable Long id){
    Optional<Book> optionalBook=bookService.findById(id);
    if(optionalBook.isPresent()){
        bookService.deleteById(id);
        return ResponseEntity.status(HttpStatus.ACCEPTED).body(null); 
    }else{
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
    }
} 
//이렇게 하면 될거 같은데, 아닌가?

1. @ResponseStatus(HttpStatus.CREATED)가 적절하지 않은 이유

  • HttpStatus.CREATED는 새로운 리소스가 생성되었음을 의미합니다.
  • 삭제 작업(@DeleteMapping)에는 HttpStatus.CREATED는 부적합합니다.
  • 삭제 작업에서는 일반적으로 HttpStatus.NO_CONTENT (204) 또는 **HttpStatus.ACCEPTED (202)**를 사용하는 것이 RESTful API 설계 원칙에 맞습니다.

2. Book book 변수와 optionalBook.get() 호출의 불필요성

  • 코드에서 Book book 변수와 optionalBook.get() 호출은 실질적으로 불필요합니다. 이유는 다음과 같습니다:
    • 삭제 작업에서는 엔티티의 필드나 값 자체를 활용하지 않고, 단순히 ID를 기준으로 삭제 작업을 수행합니다.
    • optionalBook.isPresent()로 해당 ID의 존재 여부만 확인하면 충분하며, 객체를 가져올 필요는 없습니다.

3. 추가적으로 고려할 사항

  • HttpStatus.BAD_REQUEST의 적합성:
    • BAD_REQUEST (400)은 보통 클라이언트 요청 자체가 잘못되었을 때 사용하는 상태 코드입니다.
    • 여기서는 삭제하려는 ID가 존재하지 않는 경우이므로, **HttpStatus.NOT_FOUND (404)**가 더 적합할 수 있습니다.

최종 개선 코드:

@DeleteMapping(value = "/books/{id}")
@ApiResponse(responseCode = "202", description = "Book deleted")
@Operation(summary = "Book delete")
public ResponseEntity<?> delete_Book(@PathVariable Long id) {
    if (bookService.findById(id).isPresent()) {
        bookService.deleteById(id);
        return ResponseEntity.status(HttpStatus.ACCEPTED).body(null);
    } else {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Book not found");
    }
}

4. 이 코드의 장점

  1. 불필요한 변수 제거: Book book 변수를 선언하지 않음으로써 간결한 코드를 작성.
  2. 적합한 상태 코드 사용:
    • 리소스 삭제 성공 시 202 ACCEPTED.
    • 삭제 대상이 없을 경우 404 NOT_FOUND를 반환.
  3. 더 간결하고 직관적: 논리 흐름이 더 명확하며, 코드 가독성이 향상됩니다.

swagger 에서 확인해보고 테스트도 해보자.

 

실제 DB 에도 수정이 되었다는 걸 MySQL workbench 에 들어가서 확인해보자.

삭제 DELETE 도 해보자.

202 는 ACCEPTED 의 응답코드/상태코드이다. @DeleteMapping 의 return 을 보면 ACCEPTED 가 있다.

id 2 에 해당하는 데이터가 삭제가 되어 있는 것까지 확인 완료!


책의 이미지를 서버에다가 업로드하고 다운로드하는 RESTful,

Table 을 지금은 1개인데, 다른 table 을 여러 개 만들어서 관계 설정까지 해보는 것까지 해볼 것이다.

다음 게시물로.