[Spring] SoftDelete 적용 후 @Where 절로 인해 삭제된 포스트 목록 조회 불가능한 이슈

오형상·2023년 8월 12일
0

Spring

목록 보기
6/9
post-thumbnail

현재 상황

이전에 작성한 Soft delete 적용한 코드에서는 deleted_at 컬럼이 null이면 삭제되지 않은 것으로 간주하고 조회하도록 @Where 절을 이용하여 처리하고 있었습니다. 그러나 이로 인해 삭제된 포스트 목록을 조회하는 경우 문제가 발생했습니다. 아래와 같이 코드를 작성하였으나 삭제된 포스트 목록을 조회하려고 해도 @where로 인한 조건과 where절이 중첩되어 빈 결과만 반환되었습니다.

Post

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Where(clause = "deleted_at IS NULL")
@SQLDelete(sql = "UPDATE post SET deleted_at = CURRENT_TIMESTAMP WHERE post_id = ?")
public class Post extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "post_id")
    private Integer id;

    private String title;

    private String body;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @OneToMany(mappedBy = "comment", cascade = CascadeType.ALL)
    private List<Comment> comments = new ArrayList<>();

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
    private List<Like> likes = new ArrayList<>();

	//.. 중략
}

PostController

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/posts")
public class PostRestController {

    private final PostService postService;

    //..중략

    @ApiOperation(value = "삭제된 피드 목록", notes = "삭제 된 포스트 목록")
    @GetMapping("/deleted")
    public Response<Page<PostDto>> getDeletedPost(@PageableDefault(size = 20)
                                                  @SortDefault(sort = "deleted_at", direction = Sort.Direction.DESC) Pageable pageable, Authentication authentication) {
        String userName = authentication.getName();
        Page<PostDto> deletedPosts = postService.getDeletedPost(pageable, userName);

        return Response.success(deletedPosts);
    }
 
	//..중략
}

PostService

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class PostService {
    private final UserRepository userRepository;
    private final PostRepository postRepository;
    
    //..중략

    @Transactional(readOnly = true)
    public Page<PostDto> getDeletedPost(Pageable pageable, String userName) {

        User user = userRepository.findByUserName(userName)
                .orElseThrow(() -> new SnsAppException(USERNAME_NOT_FOUND, USERNAME_NOT_FOUND.getMessage()));

        Page<Post> deletedPosts = postRepository.findAllByDeletedAtNotNull(pageable);


        return PostDto.toDtoList(deletedPosts);
    }
    //..중략
}

PostRepository

public interface PostRepository extends JpaRepository<Post, Integer> {

    //..중략

	// deleteAt이 null이 아닌것 조회(삭제 된것 조회)
    Page<Post> findAllByDeletedAtNotNull(Pageable pageable);
}

where절에 deleted_at is null과 deleted_at is not null이 중첩되어 있어 빈 결과를 반환한다.

해결 방법

1. @Query로 nativeQuery 작성

첫 번째 해결 방법은 @Query 어노테이션을 활용하여 네이티브 쿼리를 작성하는 것입니다.
이로인해 @Where 설정을 회피할 수 있습니다.

public interface PostRepository extends JpaRepository<Post, Integer> {

    //..중략

	// 수정 전
    Page<Post> findAllByDeletedAtNotNull(Pageable pageable);

	// 수정 후 ( nativeQuery 적용 )
    @Query(value = "SELECT * FROM post WHERE deleted_at IS NOT NULL", nativeQuery = true)
    Page<Post> getDeletedPosts(Pageable pageable);

}

아래와 같이 @Where 조건이 적용되지 않아 정상적으로 작동한다.

2. @FilterDef 및 @Filter

두 번째 해결 방법은 @Where대신 Hibernate의 @FilterDef와 @Filter를 활용하여 삭제된 포스트를 필터링하는 것입니다.

Post

@FilterDef는 Hibernate에서 사용자 정의 필터를 정의하는 어노테이션입니다. name 속성은 필터의 이름을 지정하고, parameters 속성은 필터에 사용될 파라미터를 정의합니다. 위의 코드에서는 deletedPostFilter라는 이름의 필터를 정의하고, isDeleted라는 이름의 파라미터를 사용하겠다고 선언하였습니다.

