개요
Hibernate Envers 프로젝트는 각각의 대상 엔티티의 이력관리를 간편하게 도와줍니다
-
-
Hibernate/JPA Entity의 변경 사항을 추적할 수 있게 별도 테이블에 변경 사항을 자동 저장해준다.
-
-
-
Hibernate Envers 의 변경 사항을 쉽게 조회할 수 있게 해준다.
-
Spring Data Envers 적용하기
1. dependency 추가
Gradle (build.gradle)
dependencies { // envers implementation "org.springframework.data:spring-data-envers" }
Maven(pom.xml)
<!-- envers --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-envers</artifactId> </dependency>
2. EnableJpaRepositories 추가
@EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class) @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Spring Boot 기준 main 메서드의 클래스에 @EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class) 어노테이션을 추가해줍니다.
3. @Audited
@Table(name = "user_info") @Entity @Audited public class User extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(columnDefinition = "varchar(100) comment '로그인 아이디'",unique = true) private String loginId; @Column(columnDefinition = "varchar(255) comment '이메일 '",unique = true) private String email; // .... }
Entity 위에 @Audited 어노테이션을 추가하면 Entity 의 모든 필드를 추적하게되고,
각 필드에 @Audited 어노테이션을 추가하면 해당 필드만 추적합니다.
추적하고 싶지 않은 필드는?
@NotAudited 를 사용하여 likeGuides 필드는 추적되지 않도록 하면 해결할 수 있습니다.
@NotAudited @OneToMany(mappedBy = "guide", cascade = CascadeType.REMOVE, orphanRemoval = true) private List<LikeGuide> likeGuides = new ArrayList<>();
BaseEntity와 같이 사용하는 경우에는?
// 상속 관계에 있을 때, 부모에 있는 속성까지 히스토리 관리 // 아래 예시에서는 BaseEntity 클래스까지 히스토리가 관리됨. @AuditOverride(forClass=BaseEntity.class)
필드 변경여부 관리
@Audited(withModifiedFlag = true)
withModifiedFlag 를 true 로 설정합니다. (기본값 false)
여기서 중요한점은 Transaction 단위로 revision을 관리한다는 것입니다.
아주 큰 장점으로 생각되는게, 한 Transaction에서 변경된 내용을 한번에 파악이 가능하기 때문입니다.
user_info 테이블

user_info_history 테이블

revision_info 테이블

4. application.yaml
spring: jpa: properties.org.hibernate.envers: audit_table_suffix: _history revision_field_name: revision_id revision_type_field_name: revision_type # 해당 transaction 변경이력 있는 데이터 불러오기 # track_entities_changed_in_revision: true
5. Revision & History Table
- Rev : auto_increment와 같은 이력관리 ID값입니다.
- RevType
- 0: 등록
- 1: 수정
- 2: 삭제
jpa 의 테이블 자동생성 기능 으로 테이블을 로컬에서 생성후, 부가적인 정보를 수정한 후 sql client tool 에서 table 을 생성하면 간편하게 히스토리 테이블을 생성할 수 있습니다.
CREATE TABLE revision_info ( revision_id bigint NOT NULL auto_increment, revtstmp bigint, PRIMARY KEY (revision_id) ) engine=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE user_info_history ( id bigint NOT NULL, revision_id bigint not null, revision_type tinyint, email varchar(100) comment '이메일 ', gender varchar(50) null comment '성별', login_id varchar(100) comment '로그인 아이디', created_at datetime default current_timestamp comment '등록일시', updated_at datetime default current_timestamp comment '수정일시', primary key (id, rev_id) ) engine=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci comment '사용자 History'; ALTER TABLE user_info_history ADD CONSTRAINT user_info_history_revinfo_fkey FOREIGN KEY (rev_id) REFERENCES revinfo (rev);

