엔티티의 이력 유형 데이터 모델링
들어가기전
데이터베이스에서 이력 데이터 모델링은 실무에서 중요한 부분이라 할 수 있습니다.
- 생성자
- 생성 일자
- 수정자
- 수정 일자
위의 테이블 모델링을 살펴보시면 낯설지 않게 느끼실 수 있습니다. 이러한 테이블 모델링은 기본적이면서도 중요한 부분이라 할 수 있습니다. 개발자는 생성자, 생성 일자, 수정자, 수정 일자 컬럼의 데이터를 통해, 누가 언제 테이블의 데이터를 저장하고 수정 또는 삭제 했는지에 대해 알 수 있기 때문입니다. 이러한 칼럼의 유형을 이력유형이라 하는데, 실무에서 이력 유형 컬럼들을 고려하지 않은 테이블 모델링을 마주하게 된다면 데이터를 누가 삽입/수정/삭제했는지에 대해 추적하는데 어려움이 따릅니다.
만약 Spring과 JPA를 사용하는 프로젝트라면 이력 유형 데이터들을 쉽게 관리할 수 있습니다. 본 포스팅에선 엔티티의 이력유형 데이터 모델링을 어떻게 구성하는 방법을 소개해드리겠습니다. 전체 코드는 GitHub에서 확인해볼 수 있습니다.
- Spring Boot 2.2.2.RELEASE
- Hibernate 5.4.9 Final
학습 목표
- Spring Data Audit 기능
- 엔티티의 이력 유형 데이터 모델링
1. Spring Data JPA의 Audit 기능
Spring Data JPA에선 이력 유형 데이터를 쉽게 관리할 수 있도록 @CreatedBy, @CreatedDate, @LastModifiedBy, @LastModifiedDate 이력 유형 애노테이션을 제공하고 있습니다. 애노테이션의 네이밍 그대로 생성자, 생성 일자, 마지막 수정자, 마지막 수정 일자를 의미합니다.
- org.springframework.data.annotation.CreatedBy
- org.springframework.data.annotation.CreatedDate
- org.springframework.data.annotation.LastModifiedBy
- org.springframework.data.annotation.LastModifiedDate
이력 유형 애노테이션의 사용법은 간단합니다.
- 이력 유형 필드에 애노테이션 명시
- 이력 유형 리스너 활성화
1.1. 이력 유형 필드에 애노테이션 명시
우선 엔티티를 구성할 때, 이력 유형 필드에 애노테이션을 명시해줍니다.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String userId;
private Stirng password;
@CreatedBy
private Long createdBy;
@CreatedDate
private LocalDateTime createdDt;
@LastModifiedBy
private Long modifiedBy;
@LastModifiedDate
private LocalDateTime modifiedDt;
}
그다음으로 이력 유형 애노테이션을 활성화하기 위해 부가적인 작업이 필요합니다. 아래 테스트 코드 결과를 보면 이력 유형 애노테이션이 활성화되지 않아 데이터들이 null로 저장되는 걸 확인할 수 있습니다.
@Slf4j
@DataJpaTest
@TestConstructor(autowireMode = ALL)
@RequiredArgsConstructor
class MemberRepositoryTest {
private final memberRepository;
@Test
void saveAutoAuditedFields() {
Member member = memberRepository.save(Member.builder()
.userId("gmoon")
.password("111111")
.build());
assertAll("auto init audited fields",
() -> assertNotNull(member.getCreatedBy()),
() -> assertNotNull(member.getCreatedDt()),
() -> assertNotNull(member.getModifiedBy()),
() -> assertNotNull(member.getModifiedDt())
);
}
}
1.2. Audit Listener 등록 및 활성화
@EntityListeners, @EnableJpaAuditing
Spring Data JPA 애노테이션을 활성화하기 위해선, JPA 표준 규격 패키지에 포함된 javax.persistence.EntityListeners 애노테이션이 필요합니다.
package javax.persistence;
@Target({TYPE})
@Retention(RUNTIME)
public @interface EntityListeners {
Class[] value();
}
@EntityListeners 애노테이션에 리스너 클래스를 지정하면 엔티티 상태가 변화할 때 지정된 리스너로 콜백받는 용도로 사용되며, @Entity 또는 @MappedSuperclass 애노테이션이 부착된 클래스에서 사용할 수 있습니다. 별도의 리스너 클래스를 정의하여 사용해도 되지만, 기본적으로 Spring Data에서 제공하는 AuditingEntityListener 클래스를 지정해주면 됩니다.
AuditingEntityListener 클래스는 내부적으로 엔티티의 콜백 이벤트 애노테이션인 @PrePersist, @PreUpdate를 사용하여, 영속 상태 이전에 값을 삽입하거나 데이터베이스의 업데이트 질의문을 수행하기 이전에 이력 유형 데이터를 관리해줍니다. 간단히 이벤트 애노테이션들을 소개해드리자면 다음과 같습니다.
애노테이션 | 설명 |
---|---|
@PostLoad | 엔티티가 영속성 컨텍스트에서 조회된 직후 또는 refresh된 경우에 호출된다. |
@PrePersist | EntityManager의 persist 또는 cascaded 전에 호출된다. 참고로 식별자 생성 전략을 사용한 경우 엔티티에 식별자는 존재하지 않는다. |
@PostPersist | EntityManager의 persist 또는 cascaded 후에 호출되며 식별자가 항상 존재한다. 참고로 식별자 생성 전략이 IDENTITY면 식별자를 생성하기 위해 persist를 호출한 직후에 이벤트가 호출된다. |
@PreRemove | EntityManager의 remove 또는 cascaded 전에 호출되며 orphanRemoval에 대해서는 flush나 commit할 시점에도 호출된다. |
@PostRemove | EntityManager의 remove 또는 cascaded 후에 호출된다. |
@PreUpdate | 데이터베이스 UPDATE 쿼리가 실행되기 전에 호출된다. |
@PostUpdate | 데이터베이스 UPDATE 쿼리가 실행되기 후에 호출된다. |
본론으로 돌아와서 회원 클래스에 AuditingEntityListener 클래스를 지정해줍시다.
@Entity
@EntityListeners(value = { AuditingEntityListener.class })
public class Member { ... }
아래 Bean 구성 클래스에 다음과 같이 @EnableJpaAuditing 애노테이션을 명시하여 리스너를 활성화해줍니다. 이처럼 리스너를 활성화하게 되면, @CreatedDate, @LastModifiedDate 애노테이션이 지정된 날짜 이력 유형 필드들은 Spring Data에 의해 자동으로 관리됩니다.
@Configuration
@EnableJpaAuditing // Audit 리스너 활성화
public class AuditedConfig { }
마지막으로 @CreatedBy, @LastModifiedBy 애노테이션을 활성화 해주기 위해선, AuditorAware 인터페이스를 구현한 클래스를 Bean으로 등록해주면 됩니다.
@Configuration
@EnableJpaAuditing
public class AuditedConfig {
@Bean
public AuditorAware<Long> auditorAware() {
return () -> Optional.ofNullable(0L);
}
}
물론 Spring Security를 사용한다면, 시큐리티 컨텍스트에서 로그인한 사용자 정보를 반환해주는 코드를 작성해주면 되지만, 위 코드는 예제 코드임으로 엔티티 ID를 0L로 지정해주었습니다. 이제 마지막으로 테스트 코드를 실행하여 정상적으로 이력 유형 필드의 데이터들이 관리되는지 확인해보겠습니다.
@DataJpaTest
@TestConstructor(autowireMode = ALL)
@RequiredArgsConstructor
class MemberRepositoryTest {
private final MemberRepository memberRepository;
@Test
void saveAutoAuditedFields() {
Member member = memberRepository.save(Member.builder()
.userId("gmoon")
.password("111111")
.build());
assertAll("auto init audited fields",
() -> assertNotNull(member.getCreatedBy()),
() -> assertNotNull(member.getCreatedDt()),
() -> assertNotNull(member.getModifiedBy()),
() -> assertNotNull(member.getModifiedDt())
);
}
@TestConfiguration
@EnableJpaAuditing
static class TestAuditedConfig {
@Bean
public AuditorAware<Long> auditorAware() {
return () -> Optional.ofNullable(0L);
}
}
}
1.3. 이력 유형 데이터 추상화
@MappedSuperclass 애노테이션 활용
앞서 본 예제 코드처럼 엔티티 클래스에 이력 유형 애노테이션들을 명시하여 관리할 수 있습니다.
하지만 부가적으로 여러 도메인에 공통된 이력 유형 필드에 대해 감사(audit)가 필요하다면 직접적인 클래스에 필드로 명시하는 것보다는 공통된 부분인 이력 유형 필드를 추상화하여 관리해야 합니다. 이럴 때 JPA 표준 패키지에 포함된 @MappedSuperclass 애노테이션을 활용한다면, 코드 관리 관점에서도 이력 데이터를 더욱 쉽게 관리할 수 있습니다.
우선 기존 회원 클래스에 존재했던 이력 유형 필드들을 BaseTraceEntity 추상화 클래스로 모듈화해줍니다.
@Getter
@MappedSuperclass
@EntityListeners(value = { AuditingEntityListener.class })
public abstract class BaseTraceEntity {
@CreatedBy
private Long createdBy;
@CreatedDate
private LocalDateTime createdDt;
@LastModifiedBy
private Long modifiedBy;
@LastModifiedDate
private LocalDateTime modifiedDt;
}
앞서 설명했던 것처럼 @EntityListeners 애노테이션은 @Entity 클래스뿐만 아니라 @MappedSuperclass 클래스에도 지정할 수 있으므로, BaseTraceEntity 클래스에 리스너를 명시해줍니다.
마지막으로 회원 클래스에선 생성자, 생성 일자, 수정자, 수정 일자 필드를 제거하고, 구성한 BaseTraceEntity 클래스를 상속만 해주면 됩니다.
@Entity
public class Member extends BaseTraceEntity{
@Id
@GeneratedValue
private Long id;
private String userId;
private String password;
}
마무리
다음과 같이 Spring Data와 JPA를 활용하여 이력 유형 데이터를 모델링하는 방법에 대해 소개해드렸습니다.
- 이력 유형 애노테이션 활용 : @MappedSuperclass를 활용하여 추상화
- 리스너 활성화 : @EntityListeners, @EnableJpaAuditing
- AuditorAware 인터페이스 구현 및 Bean 등록
이처럼 간단한 방법을 통해 엔티티를 누가 저장했는지, 누가 수정했는지에 대해 추적할 수 있습니다. 단 엔티티의 삭제 이력 데이터에 대해선 다음과 같은 방법으론 추적할 수 없습니다. 다음 포스팅에선 삭제된 이력 데이터에 대해 어떻게 추적할 수 있는지에 대해 소개해드리겠습니다.