인프런 커뮤니티 질문&답변

김태훈님의 프로필 이미지

작성한 질문수

실전! 스프링 데이터 JPA

게시판에서 삭제된 댓글을 보여주기 위해 Spring Data JPA에서는 어떻게 접근해야 할까요?

해결된 질문

작성

·

497

·

수정됨

1

상황 설명

기본적인 게시판을 만들고 있어요.

해당 게시판에는 게시물를 달 수 있고 해당 게시물에는 댓글을 달 수 있어요.

댓글과 관련한 요구사항들은 다음과 같습니다.

  1. 댓글 Create, Update, Delete

  2. 각 게시물은 몇 개의 댓글이 달렸는지 확인이 가능하다.

  3. 게시판에서는 전체 댓글이 몇 개가 달렸는지 확인이 가능하다.

  4. 게시물에 달려 있는 모든 댓글들을 확인할 수 있다. 다만, 삭제된 댓글의 경우 "삭제된 댓글입니다" 라는 메세지로 보여준다.

     

     

내 접근 방법(Where 어노테이션을 사용)

우선은 4번 조건 때문에, 그리고 실무에서 관리를 위해 데이터를 잘 삭제하지 않는다는 걸 근거로 Soft-Delete를 적용했습니다.

그리고 Comment 엔티티를 아래와 같이 작성했습니다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SQLDelete(sql = "UPDATE Comment SET deleted = true where comment_id = ?")
@Where(clause = "deleted = false")
public class Comment extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "comment_id")
    private Long id;

    private boolean deleted;

    ... 생략 ...
}

Comment 엔티티를 조회하는 대부분의 요청(1개 제외)은 deleted 필드가 false인걸 찾아와야 합니다. 그래서 디폴트 속성으로 deleted=false를 적용하면 편하겠다고 생각하여 Where 어노테이션을 사용했는데요.

 

문제점

이 방식의 문제는 4번 요구사항을 구현할 수 없다는 것입니다.

Spring Data JPA의 기본 메서드는 물론이고, JPQL, QueryDsl 을 사용한 모든 Comment 조회 쿼리에도 "deleted=false" 속성이 기본으로 달라붙어 deleted가 true인 Comment를 가져올 수 없습니다.

(확실하지는 않지만, Native Query를 사용하면 하이버네이트 구현체의 영향을 안받고 제가 원하는 기능을 구현할 수 있을 거 같습니다. 그런데 Native Query를 쓰는게 최선일까 자꾸 꺼려지더라구요.)

 

임시 방안

저는 어쩔 수 없이 Where 어노테이션을 제거하고, Comment에 관련한 모든 조회 쿼리를 JPQL로 만들어줬습니다.

하지만 고작 한 개의 메서드에서 삭제된 메서드를 보여주기 위해 전체 Comment 조회 메서드를 변경하는 게 마음에 들지 않습니다. 관리를 어렵게 만든다는 생각이 들어요.

실제로 저는 "게시판에서는 전체 댓글이 몇 개가 달렸는지 확인이 가능하다." 요구사항을 구현할 때, where deleted=false 조건을 붙이는 걸 깜빡해서 삭제된 댓글들의 개수까지 전부 보여줬습니다.

 

이러한 상황에서는 코드를 어떻게 작성하는 게 좋을까 계속 고민을 하고 있는데요,,, 함께 고민해주실 수 있을까 하여 이렇게 질문을 남깁니다. 감사합니다.

 

 

 

 

답변 2

3

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 김태훈님

말씀하신 문제점들 때문에 @Where은 권장하지 않습니다.

여기에는 3가지 대안이 있습니다.

1. JPQL등에서 직접 처리하기

실무에서는 다양한 상황들이 나타나기 때문에 모든 상황을 직접 다룰 수 있게 JPQL을 사용하는 것을 권장드립니다. 참고로 실무에서는 대부분의 조회 쿼리가 단순하지 않기 때문에 JPQL을 자주 사용하게 됩니다.

2. 하이버네이트 @Filter

대안으로는 하이버네이트 @Filter를 사용할 수 있는데, 이 기능을 사용하면 원하는 시점에 @Where와 같은 기능을 적용할 수 있습니다. 하지만 제대로 사용하기가 매우 복잡하고, 실수로 필터를 적용하지 않으면 또 버그가 발생하기 때문에 저는 권장하지는 않습니다. 또한 하이버네이트 전용 기능이어서 하이버네이트 session으로 전환하고 사용해야 하는데 이런 부분을 구현하기도 까다롭습니다.

