[Spring] 하이버네이트 Envers(Hibernate envers) / 스프링 데이터 Envers(spring data envers) - 변경 이력 관리

최가희·2023년 8월 20일
0

SpringBoot

목록 보기
13/13

졸업 프로젝트를 진행하면서 엔티티의 변경 이력을 관리하는 기능을 구현하게 되었다.

처음에는 전에 구현한 것과 동일한 방식으로 변경 이력을 기록하는 history 테이블을 하나 더 만들어 구현하려고 했다. 그러나 새로운 엔티티를 작성하고, 변경 이력 관리 로직을 짜던 중 갑자기 굉장히 번거롭고 귀찮게 느껴졌다. 그때는 하나의 테이블에 대해서만 변경 이력을 관리하면 됐기 때문에 테이블 한 개만 추가하면 됐지만, 이번 프로젝트에서는 무려 8개나..^.^ 만들어야 했다.

하라면 할 수 있겠지만, 나에게 시간은 별로 없고... 무엇보다 효율적으로 작업하고 싶었다. 그때 머릿속에서 스쳐지나간 생각이 있으니, 바로 JPA Auditing이었다. 생성 시간과 수정 시간도 관리해주는데, 분명히 변경 이력을 관리해주는 라이브러리도 있을 거라며 구글링 해봤는데 역시나! 변경 이력을 관리해주는 스프링 데이터 envers 라이브러리가 있었다. 개꿀. 전세계 개발자님들 저는 당신들을 믿고 있었어요.

그렇게 진행 중인 프로젝트에 스프링 데이터 envers를 적용하게 되었다. 프로젝트 규모와 달리 꽤 복잡한 검색 조건을 걸어야 했기에 정확히는 하이버네이트 envers를 적용했고, 아래 내용은 이를 위해 학습한 내용을 정리한 글이다.

하이버네이트 Envers

  • 하이버네이트가 제공하는 핵심 모듈
  • JPA 스펙에 정의된 모든 매핑 감사
  • 엔티티의 변경 이력을 자동으로 관리

하이버네이트 Envers의 변경 이력 관리 방식

  • XXX 테이블 -> XXX_AUD 테이블로 이력 관리
  • 히스토리를 계속 쌓는 방식으로 관리
  • REV) 리비전 식별자(개정 번호)
  • REVTYPE) 0: 등록, 1: 수정, 2: 삭제
  • 테이블 단위가 아닌 트랜잭션 단위로 Revision을 관리한다(REVINFO 테이블).

하이버네이트 Envers 설정

  • 라이브러리 (build.gradle)
implementation 'org.hibernate:hibernate-envers:${version}'

하이버네이트 Envers 사용

  • 히스토리를 관리하려는 클래스 또는 필드 위에 @Audited 어노테이션을 사용한다.
  • 히스토리 관리에서 제외하고 싶은 필드에는 @NotAudited 어노테이션을 사용한다.
@Entity
@Audited
public class Member {

	@Id @GeneratedValue
    private Long id;
    private String name;
    private String tel;
    ...
}

그러면 아래와 같은 MEMBER_AUD테이블이 생성된다.

CREATE TABLE MEMBER_AUD (
	id			BIGINT		NOT NULL,
    rev			INTEGER 	NOT NULL,
    revtype     TINYINT,
    name    	VARCHAR(255),
    tel    		VARCHAR(255),
    PRIMARY KEY(id, rev)
);

하이버네이트 Envers - 조회

과거 특정 시점의 Revision을 조회할 수 있다. 단건 조회, 리스트 조회 모두 가능하며, 검색 조건과 페이징도 지원된다.

  • 단순 리스트 조회
public List<Member> findRevisions(Long id) {
	return auditReader().createQuery()
    		.forRevisionsOfEntity(Member.class, true, true)
            .add(AuditEntity.id().eq(id))
            .getResultList();
}
  • 검색 조건 조회(이름이 무명이면서, 수정 이력만 조회)
public List<Member> findRevisionsWithWhere() {
	return auditReader().createQuery()
    		.forRevisionsOfEntity(Member.class, true, true)
            // 이름이 무명인 데이터 조회
            .add(AuditEntity.property("name").eq("무명"))
            // 수정 이력만 조회
            .add(AuditEntity.revisionType().eq(RevisionType.MOD))
            // 페이징 조건
            .setFirstResult(0)
            // 페이징 조건
            .setMaxResults(2)
            // 정렬 조건
            .addOrder(AuditEntity.property("tel").desc())
            .getResultList();
}
  • 리비전 메타 데이터와 함께 조회
    forRevisionsOfEntity()의 두 번째 파라미터를 false로 바꾸면 리턴 타입이 Object[]로 바뀐다.
