엔티티를 설계하는 과정에서, 공통으로 사용되는 부분들을 BaseEntity로 묶어 관리하기로 했다.
이렇게 하면 중복되는 필드를 한 클래스에서 통합 관리할 수 있어, 추후 공통 필드를 추가,관리,삭제 할 때 효율적일 것이라 판단했다.
먼저 공통 필드로 떠올린 항목은 다음 세 가지였다.
@Getter
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@EqualsAndHashCode(of = "id")
@SoftDelete
public class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime updatedAt;
}
ID값은 Mysql 을 사용하고 있어서 GenerationType.IDENTITY로 설계하였다. Mysql의 Auto_Increment 전략을 사용하고자 하였는데 단순하게 사용할 수 있기에 선택하였다.
createDate,updatedAt을 통해서 엔티티가 생성된 시점과 최종 수정된 시점을 저장하고자 하였다. 엔티티의 생성 시점과 변경시점을 알면 로깅을 할 때에 도움을 많이 줄 수 있다고 판단하였다. 또한 데이터를 주기적으로 삭제하거나 관리할 때 해당 시간을 통해서 관리할 수 있다는 장점도 유의미해보였다.
createDate의 경우에는 updatable = false 을 설정하여 원래의 의도에 맞게 변경되지 않도록 하였다. JPA가 Update sql문을 사용할때 해당 필드는 자동으로 제외되도록 하였다.
@EntityListeners를 붙혀주게 되면서 엔티티 생명주기 이벤트(persist,update,remove,load 등)가 업데이트 될때 마다, AuditingEntityListenr 클래스가 호출되어 @CreatedDate, @LastModified 어노테이션이 붙어있는 필드값을 채워주도록 하였다.
AuditingEntityListener 를 사용하기 위해서 메인 어플리케이션 클래스에 @EnableJpaAuditing 를 붙혀주었다. Spring data Jpa의 기본 설정에는 Auditing 기능이 꺼져 있기에, 시작 지점에서 활성화를 해주었다.
@EqualsAndHashCode(of = "id") 를 사용하여 Id기반으로 equals와 hash값을 정의하였다.
@MappedSuperClass 를 설정해주어 테이블은 생성되지 않고, 자식 엔티티 테이블에 필드가 합쳐진다.
@SoftDelete의 경우에는 프로젝트가 진행되고 후에 추가되었다. 추가하게 된 이유에 관해서 추가로 블로깅을 할 예정이다.
앞서 도입 과정에서 말했듯이 공통 필드에 대한 관리가 수월하였다. 매번 엔티티를 설계할 때 마다 반복되는 코드를 칠 필요가 없었고, 필드의 공통 변경 사항이 생길 때 수정이 용이해보인다.
또한 최근에 경험한 @SoftDelete를 사용하게 되었을 때 BaseEntity에서만 코드를 작성하니 변경 되는 부분이 매우 적게 softDelete를 사용할 수 있었기에 이점을 크게 느꼈던 것 같다.
BaseEntity에 공통 필드를 관리하다 보면 점차 커질 수 있을 것 같다. 불필요한 필드가 추가 될 경우 나중에 추가된 Entity에 필요 없는 필드가 강제로 생기는 경우가 있을 것 같다.
돌이켜보니 몇가지 발전시키거나 더 알아봐야 할 점들이 보였다.
@EqualsAndHashCode 롬복 어노테이션 사용 시 주의점[프록시를 통해서 다르다고 판단되는 경우]
User u1 = em.find(User.class, 1L); // 실제 User 객체(비프록시)일 수 있음
Post p = em.find(Post.class, 10L);
User u2 = p.getAuthor(); // LAZY → User의 프록시가 반환될 수 있음
System.out.println(u1.getClass()); // class com.example.User
System.out.println(u2.getClass()); // class com.example.User$HibernateProxy$abc123
// equals가 getClass()로 비교한다면:
u1.equals(u2); // false (클래스가 다르다고 판단)
위의 내용을 토대로 재설계를 해보자면 적용시킬만한 부분은 equals를 직접 구현하는 것과 추상클래스를 명시하는 것 정도이다.
@Getter
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@SoftDelete
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime updatedAt;
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
BaseEntity other = (BaseEntity) o;
if (id == null || other.id == null) return false;
return id.equals(other.id);
}
@Override
public final int hashCode() {
return Hibernate.getClass(this).hashCode();
}
}