우리팀에서는 논리적 삭제 적용을 위해 soft delete를 사용했다. 위시리스트를 예를들어 보겠다.
@Getter
@Setter
@Entity
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Where(clause = "is_deleted = false")
@Table(name = "wishlist", uniqueConstraints = {@UniqueConstraint(columnNames = {"member_id", "club_id"})})
public class Wishlist extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "wishlist_id")
private Long id;
@Builder.Default
private boolean isDeleted = Boolean.FALSE;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "club_id")
private Club club;
}
Wishlist 엔티티에 isDeleted 필드를 두어 DB에서 실제로 삭제하지 않고 해당 필드를 업데이트 하는 식으로 구현했다.
따라서 wishlist에 추가 후, 삭제 로직을 실행하면 isDeleted 필드가 true로 된다.
@Where(clause = "is_deleted = false")코드에 의해 wishlist 관련 query 문법은 모두 is_deleted 가 false 인 row에 대해서만 수행이 된다.
삭제 후 다시 추가 로직을 실행하면 중복을 검증하는 과정에서 에러가 발생한다.
@Table(name = "wishlist", uniqueConstraints = {@UniqueConstraint(columnNames = {"member_id", "club_id"})})
이 부분은 해당 테이블에 member_id와 club_id의 조합이 중복되는 것을 막는 코드이다.
따라서 1번 사용자가 1번 클럽에 대해 위시리스트를 추가 했다가 삭제 후(soft delete 이므로 DB에는 여전히 남아있음) 다시 추가 하려고 할때 문제가 생긴다!.
@Transactional
public void addWishlist(Long clubId, Long memberId) {
try {
ClubResponse clubResponse = clubService.getClubById(clubId); // 있는 클럽인지 검사
Club club = clubMapper.responseToEntity(clubResponse); // createdAt, updatedAt은 추가해야함.
MemberResponse memberResponse = memberService.getMemberById(memberId); // 있는 멤버인지 검사
Member member = memberMapper.responseToEntity(memberResponse); // createdAt, updatedAt은 추가해야함.
// 삭제된 애들 & 삭제 안된 애들 둘다 조회됨
Optional<Wishlist> existingWishlist = wishlistRepository.findAllByClubIdAndMemberId(clubId, memberId);
if (existingWishlist.isEmpty()) {
WishlistRequest request = WishlistRequest.builder()
.createdAt(LocalDateTime.now())
.club(club)
.member(member)
.build();
Wishlist wishlist = wishlistMapper.requestToEntity(request);
wishlistRepository.save(wishlist);
} else {
Wishlist wishlist = existingWishlist.get();
if (wishlist.isDeleted()) {
wishlistRepository.restoreWishlist(clubId, memberId);
} else {
throw new BusinessExceptionHandler("이미 위시리스트에 존재하는 항목입니다.", ErrorCode.BAD_REQUEST_ERROR);
}
}
} catch (Exception e){
throw new BusinessExceptionHandler("위시리스트에 추가 하는 과정에서 에러 : " + e.getMessage(), ErrorCode.IO_ERROR);
}
}
위와 같이 코드를 수정하려 했다.
근데
wishlistRepository.findAllByClubIdAndMemberId(clubId, memberId);
이 로직에서 자동으로 @Where 이 적용되어 isDeleted가 false 인 row 에 대해서만 검색이 진행 되었다.
해결방법을 찾아본 결과...! nativeQuery를 사용하는 것이었다. JPQL이 아닌 순수 SQL을 사용하면 @Where 가 적용되지 않는다.
@Query(value = "SELECT * FROM wishlist WHERE club_id = :clubId AND member_id = :memberId", nativeQuery = true)
Optional<Wishlist> findAllByClubIdAndMemberId(@Param("clubId") Long clubId, @Param("memberId") Long memberId);
위와 같이 작성하면 isDeleted 에 상관없이 모든 row 에 대해 검색을 진행한다.
찾아보니 실무에서는 soft delete 를 막 엄청 많이 사용하지는 않는 느낌이었다. 이러한 이슈가 있는 것도 한 몫을 하지 않을까 생각했다.
결론적으로는 @Where 를 적용하고 싶지 않은 쿼리에 대해서는 nativeQuery를 사용하자!