co-cherry
게시판 CRUD 본문
앞서 게시글 생성과 상세 조회를 구현했다.
이번 시간에는 게시글 삭제와 수정을 구현해보려고 한다.
게시글 삭제와 수정은 작성자만 가능하므로 작성자 권한 검증이 서비스 로직에 들어가야 함을 유의하고 API를 설계해보자.
게시글 삭제

게시글을 삭제하기 위해서는 삭제할 게시글의 Id와 권한 검증을 위해 해당 게시글을 삭제할 user의 Id가 필요하다.
게시글을 삭제하기 전에, 아래 두 가지의 검증이 필요하다.
- 해당 아이디의 게시글이 존재하는지 확인 → 아니라면 POST_NOT_FOUND 반환
- 해당 게시글의 작성자가 삭제 요청을 보낸 user와 일치하는지 확인(권한 검증) → 아니라면 POST_UNAUTHORIZED 반환
@Transactional
public void deletePost(Long postId, Long userId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new GeneralException(PostErrorCode.POST_NOT_FOUND));
if (!post.getUser().getId().equals(userId)) {
throw new GeneralException(PostErrorCode.POST_UNAUTHORIZED);
}
postRepository.delete(post);
}
- postRepository.delete에서 delete는 JpaRepository에서 기본으로 제공하는 메서드
후에 Controller를 아래와 같이 작성하면 된다.
반환값은 성공적으로 요청을 처리했다는 뜻에서 GeneralSuccessCode.OK를 반환했다.
게시글을 삭제했으므로 result는 null로 처리했다.
@DeleteMapping("/{postId}")
public ApiResponse<Void> deletePost(@PathVariable Long postId, @RequestParam Long userId) {
postService.deletePost(postId, userId);
return ApiResponse.onSuccess(GeneralSuccessCode.OK, null);
}
게시글 수정
게시글 수정을 구현할 때, PUT과 PATCH 중 어떤 것을 사용해야 할까 고민하다가 아래 글을 보게 되었다.
https://docs.tosspayments.com/blog/rest-api-post-put-patch
POST, PUT, PATCH의 차이점 | 토스페이먼츠 개발자센터
REST API 디자인의 기본이 되는 POST, PUT, PATCH 메서드를 자세히 살펴볼게요.
docs.tosspayments.com
멱등성(Idempotent)
같은 요청을 여러 번 해도 결과가 동일한 성질
- 멱등성 있음: 재시도 해도 안전
- 멱등성 없음: 매번 다른 결과 발생

- PUT 100번 요청을 하더라도 전체 리소스를 완전히 교체하므로 멱등성 보장
- PATCH 동시에 2개의 PATCH 요청이 오는 경우, 멱등성을 보장할 수 없음(비멱등성)
따라서 PATCH의 경우, 필요한 필드만 전달하는 이점이 있지만, 동시 요청 시 데이터 손실이나 예상치 못한 결과가 발생할 수 있어 멱등성을 보장하지 않는다.
이에 따라, 게시글 수정 API를 PUT으로 작성하기로 했다.

