본문 바로가기
스프링/JPA

히스토리 관리 - 데이터 변경 감지

by 공부 안하고 싶은 사람 2026. 2. 25.
반응형

📌 데이터 변경 감지 방식 비교 정리

🎯 목적

  • 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

댓글