글을 작성하게 된 계기
JPA Envers의 간단한 사용법과 주의할 점을 정리하기 위해 글을 작성하게 되었습니다.
1. Envers
Envers는 Hibernate의 모듈로 동일한 트랜잭션 내에서 발생하는 엔티티의 모든 변경 사항을 자동으로 감사하고 추적 합니다. 이는 엔티티의 버전 관리를 용이하게 하며, 데이터 변경에 대한 이력을 효율적으로 관리하고 추적할 수 있습니다.
이를 사용하면 테이블에 rev_type이라는 칼럼이 생기며, 이는 테이블의 변경 이력 을 나타냅니다. 각 값은 추가(0), 수정(1), 삭제(2)를 나타내며, 실시간으로 많은 데이터가 쌓이지 않는 경우 유용하게 사용할 수 있습니다.
2. 사용법
Envers는 사용법이 굉장히 간단한데, 먼저 Envers의 의존성을 추가합니다.
1
implementation("org.springframework.data:spring-data-envers: ${VERSION}")
이후 애플리케이션 시작점에 아래와 같은 설정을 추가해 줍니다. 멀티 데이터 소스를 사용하는 경우 추가 설정이 필요한데, 이에 대한 설명은 공식 문서를 찾아보면 어렵지 않게 알 수 있기에 이번 포스팅에서는 생략하겠습니다.
1
2
3
4
5
6
7
8
9
10
11
@EnableJpaAuditing
@EnableFeignClients
@SpringBootApplication
@EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class)
public class SwithMeOrderApplication {
public static void main(String[] args) {
SpringApplication.run(SwithMeOrderApplication.class, args);
}
}
Envers는 JpaRepository의 추가 기능을 활용하기 위해 RevisionRepository 인터페이스가 필요 합니다. 이는 Envers의 리비전 정보를 처리하는데, 이를 사용하려면 FactoryBean 설정을 통해 RevisionRepository의 구현체를 생성해야 합니다.
1
2
3
public interface OrderPaymentHistoryJpaRepository extends
JpaRepository<OrderPaymentHistory, Long>, RevisionRepository<OrderPaymentHistory, Long, Long> {
}
이후 다른 엔티티들과 마찬가지로 엔티티에 대응하며, 추적하고자 하는 엔티티에는 @Audited를 붙여줍니다. 이후 동일 트랜잭션 내에서 발생하는 정보 에 대해서는 자동으로 JPA가 이를 감지해 테이블에 반영해 줍니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
@RevisionEntity
@Entity(name = "order_payment_history")
public class OrderPaymentHistory implements Serializable {
@Id
@RevisionNumber
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@RevisionTimestamp
@Column(name = "created_at", nullable = false)
private Long createdAt;
......
}
1
2
3
4
5
6
7
8
9
10
11
12
@Getter
@Audited
@Entity(name = "orders")
public class Order extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
......
}
아래와 같이 커스텀 엔티티를 생성해주는 이유는 기본적인 Envers의 기능을 사용하면 PK 값이 Integer 이기 때문입니다. 즉, 2^31 -1개의 데이터만 저장할 수 있는데, 이러면 금방 최대 치를 초과합니다. 따라서 PK로 Long을 사용하기 위해 아래와 같이 커스텀 엔티티를 생성합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
@RevisionEntity
@Entity(name = "order_payment_history")
public class OrderPaymentHistory implements Serializable {
@Id
@RevisionNumber
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@RevisionTimestamp
@Column(name = "created_at", nullable = false)
private Long createdAt;
......
}
만약 어떤 칼럼이 변경됐는지에 대해 하나하나 알고 싶다면 아래와 같은 추가 설정을 할 수 있습니다. 연관관계가 있는 엔티티의 변경 이력을 조회하고 싶지 않다면 Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED 설정을 추가합니다. 이 외의 설정은 공식 문서 를 참조해주세요.
1
2
3
4
5
6
7
8
9
10
11
12
@Getter
@Audited(withModifiedFlag = true) // 변경 유무 필드 추가
@Entity(name = "orders")
public class Order extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
......
}
3. 주의할 점과 한계
Envers는 사용하기 편리하지만 테스트 격리 시 테이블 명 조회, 성능이 중요할 때, 테이블 이력이 변경됐을 때 와 같은 몇 가지 주의사항과 한계가 명확한데, 이에 대해 살펴보겠습니다.
- 테스트 격리 시 테이블 명 조회
- 성능이 중요할 때
- 테이블 이력이 변경됐을 때
3-1. 테스트 격리 시 테이블 명 조회
Envers에서 관리하는 엔티티는 @Entity가 붙었더라도 EntityManager를 통해 찾아올 수 없습니다. 따라서 해당 엔티티에 해당하는 테이블을 조회할 때 문제가 발생하게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class RdbInitializationConfiguration {
......
@PostConstruct
public void afterPropertiesSet() {
Metamodel metamodel = entityManager.getMetamodel();
// EntityManager로 부터 테이블 명 조회
tableNames = metamodel.getEntities().stream()
.filter(isEntityTypeAndNotNull())
.map(toLowerCase())
.collect(Collectors.toUnmodifiableSet());
}
......
}
따라서 테스트 격리를 할 때 별도의 과정이 필요한데, 아래와 같은 방법으로 테이블을 조회해 올 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class RdbInitializationConfiguration {
......
private static final String ALL_TABLE_NAMES =
"SELECT table_name FROM information_schema.TABLES WHERE TABLE_SCHEMA = ?";
......
@PostConstruct
public void afterPropertiesSet() {
Query query = entityManager.createNativeQuery(ALL_TABLE_NAMES);
query.setParameter(1, schema);
List<Object> tables = query.getResultList();
for (Object obj : tables) {
tableNames.add(obj.toString());
}
}
......
}
3-2. 성능이 중요할 때
데이터가 실시간, 또는 정말 대량으로 추가/수정/삭제 된다면 Envers 보다는 다른 툴을 도입해야 합니다. Envers는 한 트랜잭션 내에서 발생하는 모든 이력에 대해 자동으로 추적하기 때문에 그만큼 성능 저하 가 발생할 수 있기 때문입니다.
3-3. 테이블 이력이 변경됐을 때
만약 추적하는 테이블의 칼럼이 추가/수정/삭제 된다면 이에 대해서는 추적 테이블에 대해서도 변경사항을 적용 해줘야 합니다. 이를 위해서는 Liquibase, Flyway와 같은 형성 관리 툴을 함께 도입하면 좋습니다.
4. 정리
Envers는 동일 트랜잭션 내의 변경 사항을 관리해 줍니다. 이는 너무 많은 양의 데이터가 추가/수정/삭제 되지 않는 경우 유용하게 사용할 수 있으며, 이를 통해 중요한 정보의 히스토리를 관리할 수 있습니다.