3. 조회 전용 엔티티

조회 전용 엔티티를 만들어서 특별한 곳에서만 사용할 수 있습니다. 하지만 같은 엔티티가 2개가 되기 때문에 엔티티를 중복으로 관리해야 하는 문제가 있습니다.

조회 전용 엔티티 예시

package org.example.softdelete.entity;

import jakarta.persistence.*;
import lombok.Getter;

@Entity
@Getter
@Table(name = "comment")
public class ReadComment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "comment_id", insertable = false, updatable = false)
    private Long id;

    @Column(insertable = false, updatable = false)
    private boolean deleted;

}

정리하면 너무 고민하는 것 보다는 단순하게 JPQL을 직접 다루는 것을 권장드립니다.

감사합니다.

김태훈님의 프로필 이미지
김태훈
질문자

1번 작업이 너무 비효율적인게 아닐까 생각이 들어 답답한 마음에 질문을 올렸는데요. 선생님 말씀 들으니까 마음이 편안해졌습니다!!

답변 감사합니다 :)

0

안녕하세요!

 

현재 delete 필드의 처리 때문에 다른 로직들 (조회 로직이라던지) 도 영향을 받는 점이 마음에 들지 않아서 질문 해 주신 듯 합니다! 그래서 한번 delete 필드가 쓰이는 상황 두 가지를 보고, 어떻게 사용해야 할 지 한 번 고민해봤어요.

 

  1. delete 가 false 여야 하는 생성 상황

 

image

상황을 비슷하게 만들기 위해 임의로 엔티티를 하나 선언했습니다.

 

아마도 이 엔티티는 생성 할 때는 isDeleted 라는 필드가 false 여야 하고, 삭제 할 땐 (실제론 수정이 되겠죠) true 로 변경되어야 할 것 같아요!

 

그렇다면 한번 이 엔티티를 save 해보고 해당 isDeleted 필드가 어떻게 DB에 들어가는지 확인 해 봤습니다.

 

image

위와 같이 우리가 명시 선언하지 않은 IS_DELETE 는 FALSE 로 들어갑니다.

 

이 이유는 자바 기본 타입 boolean 의 기본 값이 false 로 설정되어있기 때문입니다. (그러나 생성자 매서드에서 이 boolean 이 false 임을 명시 선언하셔도 괜찮지 않을까 생각이 됩니다.)

 

image

 

이것으로 생성 시 해당 필드의 기본값이 false 가 되도록 만드는 작업을 여타 DB 관련된 프로세스들과(@Where 어노테이션이라던지) 떼어내고, 자바 코드로 관리 가능하게 했습니다.

 

  1. 업데이트 시 (즉 true 가 되어야 할 시)

해당 작업 이후는 JPA 의 변경 감지(더티체킹) 을 사용하시거나, 아니면 JPQL등 편하신 방법을 사용하시어 해당 필드를 TRUE 로 업데이트 하시면 될 것 같습니다!

 

감사합니다.

김태훈님의 프로필 이미지
김태훈
질문자

앗 직접 클래스도 만들어주시고, DB 이미지도 캡쳐해주시고 ... 되게 친절하게 답변해주셔서 감사드립니다 :)

다만 제가 질문을 애매하게 했나봐요, 제가 궁금했던 내용이 동건님에게 직관적으로 와닿지 않은 것 같습니다.

 

우선 @Where 어노테이션의 역할은 모든 조회 요청에 대해 AND 조건을 추가하는 역할을 합니다.

위 예시에서는 Comment 엔티티에 @Where(deleted=false) 라고 적혀있는데요. 이 상태에서 commentRepository.findById(1L) 이라는 요청을 보내면 아래와 같은 쿼리를 만들어줍니다.
"SELECT * FROM Comment c WHERE c.comment_id = 1 AND deleted = false"

(만약 해당 어노테이션이 없었다면 "SELECT * FROM Comment c WHERE c.comment_id = 1" 이런 쿼리가 날아가요)

 

저는 deleted=false 가 모든 조회 쿼리에 달라붙는 게 마음에 들지 않았습니다. 그래서 어떤 방식이 좋을까 고민하면서 질문을 드렸는데, 바로 위에 강사님께서 친절하게 답변을 달아주신 거 같아요. 한 번 읽어보시면 도움이 될 거 같아요.

 

혹시 이해 안가는 부분이 있다면 더 말씀해주세요, 답변해주셔서 감사합니다!