public List<Object[]> findRevisions(Long id) {
	return auditReader().createQuery()
    		// 두 번째 파라미터를 false로 바꿈
    		.forRevisionsOfEntity(Member.class, false, true)
            .add(AuditEntity.id().eq(id))
            .getResultList();
}
allRevision[0] = Member{id=1, name='무명', tel='010-1111'}
allRevision[1] = DefaultRevisionEntity(id=1, revisionDate=Aug 20, 2023 10:39:14 PM)
allRevision[2] = ADD
allRevision[0] = Member{id=1, name='무명', tel='010-2222'}
allRevision[1] = DefaultRevisionEntity(id=2, revisionDate=Aug 20, 2023 10:49:14 PM)
allRevision[2] = MOD

하이버네이트 Envers - 필드 변경 여부 관리

어떤 필드를 수정했는지 알고 싶거나 필드의 수정 여부를 검색 조건으로 사용하고 싶으면 @Audited(withModifiedFlag=true)와 같이 withModifiedFlag=true 속성을 지정해준다. 그러면 AUD 테이블에 수정 상태 컬럼이 추가된다.

ex) NAME -> NAME_MOD, TEL -> TEL_MODTRUE면 수정된 상태이고, FALSE면 수정되지 않은 상태이다.

JPA가 히스토리 테이블을 아래와 같이 생성해준다.

CREATE TABLE MEMBER_AUD (
	id			BIGINT		NOT NULL,
    rev			INTEGER 	NOT NULL,
    revtype     TINYINT,
    name    	VARCHAR(255),
    name_mod	BOOLEAN,
    tel    		VARCHAR(255),
    tel_mod 	BOOLEAN,
    CONSTRAINT CONSTRAINT_C PRIMARY KEY(id, rev)
);
  • 필드의 수정 여부를 검색 조건으로 사용
public List<Member> findRevisionsWithChanged(Long id) {
	return auditReader().createQuery()
    		.forRevisionsOfEntity(Member.class, true, true)
            .add(AuditEntity.id().eq(id))
            // 이름이 수정되지 않은 데이터 조회
            .add(AuditEntity.property("name").hasNotChanged())
            // 연락처가 수정된 데이터 조회
            .add(AuditEntity.property("tel").hasChanged())
            .getResultList();
}

하이버네이트 Envers - 기타 어노테이션

// 연관관계가 있는 엔티티를 감사하지 않음.
// 연관관계 추적 테이블 생성은 됨.
@Audited(targetAuditMode=NOT_AUDITED) 

// 상속 관계에 있을 때, 부모에 있는 속성까지 히스토리 관리
// 아래 예시에서는 BaseEntity 클래스까지 히스토리가 관리됨.
@AuditOverride(forClass=BaseEntity.class)



스프링 데이터 Envers

  • 스프링 데이터 JPA의 확장 모듈
  • 하이버네이트 Envers를 편리하게(스프링 데이터스럽게) 조회하도록 도움
  • RevisionRepository 인터페이스 제공
  • 편리한 메타데이터 조회

스프링 데이터 Envers 설정

  • 라이브러리 (build.gradle)
// hibernate-envers 자동 포함됨
implementation 'org.springframework.data:spring-data-envers:${version}'
@EnableJpaRepositories(repositoryFactoryBeansClass
					=EnversRevisionRepositoryFactoryBean.class)

RevisionRepository 인터페이스

@NoRepositoryBean
public interface RevisionRepository extends Repository<T, ID> {
	
    // 최근 리비전 조회 (현 데이터에 근접)
    Revision<N, T> findLastChangeRevision(ID id);
    
    // 모든 히스토리 조회
    Revision<N, T> findRevisions(ID id);
    
    // 히스토리 페이징 + 정렬 조회
    Page<Revision<N, T>> findRevisions(ID id, Pageable pageable);
    
    // 특정 리비전 조회
    Revision<N, T> findRevision(ID id, N revisionNumber);
}

RevisionRepository 인터페이스 적용

RevisionRepository<Entity, PK의 타입, REV의 타입>

public interface MemberRepository extends JpaRepository<Member, Long>,
						RevisionRepository<Member, Long, Integer> {
}

스프링 데이터 Envers - 조회

  • 단순 조회
Revision<Integer, Member> revision 
				= memberRepository.findRevision(member.getId(), 1);

Member entity = revision.getEntity(); 					// 엔티티
Integer revisionNumber = revision.getRevisionNumber(); 	// 리비전
DateTime dateTime = revision.getRevisionDate(); 		// 변경 날짜
  • 페이징, 정렬 조회
Page<Revision<Integer, Member>> result 
		= memberRepository.findRevisions(member.getId(), 
        		new PageRequest(0, 10, RevisionSort.desc()));

result.getTotalElements();	// 전체 수
result.getContent();		// 내용

스프링 데이터 Envers 단점

  • 복잡한 조회가 안 된다. -> 하이버네이트 Envers 직접 사용
  • 버전업이 잘 안 되는 편이다(사실 기능이 별로 없다).
  • 스프링 데이터가 지원하는 Querydsl 관련 기능과 함께 사용하려면 코드를 약간 수정해야 한다.

스프링캠프 2017 [Day2 A5] : 엔티티 히스토리를 편리하게 관리해주는 스프링 데이터 Envers
spring data envers 로 데이터 변경 로깅하기

0개의 댓글