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

김태훈님의 프로필 이미지

작성한 질문수

실전! 스프링 데이터 JPA

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

해결된 질문

24.01.13 16:45 작성

·

447

·

수정됨

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

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

2024. 01. 13. 20:52

안녕하세요. 김태훈님

말씀하신 문제점들 때문에 @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을 직접 다루는 것을 권장드립니다.

감사합니다.

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

2024. 01. 14. 04:54

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

답변 감사합니다 :)

0

Kim DongKyun님의 프로필 이미지

2024. 01. 13. 18:49

안녕하세요!

 

현재 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 로 업데이트 하시면 될 것 같습니다!

 

감사합니다.

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

2024. 01. 14. 04:48

앗 직접 클래스도 만들어주시고, 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 가 모든 조회 쿼리에 달라붙는 게 마음에 들지 않았습니다. 그래서 어떤 방식이 좋을까 고민하면서 질문을 드렸는데, 바로 위에 강사님께서 친절하게 답변을 달아주신 거 같아요. 한 번 읽어보시면 도움이 될 거 같아요.

 

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