
엔티티를 만들다 보면, 공통적으로 사용되는 컬럼이 생기곤 한다.
예를 들어, User, Post, Comment 등 거의 모든 엔티티에 생성일과 수정일이 필요한 경우가 있다.
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
각 엔티티마다 직접 작성해도 큰 문제는 없지만, 한 번에 묶어서 관리하면 중복도 줄이고 변경도 쉬워지지 않을까?
그 방법으로 공통 필드를 BaseEntity로 분리해 상속받는 방식이 있다.
@Getter
@SuperBuilder // 필드에 빌더 적용 필요 시에만
@MappedSuperclass
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "modified_at", nullable = false)
private LocalDateTime updatedAt;
}
사실 class로 만드나, abstract class로 만드나 JPA의 동작에는 차이가 없다.
BaseEntity는 도메인 모델이 아니라 공통 기능을 제공하는 추상 개념이므로 인스턴스화될 이유가 없다.
BaseEntity 그 자체로는 의미 있는 엔티티는 아니므로, abstract로 만들었다.
부모 클래스의 필드를 자식 엔티티에 매핑해주는 JPA 애너테이션이다.
JPA는 기본적으로 다음 대상만 매핑한다.
@Entity, @MappedSuperclass, @Embeddable
@MappedSuperclass를 사용하지 않으면, JPA 입장에서는 그냥 부모 Java 클래스일 뿐, 컬럼 생성 등 매핑이 동작하진 않는다.
즉 “이 클래스는 엔티티는 아니지만, 필드는 자식 엔티티에 매핑해라” 라고 알려주는 애너테이션이다.
Builder 패턴을 상속 필드에도 적용할 수 있도록 하는 Lombok 애너테이션이다.
@Getter
@SuperBuilder
public class BaseEntity {
}
@Getter
@SuperBuilder
public class User extends BaseEntity {
}
User user = User.builder()
.nickname("talkit")
.build();
직접 구현도 가능하지만 일반적으로 Lombok 을 사용하는 이유와 동일하게, 보일러플레이트 코드를 줄이는 용도로 사용했다.
부모 클래스 뿐만 아니라 상속 받는 클래스(상속 체인에 참여하는 모든 클래스) 또한 @Builder가 아니라 @SuperBuilder를 사용해야 동작한다.
사실 현재 예시에서는 JPA Auditing으로 값이 직접 주입되는 필드 createdAt, updatedAt 만 존재한다.
생성 시점에 외부 입력이 아닌 시스템이 관리하는 값이므로 Builder 대상에서 제외하는 것이 설계적으로 더 자연스럽다.
Builder가 없는 편이 좋은 구조지만 테스트 편의성을 고려해서 선택하는 것이 좋겠다.
엔티티의 생성 및 수정 이벤트를 감지해 Auditing 기능을 동작시키는 애너테이션이다.
이 애너테이션 설정이 있어야 @CreatedDate, @LastModifiedDate가 동작한다.
@EntityListeners(AuditingEntityListener.class), @CreatedDate, @LastModifiedDate 가 동작하려면 Spring Data JPA Auditing을 활성화해야 한다.
@EnableJpaAuditing 은 엔티티에 붙이는 애너테이션이 아니라, Spring Data JPA의 Auditing 기능을 활성화하라는 애플리케이션 전역 설정이다.
@SpringBootApplication
@EnableJpaAuditing
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
Application 클래스에 붙여도 정상적으로 동작한다.
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
Auditing 관련 설정이 늘어날 것 같다면, 별도 설정 클래스로 분리해 등록할 수도 있다.
BaseEntity.java
@Getter
@SuperBuilder
@MappedSuperclass
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
User.java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SuperBuilder
public class User extends BaseEntity {
@Id
@GeneratedValue
private Long id;
private String nickname;
}
필수는 아니고 기준에 따라 선택하면 되는 영역같다. 일반적인 프로젝트의 엔티티에서 생성일, 수정일 등 동일한 필드를 갖는 경우가 많으므로 중복을 공통화하기 좋은 선택지라고 생각한다.
사용하는 것이 좋은 경우
사용하지 않아도 되는 경우