운영 환경에서는 데이터가 어떻게 변경되었는지 추적해야 하는 상황이 종종 발생합니다.
예를 들어
JPA 기반의 애플리케이션에서 이러한 요구사항을 만족시키기 위해 Spring Data Envers를 도입할 수 있습니다.
Spring Data Envers 는 Hibernate Envers와 통합된 모듈로, JPA 엔티티의 변경 이력을 자동으로 기록합니다.
수정, 삭제뿐 아니라 생성까지 모든 변경을 추적하며, 리비전 번호를 기준으로 과거 데이터를 조회할 수 있습니다.
implementation 'org.springframework.data:spring-data-envers'
CREATE TABLE revinfo (
rev BIGINT AUTO_INCREMENT PRIMARY KEY,
revtstmp BIGINT
);
CREATE TABLE member_aud (
id VARCHAR(36) NOT NULL,
rev BIGINT NOT NULL,
revtype TINYINT NOT NULL,
password VARCHAR(255),
name VARCHAR(100),
age INT,
phone_number VARCHAR(20),
PRIMARY KEY (id, rev),
FOREIGN KEY (rev) REFERENCES revinfo(rev)
);
@Audited를 필드나 클래스에 붙여 변경 이력을 추적할 수 있습니다.
@Entity
@Audited
public class Member {
@Id
private String id;
private String password;
private String name;
private String phoneNumber;
private int age;
}
@Rollback(value = false)
@Test
public void insertMember() {
Member member = Member.builder()
.id("user1")
.password("pw1")
.name("foo")
.phoneNumber("010-1234-1234")
.age(20)
.role(Role.USER)
.build();
memberRepository.save(member);
}
revinfo 테이블

member_aud 테이블

@Test
@Transactional
@Rollback(false)
public void updateMember() {
Member member = memberRepository.findById("user1").get();
member.setName("bar");
}
revinfo 테이블

member_aud 테이블

삭제 직전 히스토리를 남기려면 application.yml에 설정을 해줘야합니다. 이 설정이 없으면 히스토리의 모든 컬럼에 null값이 들어갑니다.
# application.yml
spring.jpa.properties.org.hibernate.envers.store_data_at_delete: true
@Test
@Transactional
@Rollback(false)
public void deleteMember() {
memberRepository.deleteById("user1");
}
revinfo

member_aud

