🎯 결론부터
✅ Listener는 엔티티 1개당 1번씩 호출됩니다.
❌ 여러 엔티티를 한 번에 묶어서 호출하지 않습니다.
즉,
- 엔티티 3개가 변경되면
- onPostUpdate()는 3번 호출됩니다
→ 하나로 묶어서 보고 싶다.
🔥 언제 호출되냐?
핵심은 flush 시점입니다.
Hibernate는:
- 트랜잭션 중 엔티티 변경
- flush 시점에 Dirty Checking 수행
- 변경된 엔티티 목록 수집
- 각각에 대해 SQL 실행
- 각각에 대해 PostUpdateEvent 발생
📌 예시
이 시점에서는 아무 일도 안 일어납니다.
flush 발생하면:
Dirty Checking 시작 → memo1 변경 감지 → memo2 변경 감지 → memo3 변경 감지
👉 Listener는 3번 호출됩니다.
🔥 그럼 “한 번에 모아서 처리”는 못 하나?
기본 PostUpdateEventListener는:
❌ 엔티티 단위 호출
하지만 이런 방법은 있습니다:
ThreadLocal에 모아서 commit 시점 처리
리스너에서:
에 모아두고
트랜잭션 완료 시점에 한 번에 저장.
1️⃣ 성능 문제는 없나?
✅ 기본 전제
- ThreadLocal은 스레드별 저장소
- Spring 트랜잭션 = 보통 1 스레드
- 요청당 하나의 ThreadLocal List
👉 일반적인 웹 환경에서는 동시성 충돌 없음
⚠️ 하지만 주의할 점
① 메모리 증가 가능성
하나의 트랜잭션에서:
- 엔티티 1000개 변경
- JSON 1000개 누적
👉 List가 커질 수 있음
하지만 보통 실무 트랜잭션은 수십 개 수준
→ 큰 문제 없음
② 반드시 clear() 해야 함
ThreadLocal은 제거하지 않으면:
- WAS thread pool 재사용 시
- 이전 요청 데이터가 남을 수 있음
👉 afterCompletion에서 remove() 필수
③ @Async 사용하면 깨짐
ThreadLocal은 스레드 단위
사용하면 다른 스레드로 이동
→ AuditContext 공유 안 됨
2️⃣ 동시성 문제는?
❌ 없음 (기본 구조에서는)
이유:
- ThreadLocal은 스레드별 저장
- 다른 요청은 다른 스레드
즉:
완전히 분리됨
코드 추가
@Component
public class AuditCollector {
private static final ThreadLocal<List<Object>> CHANGE_LIST = ThreadLocal.withInitial(ArrayList::new);
public void add(Object change) {
CHANGE_LIST.get().add(change);
}
public List<Object> getChangeList() {
return CHANGE_LIST.get();
}
public boolean isEmpty() {
return CHANGE_LIST.get().isEmpty();
}
public void clear() {
CHANGE_LIST.remove();
}
}
Slf4j
@Component
@RequiredArgsConstructor
public class AuditTransactionHandler {
private final AuditCollector collector;
private final ObjectMapper objectMapper;
private static final String SYNC_KEY = "AUDIT_TX_SYNC";
public void registerIfNeeded() {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
return;
}
if (TransactionSynchronizationManager.getResource(SYNC_KEY) != null) {
return; // 이미 등록됨
}
TransactionSynchronizationManager.bindResource(SYNC_KEY, true);
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
if (collector.isEmpty()) {
return;
}
try {
Map<String, Object> txRoot = new LinkedHashMap<>();
txRoot.put("changedAt", LocalDateTime.now());
txRoot.put("entities", collector.getChangeList());
String json = objectMapper.writeValueAsString(txRoot);
// 🔥 여기서 DB 저장 or Kafka 발행
log.info("AUDIT TX: {}", json);
} catch (Exception e) {
log.error("Audit commit error", e);
}
}
@Override
public void afterCompletion(int status) {
collector.clear();
TransactionSynchronizationManager.unbindResource(SYNC_KEY);
}
}
);
}
}
@Component
@RequiredArgsConstructor
@Slf4j
public class AuditEventListener implements PostUpdateEventListener {
... 중략
// =========================
// 최종 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);
auditCollector.add(root); // ThreadLocal에 저장
auditTransactionHandler.registerIfNeeded(); ; // 트랜잭션 동기화 등록
}
... 중략
}
{
"changedAt": "2026-03-03T11:07:29",
"entities": [
{
"entity": "TestEntity",
"entityId": "142",
"changedAt": "2026-03-03T11:07:29",
"changes": {
"updatedAt": {
"before": "2026-03-03T11:03:41",
"after": "2026-03-03T11:07:29"
},
"memo": {
"before": "변경 감지 테스트_final13333333",
"after": "변경 감지 테스트_final"
}
}
},
{
"entity": "TestEntity",
"entityId": "140",
"changedAt": "2026-03-03T11:07:29",
"changes": {
"updatedAt": {
"before": "2026-03-03T11:03:41",
"after": "2026-03-03T11:07:29"
},
"memo": {
"before": "변경 감지 테스트_final13333333_test",
"after": "변경 감지 테스트_final_test"
}
}
}
]
}
주의사항
1.afterCommit()은 @Transactional이 없으면 호출 안됨
왜 afterCommit이 호출 안 되냐?
Spring 내부 구조:
이건:
활성화된 트랜잭션이 있을 때만 동작
@Transactional이 없으면:
그래서 등록 자체가 안 됨.
2. beforeCommit()보다 afterCommit()을 사용해야
repository.save()
findById -> save -> findById -> save 이런 구조를 사용할 수 있다.
'스프링 > JPA' 카테고리의 다른 글
| 히스토리 관리 - 데이터 변경 감지 (0) | 2026.02.25 |
|---|---|
| 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 |
댓글