@Filter는 @FilterDef에서 정의한 필터를 실제로 엔티티에 적용하는 어노테이션입니다. name 속성은 적용할 필터의 이름을 지정하고, condition 속성은 필터의 조건을 지정합니다. 조건은 SQL 조건식으로, 필터가 적용될 때 해당 조건에 따라 엔티티가 필터링됩니다. 위의 코드에서는 deletedPostFilter라는 이름의 필터를 사용하고, deleted_at 컬럼이 null이 아닌 경우를 필터링 조건으로 정의하였습니다.

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SQLDelete(sql = "UPDATE post SET deleted_at = CURRENT_TIMESTAMP WHERE post_id = ?")
@FilterDef(
        name = "deletedPostFilter",
        parameters = @ParamDef(name = "isDeleted", type = "boolean")
)
@Filter(
        name = "deletedPostFilter",
        condition = "(deleted_at IS NOT NULL) = :isDeleted"
)
public class Post extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "post_id")
    private Integer id;

    private String title;

    private String body;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @OneToMany(mappedBy = "comment", cascade = CascadeType.ALL)
    private List<Comment> comments = new ArrayList<>();

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
    private List<Like> likes = new ArrayList<>();

    // ..중략
}

FilterManger

FilterManager 클래스는 필터를 활성화하고 비활성화하기 위한 메서드를 제공하는 클래스입니다.

@Component
@RequiredArgsConstructor
public class FilterManager {

    private final EntityManager entityManager;

    /**
     * 지정된 필터를 활성화하고 파라미터를 설정합니다.
     *
     * @param filterName  활성화할 필터의 이름
     * @param paramName   설정할 파라미터의 이름
     * @param paramValue  설정할 파라미터의 값
     */
    public void enableFilter(String filterName, String paramName, Object paramValue) {
        Session session = entityManager.unwrap(Session.class);
        Filter filter = session.enableFilter(filterName);
        filter.setParameter(paramName, paramValue);
    }

    /**
     * 지정된 필터를 비활성화합니다.
     *
     * @param filterName  비활성화할 필터의 이름
     */
    public void disableFilter(String filterName) {
        Session session = entityManager.unwrap(Session.class);
        session.disableFilter(filterName);
    }
}

PostService

위에서 생성한 필터 관리 클래스를 서비스 클래스에서 활용하여 필터링을 적용합니다.

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class PostService {

    private final UserRepository userRepository;
    private final PostRepository postRepository;
    private final LikeRepository likeRepository;
    private final CommentRepository commentRepository;
    private final FilterManager filterManager;
    
    //.. 중략

    /**
     * 모든 게시물을 조회합니다. (삭제되지 않은 것만)
     *
     * @param pageable 페이지 정보
     * @return 게시물 DTO 페이지
     */
    @Transactional(readOnly = true)
    public Page<PostDto> getAllPost(Pageable pageable) {
    
		// DELETED_POST_FILTER를 활성화하여 삭제된 게시물만 조회하도록 설정합니다.
        filterManager.enableFilter(FilterConstants.DELETED_POST_FILTER, FilterConstants.DELETED_POST_AT_PARAM, false);
        
        // 페이지 정보를 이용하여 게시물을 조회합니다.
        Page<Post> posts = postRepository.getPosts(pageable);
        
        // DELETED_POST_FILTER를 비활성화하여 원래 상태로 복구합니다.
        filterManager.disableFilter(FilterConstants.DELETED_POST_FILTER);
        
        // DTO로 변환
        return PostDto.toDtoList(posts);
    }

    /**
     * 삭제된 게시물을 조회합니다.
     *
     * @param pageable 페이지 정보
     * @param userName 사용자 이름
     * @return 삭제된 게시물 DTO 페이지
     */
    @Transactional(readOnly = true)
    public Page<PostDto> getAllDeletedPost(Pageable pageable, String userName) {
        // 사용자 정보 가져오기 및 해당 사용자의 삭제된 게시물 조회
        User user = userRepository.findByUserName(userName)
                .orElseThrow(() -> new SnsAppException(USERNAME_NOT_FOUND, USERNAME_NOT_FOUND.getMessage()));

        // DELETED_POST_FILTER를 활성화하여 삭제된 게시물만 조회하도록 설정합니다.
		filterManager.enableFilter(FilterConstants.DELETED_POST_FILTER, 			FilterConstants.DELETED_POST_AT_PARAM, true);

		// 페이지 정보를 이용하여 게시물을 조회합니다.
		Page<Post> posts = postRepository.getPosts(pageable);

		// DELETED_POST_FILTER를 비활성화하여 원래 상태로 복구합니다.
		filterManager.disableFilter(FilterConstants.DELETED_POST_FILTER);
        
        // DTO로 변환
        return PostDto.toDtoList(posts);
    }

    
    //.. 중략
}

아래와 같이 정상적으로 작동한다.

0개의 댓글