@Audited를 붙이면 기본적으로 생성, 수정, 삭제 이력이 추적됩니다.
하지만 실무에서는 연관 관계 추적, 감사 정보 커스터마이징, 필드 변경 여부 등 더 세밀한 제어가 필요할 수 있습니다.
Spring Data Envers는 이런 요구를 충족할 수 있는 다양한 옵션을 제공합니다.
아래는 자주 사용하는 확장 기능들 입니다.
@Entity
public class Member {
@Id
private String id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
@Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
private Team team;
// 생략
}
@Getter
@Table(name = "revinfo")
@Entity
@RevisionEntity
public class Revinfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@RevisionNumber
private Long rev;
@RevisionTimestamp
private Long revtstmp;
@Setter
private String updatedBy;
// timestamp -> LocalDateTime
public LocalDateTime getUpdatedAt() {
return Instant.ofEpochMilli(revtstmp)
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
}
}
CREATE TABLE `revinfo` (
`rev` BIGINT AUTO_INCREMENT NOT NULL,
`revtstmp` BIGINT NULL,
`updatedBy` VARCHAR(36) NULL, -- updatedBy 필드 추가
PRIMARY KEY (`rev`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
public class CustomRevisionListener implements RevisionListener {
// 인증 객체의 수정자 정보(username, 권한)를 revinfo에 저장
@Override
public void newRevision(Object o) {
Revinfo revinfo = (Revinfo) o;
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (Objects.nonNull(authentication) && authentication.getPrincipal() instanceof CustomUserDetails) {
CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal();
revinfo.setModifiedBy(customUserDetails.getUsername());
revinfo.setAuthorities(
customUserDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","))
);
}
}
}
@Entity
@RevisionEntity(CustomRevisionListener.class) // 리스너 지정
public class Revision {
//생략
}
@Audited(withModifiedFlag = true)@Entity
public class Member {
@Id
private String id;
@Audited(withModifiedFlag = true)
private String password;
@Audited(withModifiedFlag = true)
private String name;
@Audited(withModifiedFlag = true)
private Integer age
}
CREATE TABLE `member_aud` (
`id` VARCHAR(36) NOT NULL,
`rev` BIGINT NOT NULL,
`revtype` TINYINT NOT NULL,
`password` VARCHAR(255),
`password_mod` TINYINT(1), -- mod 컬럼 추가
`name` VARCHAR(100),
`name_mod` TINYINT(1), -- mod 컬럼 추가
`age` INT,
`age_mod` TINYINT(1), -- mod 컬럼 추가
PRIMARY KEY (`id`, `rev`),
CONSTRAINT `fk_member_aud_revinfo` FOREIGN KEY (`rev`) REFERENCES `revinfo`(`rev`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Spring Data Envers는 엔티티의 히스토리를 조회할 수 있는 다양한 방법을 제공합니다.
복잡한 SQL 없이 히스토리 테이블을 탐색할 수 있도록 AuditReader, RevisionRepository 같은 도구들이 준비되어 있습니다.
조회 방식은 크게 세 가지.
• 직접 쿼리: aud 테이블을 native SQL이나 MyBatis로 직접 조회
• AuditReader: 조건 검색, 페이징, 정렬 등 유연한 조회 가능
• RevisionRepository: Spring Data 방식으로 간단하게 조회
Spring Data Envers는 AuditReader를 통해 엔티티 히스토리를 조건에 맞게 조회할 수 있도록 지원합니다.
정렬, 페이징, 필터링, 수정 필드 여부 등 다양한 조건을 조합해 조회할 수 있습니다.
AuditReader Bean 등록
@Configuration
public class AuditReaderConfig {
@Bean
public AuditReader auditReader(EntityManagerFactory entityManagerFactory) {
return AuditReaderFactory.get(entityManagerFactory.createEntityManager());
}
}
전체 히스토리 조회
@Repository
@RequiredArgsConstructor
public class MemberRevisionRepository {
private final AuditReader auditReader;
public List<Member> findAll() {
return auditReader.createQuery()
.forRevisionsOfEntity(Member.class, true, true) // true, true -> 삭제 엔티티 포함 여부, 중복 제거 여부
.getResultList();
}
}
조건 검색 (id, name 등)
public List<Member> findAllById(String id) {
return auditReader.createQuery()
.forRevisionsOfEntity(Member.class, true, true)
.add(AuditEntity.id().eq(id))
.getResultList();
}
public List<Member> findAllByName(String name) {
return auditReader.createQuery()
.forRevisionsOfEntity(Member.class, true, true)
.add(AuditEntity.property("name").eq(name))
.getResultList();
}
조건 + 페이징 + 정렬 + RevisionType
public List<Member> findAllByCondition(String id) {
return auditReader.createQuery()
.forRevisionsOfEntity(Member.class, true, true)
.add(AuditEntity.id().eq(id))
.add(AuditEntity.revisionType().eq(RevisionType.MOD)) // 수정 이력만
.setFirstResult(0) // 페이징
.setMaxResults(2)
.addOrder(AuditEntity.id().asc()) // 정렬
.getResultList();
}
필드 변경 여부 조건 조회
public List<Member> findByIdHasChanges(String id) {
return auditReader.createQuery()
.forRevisionsOfEntity(Member.class, true, true)
.add(AuditEntity.id().eq(id))
.add(AuditEntity.property("name").hasChanged()) // name이 수정된 히스토리
.add(AuditEntity.property("age").hasNotChanged()) // age는 그대로인 히스토리
.getResultList();
}
메타데이터 함께 조회
public List<Object[]> findByIdWithMetadata(String id) {
return auditReader.createQuery()
.forRevisionsOfEntity(Member.class, false, true) // 메타데이터 포함
.add(AuditEntity.id().eq(id))
.getResultList();
}
반환 타입 Object[]
•[0]: Member
•[1]: Revinfo
•[2]: RevisionType
수정된 필드 정보 포함 조회
public List<RevisionWithMetadata<Member>> findByIdWithMetadata(String id) {
List<Object[]> result = auditReader.createQuery()
.forRevisionsOfEntityWithChanges(Member.class, true)
.add(AuditEntity.id().eq(id))
.getResultList();
return result.stream()
.map(RevisionWithMetadata::<Member>from)
.toList();
}
커스텀 DTO
@Getter
public class RevisionWithMetadata<T> {
private final T entity;
private final Revinfo revinfo;
private final RevisionType revisionType;
private final Set<String> modifiedFields;
@Builder
public RevisionWithMetadata(T entity, Revinfo revinfo, RevisionType revisionType, Set<String> modifiedFields) {
this.entity = entity;
this.revinfo = revinfo;
this.revisionType = revisionType;
this.modifiedFields = modifiedFields;
}
public static <T> RevisionWithMetadata<T> from(Object[] objects) {
return RevisionWithMetadata.<T>builder()
.entity((T) objects[0])
.revinfo((Revinfo) objects[1])
.revisionType((RevisionType) objects[2])
.modifiedFields((Set<String>) objects[3])
.build();
}
}
AuditReader는 조건 기반 조회에 유리하며, 메타데이터와 수정 필드까지 확인할 수 있습니다.
복잡한 조건 조회가 필요한 경우 유용하고, 단순한 히스토리 조회는 RevisionRepository로도 충분합니다.
RevisionRepository는 Spring Data Envers에서 제공하는 인터페이스로,
엔티티 히스토리를 간단하게 조회할 수 있는 메서드들을 기본으로 제공합니다.
복잡한 조건 검색은 어렵지만, 최근 이력 조회나 페이징 기반 히스토리 조회에 적합합니다.
인터페이스
@NoRepositoryBean
public interface RevisionRepository<T, ID, N extends Number & Comparable<N>> extends Repository<T, ID> {
Optional<Revision<N, T>> findLastChangeRevision(ID id); // 최근 리비전 1건
Revisions<N, T> findRevisions(ID id); // 전체 리비전 목록
Page<Revision<N, T>> findRevisions(ID id, Pageable pageable); // 페이징 조회
Optional<Revision<N, T>> findRevision(ID id, N revisionNumber); // 리비전 번호로 조회
}
설정 - Envers 레포지토리 사용 등록
@EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class)
@SpringBootApplication
public class MemberHistoryManagementApplication {
public static void main(String[] args) {
SpringApplication.run(MemberHistoryManagementApplication.class, args);
}
}
JPA 레포지토리에 추가 RevisionRepository<엔티티 타입, 엔티티 Id 타입, Revinfo Id 타입>
public interface MemberRepository extends JpaRepository<Member, String>,
RevisionRepository<Member, String, Long> {
}
사용 예시
// 최근 리비전 1건
memberRepository.findLastChangeRevision("user1");
// 전체 리비전 리스트
memberRepository.findRevisions("user1");
// 페이징 처리
memberRepository.findRevisions("user1", PageRequest.of(0, 10));
// 특정 리비전 번호로 조회
memberRepository.findRevision("user1", 3L);
Spring Data Envers는 설정만 잘 해두면 별다른 비즈니스 로직 수정 없이도 변경 이력을 자동으로 관리해줍니다. 운영 중 발생하는 데이터 변경 추적 요구사항을 안정적으로 해결할 수 있으니, 감사 로깅이나 변경 추적이 필요한 프로젝트에서 적극적으로 고려해볼 만한 기능입니다.
사용자 별 히스토리를 조회할 수 있는 간단한 관리자 기능 예제
https://github.com/hingjae/member-history-management


