spring data envers 로 데이터 변경 로깅하기

이 글은 이력 생성까지 다루고 있으며 이력 조회까지 다루고 있지 않습니다.

envers 란

envers는 hibernate 에서 만든 데이터 변경 이력을 로깅하기 위한 라이브러리입니다.
과거엔 로깅을 위해 customer_hist 와 같은 엔티티를 정의하고 customer 에 insert, update, delete 등의 작업이 발생하면,
history 테이블에도 같은 작업을 해줘야 했으며, 이력을 쌓아야 하는 테이블이 늘어날 수록 이러한 번거로운 반복작업을 계속해야했습니다.
그러나 envers를 사용하면 이러한 번거로운 작업을 대폭 줄일 수 있습니다.
기본적으로 jpa로 구현되어있으며 후에 spring에도 spring data envers 프로젝트로 추가되었습니다.
spring data envers 역시 hibernate에서 관리하기 때문에 큰 차이는 없습니다.

의존성 추가

2020.4월 기준 recent stable version인 hibernate-envers 라이브러리를 build.gradle에 추가해줍니다.

1
compile group: 'org.hibernate', name: 'hibernate-envers', version: '5.4.14.Final'

@Audited로 변경이력 저장할 테이블 생성

변경이력을 추적할 엔티티에 @Audited 어노테이션을 추가해줍니다.
만약 extends 한 엔티티의 이력도 추가되길 원한다면 @AuditOverride(forClass=BaseEntity.class) 도 추가합니다.
그러면 기본적으로 총 두개의 테이블이 생성되게 됩니다.
예를들어 customer 엔티티의 상단에 @Audited를 추가하면 다음과 같은 customer_aud, revinfo 테이블이 생성됩니다.

1
2
3
@Entity
@Audited
public class Customer {... }


이미지출처 : Hibernate Envers - History Data and Versioning

이 revinfo는 central revision table 로, 최초 1회만 생성되며, 이 revinfo 테이블을 삭제하거나 disable 할 순 없습니다.
네이밍 및 필드 변경은 가능합니다.
또한 rev 필드의 네이밍 변경을 각 테이블마다 다르게 줄 수 없습니다.
customer_aud 테이블의 rev_type 은 int타입으로 생성, 수정, 삭제를 구분하는 컬럼입니다.
값은 다음과 같습니다:

1
2
3
0 : insert
1 : update
2 : delete

아쉽게도 타입, 필드명, 값을 변경할 순 없습니다.

테이블 변경이력 로깅에 대한 몇가지 디자인이 있지만, 이렇게 중앙에서 관리하는 테이블이 있는 방식은 createAt, createUser 등을 각 테이블마다 생성하지 않고 중앙 테이블에서 한번만 생성하면 된다는 장점이 있습니다.
이렇게 revision 관리를 하는 테이블이 왜 필요한지에 대한 stackoverflow 글이 몇개 있으니 궁금하신 분은 읽어보시는걸 추천드립니다

property config 설정

1
2
3
4
5
6
7
8
spring:
jpa:
org:
hibernate:
envers:
audit_table_suffix: _history
revision_field_name: rev_id
store_data_at_delete: true

audit_table_suffix, audit_table_suffix 등으로 auditing table의 prefix, suffix를 수정할 수 있습니다.
또한 revision_field_name 로 revision fk 로 쓰는 테이블들의 revid 바꾸고 싶은 경우 설정할 수 있지만 테이블마다 각각 rev_id를 따로 설정하는 것은 불가능합니다. (order_rev_id, product_rev_id 이렇게 개별로 설정 불가능)
delete 시 aud 테이블에서 타겟 테이블의 pk 만 쌓을뿐 다른 필드의 값은 기본적으로 null 입니다.
null이 아니라 delete 직전의 모든 필드의 값을 쌓고 싶다면 store_data_at_delete: true 를 설정합니다.
더 많은 property config는 Envers Configuration Properties에서 확인해주세요.

필수 설정

central revision table인 revinfo의 pk인 rev 컬럼의 타입은 기본적으로 int 로 되어있습니다. 데이터가 20억개 이상 넘어가면 오류가 발생하므로, 권남님 포스팅의 [REV 를 long 으로 변경해야한다]처럼 int 타입을 long 타입으로 변경해줘야 합니다.

그 외

테이블이 서로 연관관계인 경우

추적 테이블을 만들지 않을 경우

연관관계 추적 테이블 생성은 되지만 추적하진 않을거라면 엔티티 내 필드에 다음과 같이 어노테이션을 추가합니다.

1
2
3
4
5
6
7
8
@Entity
@Audited
public class Customer {
...
@OneToOne
@Audited(targetAuditMode = NOT_AUDITED)
private CustomerDetail customerDetail;
}

연관관계 테이블 생성도 하지 않고 추적하지도 않을거라면 엔티티 필드에 다음과 같은 어노테이션을 추가합니다

1
2
3
4
5
6
7
8
@Entity
@Audited
public class Customer {
...
@NotAudited
@OneToOne
private CustomerDetail customerDetail;
}

양방향관계에선 AuditMappedBy로 관계 명시를 해줘야 합니다.

  • @AuditMappedBy(mappedBy = “name”)
  • @OneToMany + @JoinCoulmn 인 경우 @AuditJoinTable
1
2
3
4
5
6
7
8
9
@Entity
@Audited
public class Customer {
...
@OneToMany
@JoinColumn(name="customer_id")
@AuditJoinTable
private Set<CustomerDetail> customerDetail = new LinkedHashSet<>();
}

참고 : onetomany_code_with_code_joincolumn_code

상속관계에 있는 테이블도 audit 하고싶은 경우

클래스 상단에 @AuditOverride(forClass=BaseEntity.class) 추가합니다.
BaseEntity에 굳이 @Audited 추가해주지 않아도 됩니다.

그 외의 문제

queryDSL과 함께 쓰면 조회시 호환이 되지 않는 문제가 발생합니다 [해당 이슈]
다음과 같이 EnversQueryDslRepositoryImpl.java를 수정하여 해결할 수 있습니다.
[EnversQueryDslRepositoryImpl.java gist]

혹 글의 내용이 잘못되었거나, 추가하실 부분 또한 궁금한 부분이 있다면 편하게 코멘트 남겨주세요.
읽어주셔서 감사합니다.

참고