[데브코스] Spring Boot REST API 실습 (11강) - 댓글 삭제 API 구현과 @Transactional의 역할

zuno·2025년 12월 21일

이번 글은 데브코스 Spring Boot REST API 실습 11강 내용을 정리한 글이다.
11강에서는 댓글 삭제 API를 구현하면서,
왜 JPA에서 @Transactional이 없으면 삭제가 DB에 반영되지 않는지를 직접 체험한다.

또한 orphanRemoval = true 옵션이
어떤 방식으로 실제 DELETE SQL까지 이어지는지도 함께 정리한다.


1️⃣ 11강에서 구현한 기능

11강의 목표는 다음과 같다.

  • 특정 게시글의 댓글 삭제 API 구현
  • 개발 편의를 위해 GET 방식으로 삭제 요청
  • @Transactional이 없을 때 삭제 실패 원인 이해
  • 트랜잭션을 붙였을 때 정상 동작 확인

2️⃣ 댓글 삭제 API (초기 구현)

댓글 삭제 컨트롤러

@GetMapping("/{id}/delete")
public String delete(
        @PathVariable int postId,
        @PathVariable int id
) {
    Post post = postService.findById(postId).get();

    PostComment postComment = post.findCommentById(id).get();

    postService.deleteComment(post, postComment);

    return "%d번 댓글이 삭제되었습니다.".formatted(id);
}

코드만 보면:

  • 댓글을 찾고
  • 삭제 메서드를 호출하고
  • 메시지를 반환하므로

👉 정상적으로 삭제될 것처럼 보인다.

하지만 실제로는 DB에서 삭제가 일어나지 않았다.


3️⃣ 왜 삭제가 실패했을까?

핵심 원인은 단 하나다.

트랜잭션(@Transactional)이 없었다.


4️⃣ Post 엔티티의 연관관계 설정 다시 보기

@OneToMany(
    mappedBy = "post",
    fetch = LAZY,
    cascade = {PERSIST, REMOVE},
    orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();

그리고 댓글 삭제는 다음 메서드로 처리된다.

public boolean deleteComment(PostComment postComment) {
    if (postComment == null) return false;

    return comments.remove(postComment);
}

여기서 중요한 포인트는:

  • comments.remove()자바 컬렉션에서 제거
  • orphanRemoval = true 설정 덕분에
  • 고아가 된 댓글은 DB에서도 삭제 대상이 된다는 점이다

5️⃣ 그럼 왜 트랜잭션이 없으면 DB에 반영되지 않을까?

JPA의 기본 동작 원리

JPA는 다음과 같은 방식으로 동작한다.

  1. 엔티티 상태 변경 발생 (comments.remove())
  2. JPA가 변경 사항을 감지 (Dirty Checking)
  3. 트랜잭션 커밋 시점에
  4. 변경 내용을 DB에 반영 (flush → SQL 실행)

즉,

❌ 메서드 호출 순간에 DELETE SQL 실행
트랜잭션 종료 시점(commit)에 SQL 실행


트랜잭션이 없는 경우

  • 커밋 시점이 없음
  • flush 타이밍이 불명확
  • 결과적으로 DELETE SQL이 실행되지 않음

👉 그래서 컬렉션에서는 삭제된 것처럼 보이지만,
👉 DB에는 그대로 남아 있는 상태가 된다.


6️⃣ @Transactional을 붙여서 해결

댓글 삭제 메서드 수정

@GetMapping("/{id}/delete")
@Transactional
public String delete(
        @PathVariable int postId,
        @PathVariable int id
) {
    Post post = postService.findById(postId).get();

    PostComment postComment = post.findCommentById(id).get();

    postService.deleteComment(post, postComment);

    return "%d번 댓글이 삭제되었습니다.".formatted(id);
}

이제 흐름은 다음과 같다.

@Transactional 시작
↓
comments.remove(postComment)
↓
JPA 변경 감지
↓
@Transactional 종료 (commit)
↓
flush 발생
↓
DELETE SQL 실행

👉 DB에서도 댓글이 정상적으로 삭제된다.


7️⃣ 실제로 어떤 SQL이 실행될까?

트랜잭션 종료 시점에 JPA는 다음과 같은 SQL을 실행한다.

DELETE FROM post_comment
WHERE id = ?

이 과정에서:

  • repository.delete()를 직접 호출하지 않아도
  • orphanRemoval = true 설정 덕분에
  • 자동으로 DELETE가 처리된다

8️⃣ 조회 메서드에는 왜 readOnly = true 를 붙일까?

11강에서는 조회 API들에도 다음과 같이 설정했다.

@Transactional(readOnly = true)

이 설정의 의미는 다음과 같다.

  • 이 트랜잭션은 조회 전용
  • 변경 감지(Dirty Checking) 부담 감소
  • 코드 의도 명확화 (“여기서는 수정 안 함”)

즉, 관례적으로:

  • 조회 API → @Transactional(readOnly = true)
  • 변경 API → @Transactional

이 패턴을 많이 사용한다.


9️⃣ 11강의 핵심 정리

  • 댓글 삭제는 단순한 리스트 제거가 아니다
  • JPA는 트랜잭션 커밋 시점에 DB를 변경한다
  • orphanRemoval = true는 고아 엔티티를 DELETE 대상으로 만든다
  • 변경 작업에는 반드시 @Transactional이 필요하다
  • 조회 전용 메서드는 readOnly = true로 의도를 표현한다

💡 한 줄 요약

11강은 댓글 삭제 API를 통해
JPA에서 트랜잭션이 왜 필수인지를 직접 체험하는 강의였다.

0개의 댓글