PUT을 통해 게시글 수정 시, 일부 값만 수정하는 것이 아닌 전체 값을 포함해야 하므로 DTO를 이와 같이 작성했다.
제목과 내용 둘 중 하나라도 빠지면 안 되기 때문에 @NotBlank 조건을 추가했다.
@Getter
public class PostUpdateRequestDto {
@NotBlank(message = "제목은 필수입니다.")
@Size(max = 50, message = "제목은 50자 이하여야 합니다.")
private String title;
@NotBlank(message = "내용은 필수입니다.")
@Size(max = 1000, message = "내용은 1000자 이하여야 합니다.")
private String content;
}
또, 필드 수정 시 Setter를 사용할 것인가를 두고 고민이 있었다.
Setter를 사용하면 간단하게 코드를 작성할 수 있는 이점이 있지만, 아래 글들을 참고해보면 지양하라는 말밖에 없었다...
https://colabear754.tistory.com/173
[OOP] Getter와 Setter는 지양하는게 좋다
목차 들어가기 전에 얼마 전 사내에서 Getter와 Setter를 함부로 사용하면 안되는 이유에 대한 세미나가 있었다. Setter에 대한 이야기는 워낙 많이 알려져있었지만 Getter에 대한 이야기는 잘 하지 않
colabear754.tistory.com
https://octoping.tistory.com/33
사내 세미나 - Getter와 Setter를 함부로 사용하면 안되는 이유;;
들어가기 앞서 지난 번에 작성했던 사내 세미나 - 테스트 코드에 대해 알아보자 세미나의 다음 편으로 진행한 세미나이다. Getter와 Setter의 사용을 금지하라 '리팩토링' 책의 저자로 유명한 Martin F
octoping.tistory.com
https://kcode-recording.tistory.com/339
[Spring] Getter/Setter를 지양하자?
편리해 보이는 @Getter / @Setter를 지양해야 한다? 지양을 해야하는 이유를 알기 전 @Getter / @Setter 애너테이션에 대해 먼저 알고가자! @Getter / @Setter 애너테이션이란? 자바를 공부하면서 객체 지향 프
kcode-recording.tistory.com
위 글들을 참고해 간단히 정리하자면,
Setter를 사용하면 안 되는 이유
1. 불명확한 의도(변경 의도를 알 수 없음)
// ❌ Setter — 뭘 하려는 건지 모름
post.setTitle("새 제목");
post.setContent("새 내용");
// ✅ Update() — 게시글을 수정한다는 의도가 명확
post.update("새 제목", "새 내용");
2. 검증 로직 추가 불가
// ❌ Setter — 빈 제목도 그냥 들어감
post.setTitle("");
// ✅ Update() — 메서드 안에서 검증 가능
public void update(String title, String content) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("제목은 비울 수 없습니다.");
}
this.title = title;
this.content = content;
}
3. 객체 일관성 유지의 어려움
// ❌ @Setter를 클래스에 붙이면 이런 것도 가능해짐
post.setCreatedAt(LocalDateTime.now()); // 생성일 조작 가능!
post.setId(999L); // PK 조작 가능!
// ✅ Update() — title, content만 딱 열림
// createdAt, id는 건드릴 수 없음
4. 다른 객체들로 책임이 분산
// ❌ Setter — 검증이 Service에도 있고, Controller에도 있고...
// PostService.java
post.setTitle(title);
// PostController.java
if (title.isBlank()) { ... } // 검증이 다른 곳에 따로 있음
// ✅ Update() — 로직이 엔티티 안에 응집
public void update(String title, String content) {
// 검증 + 변경이 한 곳에
}
| 구분 | Setter | Update 메서드 |
| 캡슐화 | 약함(아무나 변경 가능) | 강함 |
| 비즈니스 로직 | 없음 | 로직 포함 가능 |
| 검증 | 검증 불가 | 검증 가능 |
| 복잡도 | 간단 | 복잡 |
| 버그 위험 | 높음 | 낮음 |
따라서, @Setter를 사용하는 대신 엔티티에 Update 메서드를 추가했다.
public void update(String title, String content) {
this.title = title;
this.content = content;
}
이 메서드를 활용해 서비스 로직을 작성해보자.
삭제에서와 같이 수정 시, 아래 두 가지 검증이 필요하다.
- 해당 아이디의 게시글이 존재하는지 확인 → 아니라면 POST_NOT_FOUND 반환
- 해당 게시글의 작성자가 수정 요청을 보낸 user와 일치하는지 확인(권한 검증) → 아니라면 POST_UNAUTHORIZED 반환
둘 다 해당한다면, update 함수를 활용해 title과 content를 수정한다.
@Transactional
public PostResponseDto updatePost(Long postId, Long userId, PostUpdateRequestDto request) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new GeneralException(PostErrorCode.POST_NOT_FOUND));
if (!post.getUser().getId().equals(userId)) {
throw new GeneralException(PostErrorCode.POST_UNAUTHORIZED);
}
post.update(request.getTitle(), request.getContent());
return new PostResponseDto(
post.getId(),
post.getUser().getId(),
post.getTitle(),
post.getContent()
);
}
Controller는 아래와 같이 작성한다.
위에서 작성한 서비스를 실행해 반환받은 DTO 값을 result로 반환한다.
@PutMapping("/{postId}")
public ApiResponse<PostResponseDto> updatePost(
@PathVariable Long postId,
@RequestParam Long userId,
@Valid @RequestBody PostUpdateRequestDto request) {
return ApiResponse.onSuccess(GeneralSuccessCode.OK, postService.updatePost(postId, userId, request));
}
댓글

ERDCloud로 댓글(Comment)과 게시글(Post), 유저(User) 간 관계를 표현해보았다.
댓글은 수정 API를 따로 만들지 않았으나, 확장 가능성을 두기 위해 수정 일자 필드도 추가했다.
아래는 댓글의 엔티티다.
하나의 작성자는 여러 개의 댓글을 달 수 있으므로 1:N 관계
하나의 게시글에서 여러 개의 댓글을 달 수 있으므로 1:N 관계 로 명시했다.
또한, Post 때 처럼 setter 대신 엔티티 내 메서드를 이용해서 수정하도록 update 메서드를 작성했다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@Table(name = "comment")
@EntityListeners(AuditingEntityListener.class)
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "comment_id")
private Long id;
@Column(name = "content", nullable = false)
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@CreatedDate
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
public void update(String content) {
this.content = content;
}
}
댓글 생성