6. Revision Entity
Rev Long으로 변환하기
@Getter @ToString @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @EqualsAndHashCode(onlyExplicitlyIncluded = true) @Table(name = "revision_info") @RevisionEntity @Entity public class CustomRevisionEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @RevisionNumber @EqualsAndHashCode.Include @Column(name = "revision_id") private long id; @EqualsAndHashCode.Include @RevisionTimestamp @Column(name = "revtstmp") private long timestamp; @Transient public Date getRevisionDate() { return new Date( timestamp ); } @Override public String toString() { return String.format("CustomRevisionEntity(id = %d, revisionDate = %s)", id, DateFormat.getDateTimeInstance().format(getRevisionDate())); } }
rev는 기본적으로 Integer를 사용합니다.
하지만 rev는 DB 트랜잭션 단위로 증가하기 때문에 여러 테이블이 함께 사용하면 금방 소모되어 Integer.MAX에 다다를 수 있습니다.
따라서 REVINFO 테이블의 REV 컬럼은 int 에서 long 으로 변환해 줘야 한다.
추가/수정/삭제를 할 때 마다 REV 가 하나씩 증가하는데, 20억이 넘어가면 int 사이즈는 오버 플로우 되어버릴 것이다.
RevisionEntity를 Custom 하게 만들어서 사용할 수 있습니다. (INT -> LONG)
@RevisionEntity와 @RevisionNumber, @RevisionTimestamp만 잘 설정해 주면 됩니다.
@RevisionEntity를 만들면 envers가 사용하는 모든 revision 기록은 이 entity를 사용하게 됩니다.
7. 그 외 부가적인 설명
ToOne 관계의 join column audit
If you want to audit a relation, where the target entity is not audited (that is the case for example with dictionary-like entities, which don't change and don't have to be audited), just annotate it with @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED). Then, when reading historic versions of your entity, the relation will always point to the “current” related entity.
- 관계맺은 엔티티 자체는 audit 하지 않고, join column 값의 변화면 검사하고자 한다면 관계 매핑에 @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) 어노테이션 사용
@JoinColumn을 사용한 @OneToMany 인 경우
@JoinColumn을 사용한 @OneToMany로 양방향 연관관계인 경우, one 쪽에 @JoinTable이나 @AuditMappedBy 설정을 해줘야 한다.
@Entity @Audited @AuditTable("order_audit") class Order( @OneToMany @AuditMappedBy(mappedBy = "order") private orderItems: List<OrderItems>, )
@Version을 사용하는 경우
@Version 을 사용해서 optimistic locking 을 사용하는 경우, audit 테이블에 함께 저장하기 위해서는 do_not_audit_optimistic_locking_field 설정을 해줘야 한다.
org.hibernate.envers: do_not_audit_optimistic_locking_field: false # false 로 설정 해야 @Version 컬럼도 audit 테이블에 저장된다. default : true
삭제할 때도 스냅샷을 남기고 싶은 경우
현재 진행중인 프로젝트에서는 soft delete를 사용하고 있는데, Hibernate Envers는 hard delete 기준으로 만들어진 것 같다.
Envers는 데이터를 삭제하면 실제로 데이터를 지우는 형태로 REVTYPE=2(del) 와 모든 필드는 null 로 audit 테이블에 저장한다.
하지만, soft delete를 사용하면 일부 필드만 변경(deleted=true)을 하는 경우가 많고, 저장할 때의 값이 필요해질 때가 있다. 이런 경우, store_data_at_delete 설정을 해줘야 한다.
org: hibernate: envers: store_data_at_delete: true # Delete 될 때, 현재 상태를 함께 저장한다. false 인 경우 null 로 저장됨. default: false
History 테이블을 조회할때 Repository
@Repository public interface UserRepository extends JpaRepository<User,Long>, RevisionRepository<User, Long, Integer> { }
참고
'Spring , JPA' 카테고리의 다른 글
Spring Cloud OpenFeign 사용법 (0) | 2023.11.12 |
---|---|
Spring Boot Logging (log4j, logback, log4j2) (0) | 2023.07.09 |
Spring Boot , Java Application Graceful Shutdown (0) | 2023.03.13 |
[spring] Spring Webflux 란? (0) | 2023.01.17 |
[spring] Spring Webflux CRUD (0) | 2023.01.16 |