반응형
📌 데이터 변경 감지 방식 비교 정리
🎯 목적
- API를 통한 CRUD 변경 이력 확인
- 운영 관점에서 데이터 변경 추적
- bulk update 포함 여부 고려
- MSA 구조 + MySQL 환경
1️⃣ Hibernate Event Listener
✅ 개념
Hibernate 내부 이벤트(PostInsert, PostUpdate, PostDelete 등)를 가로채서
엔티티 변경 시점에 원하는 로직을 실행하는 방식.
“엔티티 변경 시점에 개입하는 구조”
✅ 특징
- Hibernate 레벨에서 동작
- Dirty Checking 기반
- 변경 전/후 값 접근 가능
- JSON 등 자유로운 포맷 저장 가능
- 특정 엔티티만 선택 적용 가능
- Kafka 발행 등 확장 가능
- 테이블 자동 생성 없음 (직접 설계)
❗ 한계
- JPQL bulk update 감지 불가
- native query 감지 불가
- DB 직접 수정 감지 불가
- JPA를 우회하면 이벤트 발생하지 않음
- 멀티 EntityManagerFactory 환경에서 설정 복잡
- 트랜잭션 내 동작 → 성능 영향 가능
🔎 한 줄 요약
“엔티티 기반 변경을 자유롭게 가공하는 방식”
하지만 JPA를 우회하면 감지 못함.
@Component
@RequiredArgsConstructor
@Slf4j
public class AuditEventListener implements PostUpdateEventListener {
private final ObjectMapper objectMapper;
// =========================
// Hibernate Listener Entry
// =========================
@Override
public void onPostUpdate(PostUpdateEvent event) {
Object entity = event.getEntity();
Object entityId = event.getId();
String[] propertyNames = event.getPersister().getPropertyNames();
Object[] oldState = event.getOldState();
Object[] newState = event.getState();
if (oldState == null || newState == null) {
return;
}
Map<String, Map<String, Object>> changes = buildChangesFromArray(propertyNames, oldState, newState);
buildFinalJson(entity.getClass().getSimpleName(), entityId, changes);
}
// =========================
// Bulk / Service 용 Entry
// =========================
public void buildDiffJson(
String entityName,
Object entityId,
Map<String, Object> beforeMap,
Map<String, Object> afterMap,
Set<String> ignoreFields
) {
Map<String, Map<String, Object>> changes = buildChangesFromMap(beforeMap, afterMap, ignoreFields);
buildFinalJson(entityName, entityId, changes);
}
// =========================
// 공통 변경 감지 로직 (Array)
// =========================
private Map<String, Map<String, Object>> buildChangesFromArray(
String[] propertyNames,
Object[] oldState,
Object[] newState
) {
Map<String, Map<String, Object>> changes = new LinkedHashMap<>();
for (int i = 0; i < propertyNames.length; i++) {
if (!Objects.equals(oldState[i], newState[i])) {
changes.put(propertyNames[i],
createChange(oldState[i], newState[i]));
}
}
return changes;
}
// =========================
// 공통 변경 감지 로직 (Map)
// =========================
private Map<String, Map<String, Object>> buildChangesFromMap(
Map<String, Object> beforeMap,
Map<String, Object> afterMap,
Set<String> ignoreFields
) {
Map<String, Map<String, Object>> changes = new LinkedHashMap<>();
for (String key : beforeMap.keySet()) {
if (ignoreFields != null && ignoreFields.contains(key)) {
continue;
}
Object beforeValue = beforeMap.get(key);
Object afterValue = afterMap.get(key);
if (!Objects.equals(beforeValue, afterValue)) {
changes.put(key, createChange(beforeValue, afterValue));
}
}
return changes;
}
// =========================
// 변경 객체 생성
// =========================
private Map<String, Object> createChange(Object before, Object after) {
Map<String, Object> diff = new LinkedHashMap<>();
diff.put("before", before);
diff.put("after", after);
return diff;
}
// =========================
// 최종 JSON 생성 (공통)
// =========================
private void buildFinalJson(
String entityName,
Object entityId,
Map<String, Map<String, Object>> changes
) {
if (changes == null || changes.isEmpty()) {
return;
}
Map<String, Object> root = new LinkedHashMap<>();
root.put("entity", entityName);
root.put("entityId", String.valueOf(entityId));
root.put("changedAt", LocalDateTime.now());
root.put("changes", changes);
try {
String json = objectMapper.writeValueAsString(root);
if (json != null) {
log.info(json);
}
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean requiresPostCommitHanding(EntityPersister persister) {
return false;
}
}
@Configuration
public class HibernateEventConfig {
private final AuditEventListener auditEventListener;
private final EntityManagerFactory writeEmf;
public HibernateEventConfig(AuditEventListener auditEventListener,
@Qualifier("pmsOrderWriteEntityManagerFactory") EntityManagerFactory writeEmf) {
this.auditEventListener = auditEventListener;
this.writeEmf = writeEmf;
}
@PostConstruct
public void registerListeners() {
SessionFactoryImpl sessionWriteFactory = writeEmf.unwrap(SessionFactoryImpl.class);
EventListenerRegistry registry = sessionWriteFactory.getServiceRegistry().getService(EventListenerRegistry.class);
// registry.appendListeners(EventType.POST_INSERT, auditEventListener);
registry.appendListeners(EventType.POST_UPDATE, auditEventListener);
// registry.appendListeners(EventType.POST_DELETE, auditEventListener);
System.out.println("🔥 Hibernate Listener 등록 완료");
}
}
2️⃣ Hibernate Interceptor
✅ 개념
Hibernate flush/dirty checking 시점에 개입하는 더 낮은 레벨 확장 포인트.
Event Listener와 유사하지만 더 코어 레벨.
✅ 특징
- 엔티티 변경 감지 가능
- 커스터마이징 자유도 높음
- 공통 감사 로직 구현에 적합
❗ 한계
- bulk update 감지 불가
- native query 감지 불가
- DB 직접 수정 감지 불가
- 엔티티 기반 변경만 추적 가능
🔎 한 줄 요약
Event Listener와 본질적 한계는 동일.
3️⃣ Spring Envers (간단 요약)
✅ 특징
- Hibernate 공식 Audit 모듈
- _AUD 테이블 자동 생성
- INSERT / UPDATE / DELETE 자동 이력 저장
- revision 기반 시점 조회 가능
- 빠른 도입 가능
❗ 한계
- 엔티티 기반 변경만 감지
- bulk update / native query 감지 불가
- 모든 컬럼 통째로 저장
- 커스터마이징 자유도 낮음
- 테이블 수 증가
- MSA 확장성 낮음
🔎 한 줄 요약
“빠르게 도입 가능한 엔티티 이력 관리 솔루션”
그러나 유연성과 확장성은 제한적.
4️⃣ JDBC Proxy (p6spy)
✅ 개념
DataSource를 래핑하여 모든 실행 SQL을 가로채는 방식.
✅ 특징
- 모든 SQL 감지 (bulk 포함)
- native query 감지 가능
- JPA 우회 SQL도 감지 가능
- 설정만으로 도입 가능
❗ 한계
- 변경 전 값 알 수 없음
- SQL 로그 수준
- 감사용으로는 추가 가공 필요
- 트래픽 많으면 로그 폭증
🔎 한 줄 요약
“SQL 실행 로그 추적 도구”
변경 사실은 알 수 있으나 데이터 diff 추적은 어려움.
5️⃣ MySQL Binlog CDC
✅ 개념
MySQL Binary Log를 읽어
실제 DB 변경 이벤트를 감지하는 방식.
✅ 특징
- 모든 변경 감지
- bulk update 포함
- native query 포함
- DB 직접 수정 포함
- 트랜잭션 순서 보장
- JPA와 무관
❗ 한계
- replication 설정 필요
- 운영 난이도 높음
- 직접 구현은 복잡
- 인프라 의존성 큼
🔎 한 줄 요약
“DB 레벨에서 진짜 변경을 추적하는 방식”
6️⃣ Debezium (CDC 플랫폼)
✅ 개념
MySQL binlog를 읽어 Kafka 등으로 발행하는
검증된 CDC 솔루션.
✅ 특징
- 모든 DB 변경 감지
- bulk update 감지 가능
- native query 감지 가능
- MSA 구조에 최적화
- 중앙 감사 시스템 구축 가능
- 확장성 매우 높음
- 트랜잭션 단위 이벤트 제공
❗ 한계
- Kafka 등 인프라 필요
- 초기 도입 비용 존재
- 운영 구성 이해 필요
🔎 한 줄 요약
“MSA 환경에서 가장 확장성 높은 감사 구조”
🔥 전체 비교
방식엔티티 변경Bulk UpdateNative QueryDB 직접 수정확장성
| Event Listener | ✅ | ❌ | ❌ | ❌ | 중 |
| Interceptor | ✅ | ❌ | ❌ | ❌ | 중 |
| Envers | ✅ | ❌ | ❌ | ❌ | 낮음 |
| p6spy | SQL 수준 | ✅ | ✅ | ✅ | 중 |
| Binlog CDC | ✅ | ✅ | ✅ | ✅ | 높음 |
| Debezium | ✅ | ✅ | ✅ | ✅ | 매우 높음 |
반응형
'스프링 > JPA' 카테고리의 다른 글
| 히스토리 관리 - 데이터 변경 감지 - 심화 (0) | 2026.03.03 |
|---|---|
| Querydsl (0) | 2021.08.24 |
| Spring-Data-Jpa (0) | 2021.08.06 |
| Envers / spring-data-envers (0) | 2021.08.03 |
| JPA 최적화, Hint (0) | 2021.06.04 |
댓글