이전 Soft Delete가 적용된 엔티티 구조이다.
기존 Soft Delete를 적용하여 실제 엔티티를 삭제하는 것이 아니라, deleted 컬럼을 변경해서 논리적으로 삭제되도록 구현하였다.
@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;
}
Soft Delete 방식을 적용하였던 이유는
실제로 Member 엔티티를 삭제하게 되면 TeamMemberRelation도 함께 삭제되어 아래 요구사항을 만족하지 못하였다.
따라서 이를 해결하기 위해 @NotFound 애노테이션을 적용한다.
JPA에서 @JoinColumn을 이용하여 설정된 연관관계의 엔티티가 존재하지 않을 경우
EntityNotFoundException이 발생하게 된다.
이는 @NotFound 애노테이션에 정의된 NotFoundAction이 NotFoundAction.EXCEPTION 로 정의되어 있기 때문이다.
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotFound {
NotFoundAction action() default NotFoundAction.EXCEPTION;
}
해당 애노테이션의 action을 재정의한다.
@Entity
public class TeamMemberRelation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotFound(action = NotFoundAction.IGNORE)
@ManyToOne(fetch = FetchType.LAZY) // 무시되는 속성
@JoinColumn(name = "ldap_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_code")
private Team team;
}
Member 연관관계의 NotFoundAction을 재정의하였고, 연관된 엔티티가 존재하지 않더라도 예외가 발생하지 않는다.
하지만 다음과 같은 주의사항이 존재한다.
EAGER로 로딩된다.1번 주의사항에 명시되었듯, 예외를 방지할 뿐 NPE가 항상 발생할 수 있다.
해당 연관관계를 이용하는 모든 로직에 NPE 체크 로직을 넣게 된다면 NotFoundAction을 재정의한 이유가 없게 된다.
아래 요구사항을 만족하기 위해 Member 엔티티 전부가 필요하진 않다.
FK로 이용된 ldapId 필드는 team_member_relation 테이블에도 존재하고 해당 값을 채워
임시 Member 엔티티를 사용할 수 있도록 하자.
@Column(name = "ldap_id", insertable = false, updatable = false)
private String ldapId;
@Transient
private Member temeMember;
public Member getMember() {
if (this.member != null) {
return this.member;
}
if (this.temeMember == null) {
this.temeMember = Member.builder()
.ldapId(this.ldapId)
.build();
}
return this.temeMember;
}
@Transient 필드로 관리되며 테이블엔 영향주지 않고 객체 내에서만 사용된다.