@NotFound

Soonwoo Kwon·2024년 5월 8일

이전 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도 함께 삭제되어 아래 요구사항을 만족하지 못하였다.

요구사항

  • member가 삭제되더라도 team을 통해 team에 포함된 member 정보를 조회할 수 있어야 한다.
  • member의 모든 정보는 필요하지 않고, FK로 사용된 ldapId 필드만 조회되면 된다.

따라서 이를 해결하기 위해 @NotFound 애노테이션을 적용한다.

@NotFound(action = NotFoundAction.IGNORE)

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을 재정의하였고, 연관된 엔티티가 존재하지 않더라도 예외가 발생하지 않는다.

하지만 다음과 같은 주의사항이 존재한다.

  1. 연관된 엔티티가 존재하지 않을 때에 예외가 발생하는 것을 막을 뿐,
    해당 엔티티는 Null 이므로 해당 엔티티의 속성을 활용하는 로직에서 NPE가 발생할 수 있다.
  2. @NotFound 애노테이션을 IGNORE로 재정의한 연관관계는 항상 EAGER로 로딩된다.

1번 주의사항에 명시되었듯, 예외를 방지할 뿐 NPE가 항상 발생할 수 있다.
해당 연관관계를 이용하는 모든 로직에 NPE 체크 로직을 넣게 된다면 NotFoundAction을 재정의한 이유가 없게 된다.

아래 요구사항을 만족하기 위해 Member 엔티티 전부가 필요하진 않다.

  • member의 모든 정보는 필요하지 않고, FK로 사용된 ldapId 필드만 조회되면 된다.

FK로 이용된 ldapId 필드는 team_member_relation 테이블에도 존재하고 해당 값을 채워
임시 Member 엔티티를 사용할 수 있도록 하자.

@Transient

    @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;
    }
  • getMember 메서드를 재정의하여,
    member 객체가 null 이라면 ldapId 값만을 채운 임시 Member 객체를 반환하도록 한다.
  • 모든 getMember 호출마다 임시 Member 객체를 생성하는 것은 낭비이니 tempMember 필드를 관리하여 해당 값을 반환하도록 한다.
  • tempMember 는 @Transient 필드로 관리되며 테이블엔 영향주지 않고 객체 내에서만 사용된다.

0개의 댓글