DB에 접근하고 간편한 사용을 위해 엔티티 다음 레포지토리를 생성했다.
public interface CommentRepository extends JpaRepository<Comment,Long> {
}
또한, 댓글 생성 시 필요한 Request DTO와 Response DTO를 이와 같이 선언했다.
@Getter
public class CommentRequestDto {
@NotNull(message = "사용자 ID는 필수입니다.")
private Long userId;
@NotNull(message = "게시글 ID는 필수입니다.")
private Long postId;
@NotBlank(message = "댓글 내용은 필수입니다.")
@Size(max = 500, message = "댓글은 500자 이하여야 합니다.")
private String content;
}
@Getter
@AllArgsConstructor
public class CommentResponseDto {
private Long commentId;
private Long postId;
private Long userId;
private String content;
}
서비스 로직은 게시글 생성과 비슷하게 작동하되, postId 검증 부분이 추가되었다.
게시글 존재 여부와 작성자(user) 존재 여부를 먼저 확인한 후, 댓글을 생성한다.
@Transactional
public CommentResponseDto createComment(CommentRequestDto request) {
User user = userRepository.findById(request.getUserId())
.orElseThrow(() -> new GeneralException(CommentErrorCode.USER_NOT_FOUND));
Post post = postRepository.findById(request.getPostId())
.orElseThrow(() -> new GeneralException(CommentErrorCode.POST_NOT_FOUND));
Comment comment = Comment.builder()
.content(request.getContent())
.user(user)
.post(post)
.build();
Comment savedComment = commentRepository.save(comment);
return new CommentResponseDto(
savedComment.getId(),
savedComment.getPost().getId(),
savedComment.getUser().getId(),
savedComment.getContent()
);
}
생성 완료 시, 리소스가 성공적으로 생성되었다는 CREATED 코드를 반환하고 result로 생성한 Comment를 반환한다.
@PostMapping
public ApiResponse<CommentResponseDto> createComment(@Valid @RequestBody CommentRequestDto request) {
return ApiResponse.onSuccess(GeneralSuccessCode.CREATED, commentService.createComment(request));
}
댓글 삭제

게시글 삭제 때와 동일하게, 댓글 존재 여부와 댓글 작성자가 현재 댓글 삭제를 요청하는 유저와 동일한지 검증한다.
@Transactional
public void deleteComment(Long commentId, Long userId) {
Comment comment = commentRepository.findById(commentId)
.orElseThrow(() -> new GeneralException(CommentErrorCode.COMMENT_NOT_FOUND));
if (!comment.getUser().getId().equals(userId)) {
throw new GeneralException(CommentErrorCode.COMMENT_UNAUTHORIZED);
}
commentRepository.delete(comment);
}
댓글이 정상적으로 삭제되면 OK 코드를 반환하고, 삭제되었으므로 result로는 null을 반환한다.
@DeleteMapping("/{commentId}")
public ApiResponse<Void> deleteComment(
@PathVariable Long commentId,
@RequestParam Long userId) {
commentService.deleteComment(commentId, userId);
return ApiResponse.onSuccess(GeneralSuccessCode.OK, null);
}
정상적으로 동작하는지 확인해보겠다.
게시글 작성을 하니 postId로 2가 반환되며, 작성한 게시글 내용이 보인다.

방금 작성한 게시글을 수정하니 아래와 같이 수정한 내용이 반환된다.

상세 조회를 해보아도 수정한 내용으로 보인다.

만약, 존재하지 않는 게시글을 삭제하려 한다면 게시글을 삭제할 수 없다고 404 에러 메세지가 나온다.

권한이 없는 사용자가 게시글을 삭제하려 하면 아래와 같은 403 에러 메세지가 뜬다.

정상적으로 삭제하면 이렇게 요청이 성공적으로 처리되었다는 말과 함께 null이 반환된다.

'Springboot' 카테고리의 다른 글
| 게시글 목록 API 구현하기 (페이징 · 검색, N+1) (0) | 2026.05.07 |
|---|---|
| 회원가입 및 로그인 구현 (0) | 2026.04.28 |
| ERD + DB + JPA 시작 (0) | 2026.03.24 |
| 예외 처리와 Validation (1) | 2026.03.13 |
| Spring Boot 시작하기 & REST API 기초 (0) | 2026.03.09 |