Soft Delete 란 실제 데이터를 데이터베이스에서 삭제하지 않고, 논리적으로 삭제하는 것을 말한다.
삭제될 테이블의 특정 컬럼을 update 하여 해당 컬럼값에 따라 논리적으로 삭제된 데이터로 판별하는 방식이다.
Soft Delete가 적용될 만할 상황을 가정해보자.
member, team, team_member_relation 테이블이 존재한다.member, team은 다대다 매핑이며, team_member_relation을 통해 매핑된다.team_member_relation 테이블을 각 member, team의 PK를 FK로 사용한다.member 엔티티엔 cascade = CascadeType.REMOVE 조건이 걸려있어, member가 삭제되면 team_member_relation도 삭제된다.@Entity
public class Member implements Persistable<String> {
@Id
private String ldapId;
private String username;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<TeamMemberRelation> teamMembers = new ArrayList<>();
}
@Entity
public class Team implements Persistable<String> {
@Id
private String teamCode;
private String teamName;
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
private List<TeamMemberRelation> teamMembers;
}
@Entity
public class TeamMemberRelation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ldap_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_code")
private Team team;
}
이 때 추가 요구사항이 발생하였다.
member 데이터를 실제로 삭제하지 않고, deleted 라는 컬럼을 추가하여
해당 컬럼을 true로 변경하여 soft delete를 수행한다.
deleted 필드를 추가
@Entity
public class Member implements Persistable<String> {
@Id
private String ldapId;
private String username;
// 필드 추가
private boolean deleted;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<TeamMemberRelation> teamMembers = new ArrayList<>();
}
soft delete가 적용되면 조회, 삭제 쿼리에 다음과 같은 변경이 발생한다.
SELECT *
FROM member
WHERE deleted = FALSE # 삭제되지 않은 멤버 조회의 경우
UPDATE member
SET deleted = TRUE # TRUE 로 변경하여 삭제를 구현
WHERE id = member_id
이를 통해 Soft Delete를 적용할 수 있지만, 기존 member와 관련된 로직에 deleted 컬럼과 관련된 로직을 추가하는 것은 변경점이 많다.
또한 Soft Delete 적용 여부를 모르는 다른 사람이 개발할 경우, deleted 컬럼 관련 로직을 누락할 수 있다.
다행히 JPA 에선 애노테이션 기반으로 Soft Delete를 쉽게 사용할 수 있다.
@SQLDelete 애노테이션을 통해 지정된 엔티티의 delete() 메서드를 오버라이딩 할 수 있다.
deleted 필드를 추가
@SQLDelete(sql = "UPDATE member SET deleted = true WHERE id=?")
@Entity
public class Member implements Persistable<String> {
...
}
이렇게 애노테이션 sql 파라미터에 JPQL을 지정할 수 있으며, 해당 엔티티의 delete() 메서드가
지정된 update 쿼리로 오버라이딩 된다.
@SQLDelete 적용을 통해 기존 삭제 기능에 soft delete 로 인한 변경 발생을 막을 수 있다.
@Where 애노테이션을 통해 지정된 엔티티의 조회시 항상 특정 조건을 적용할 수 있다.
@Where(clause = "deleted=false")
@Entity
public class Member implements Persistable<String> {
...
}
clause 파라미터에 JPQL을 지정할 수 있으며, 해당 엔티티 조회시 항상 해당 조건절이 포함된다.
@Where 적용을 통해 기존 조회 기능에 soft delete 로 인한 변경 발생을 막을 수 있다.
Soft Delete 적용으로 문제를 해결한 듯 하였으나, 요구사항을 만족하기엔 문제가 있었다.
Team 엔티티를 통해 연관 Member를 조회하기 위해선 TeamMemberRelation 엔티티를 이용해야 한다.
Team -> List<TeamMemberlation> -> List<Member> 순으로 연관 멤버를 찾으려 하였다.
이 때 TeamMemberRelation엔 soft delete된 member 연관관계가 포함되어 있어야 하는데,
@Where 절을 적용으로 인해 member 관련 조회에는 모두 deleted=false 조건이 포함되어
삭제된 member 연관관계을 조회할 수 없게 되었다.
해당 문제 해결은 다음 글에 기재한다.