
이벤트 발행과 Outbox에 대한 이해가 크지 않은 상태에서 구현을 했더니 이벤트 발행 실패 재처리를 위해서 row가 수정되는 값이라고 생각되어 BaseEntity를 상속받게 했는데, 이벤트 발행 로직을 리펙토링하며 보니, 다음처럼 테이블에 쓸데없는 정보가 저장되고있는 것 같았다.

이벤트 발행의 주체는 인증 주체가 아니여서 다른 도메인 엔티티에서 관리할 때 배치 작업 등으로 시스템 생성을 표시하기 위한 00000..-0000-... 상수UUID가 들어가게 되었다.
다른 테이블과의 일관성을 위해서 라는 변명을 하기에도 단순 이력 조회와 저장이 목적이라 BaseEntity를 상속받지 않은 유저 닉네임 변경 테이블이 이미 존재했다. (닉네임 변경이력은 changedAt과 changedBy로 관리된다.)
@Transactional
public SignupResponse signup(SignupCommand command) {
Password password = Password.of(command.password(), passwordEncoder);
User user = User.create(
command.loginId(),
command.email(),
password,
command.name(),
command.nickname(),
command.phone(),
command.gender(),
command.birthDate()
);
UserNicknameHistory nicknameHistory = UserNicknameHistory.of(
user.getId(),
null,
user.getNickname(),
user.getId(),
NicknameChangeReason.CREATE
);
// 1. AuditorAware가 신규 유저 본인 ID를 읽어갈 수 있도록 컨텍스트 주입
UserContextHolder.set(new UserContext(user.getId(), user.getRole()));
try {
User saved = userRepository.save(user); // 2. save
nicknameHistoryRepository.save(nicknameHistory);
// 3. 이때 이벤트는 큐에만 등록되고 리스너 미실행
eventPublisher.publishEvent(UserCreatedEvent.from(saved));
return SignupResponse.from(saved);
} catch (DataIntegrityViolationException e) {
throw resolveDuplicateException(e);
} finally {
UserContextHolder.clear(); // 4. 컨텍스트 소멸 ❌
}
}
5. 트랜잭션 커밋 직전 → BEFORE_COMMIT 발동
6. OutboxEventHandler.onUserCreated() 실행
7. outboxRepository.save(outbox) → AuditingEntityListener가 created_by 읽으려 함
8. 값이 null → System UUID ("0000...")
TransactionSynchronization으로 트랜잭션 이벤트를 만들고, cleanup 시점을 트랜잭션 안에 finally 블록 대신 트랜잭션 완료 이후로 이동시키면 된다. (어차피 필드를 지울거라 사용x)
@Transactional
public SignupResponse signup(SignupCommand command) {
Password password = Password.of(command.password(), passwordEncoder);
User user = User.create(/* ... */);
UserNicknameHistory nicknameHistory = UserNicknameHistory.of(
user.getId(), null, user.getNickname(), NicknameChangeReason.CREATE
);
UserContextHolder.set(new UserContext(user.getId(), user.getRole()));
// finally 제거 → TransactionSynchronization으로 cleanup 위임
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
UserContextHolder.clear(); // BEFORE_COMMIT 이후, 트랜잭션 완료 시점에 정리
}
}
);
try {
User saved = userRepository.save(user);
nicknameHistoryRepository.save(nicknameHistory);
eventPublisher.publishEvent(UserCreatedEvent.from(saved));
return SignupResponse.from(saved);
} catch (DataIntegrityViolationException e) {
throw resolveDuplicateException(e);
}
// finally 블록 제거
}
Outbox 테이블은 "누가 만들었는가"가 의미없는 인프라 테이블이다.
이벤트를 Kafka에 안정적으로 전달하기 위한 중간 저장소일 뿐이고, 감사(Audit) 대상이 아니다.
이벤트 발행 시 published_at과 last_failure_at 필드가 이미 이 역할을 하고 있다.
발행된 이벤트는 ublished=true로 표시되며 물리삭제도 가능한 부분이다.
p_users : BaseEntity
p_user_nickname_histories : 자체 (changedBy = 행위자)
p_outbox 에서 의미 있는 컬럼:
- created_at: row 생성 시점
- published_at: 발행 성공 시점
- last_failure_at: 마지막 실패 시점
"컴퓨터 과학에서 가장 어려운 일은 캐시 무효화와 이름 짓기이다."— 필 칼튼 (Phil Karlton)
아웃박스 필드를 정리하려다보니 기존 엔티티의 Audit도 값을 직접 할당하고있다는 것을 발견해서 고치려고 했다.
그런데 한 가지가 걸렸다. '변경' 이력 이라는 특성 때문에 changed *이름을 사용하고 있었는데, @CreatedDate / @CreatedBy 어노테이션으로 자동 채우니 조금 어색한 느낌이 들었다.
처음 닉네임 변경 이력 테이블에 created *를 사용하는 BaseEntity를 상속받지 않고
changed *를 사용한 이유는 다음과 같았다.
일반 엔티티:
이력 엔티티:
(시스템적):
- created_at, updated_at, deleted_at
- 모든 엔티티가 가지는 같은 컬럼명 (BaseEntity)
(도메인적):
- User: created_at (가입)
- Order: ordered_at (주문 발생)
- Payment: paid_at (결제 완료)
→ 닉네임 수정 기능 구현중... : "이 시간이 도메인적으로 무엇을 의미하나?"
NicknameHistory: changed_at (변경 발생!)
(망했다)
회원가입때 설정한 닉네임도 닉네임변경 이력으로 취급해 30일 제한 정책에 포함되도록 했는데,
회원가입시에도 닉네임"변경"이력 테이블에 닉네임"생성"이력을 넣는다는게 어색하다고 어제 튜터님이 피드백 주셨다.
그런데 영문 테이블 이름을 생각해봐야한다. 테이블 이름은 p_user_nickname_histories
즉, Change라는 말은 테이블이름에 없었다.
아마 무의식중에 생성 이력까지 관리한다는 의도를 반영했던 게 아닐까..?
앞으로 테이블 명칭은 닉네임변경이력이 아닌 닉네임이력 테이블이라 부르는게 맞는것 같다.
수정을 구현할 때 만든 테이블이라 입에 그렇게 붙은 탓인것 같다.
그래서 더욱, 필드 이름에 created * 를 사용하는게 맞지 않나? 라는 생각이 들었다.
그런데 지금 수정하자니 DB 마이그레이션도 작성해야한다.
그런데 지금 수정을 안하자니 이미 Outbox 필드를 수정하는 중이었다.
아 어떡하지~~

와, 너 정말 ***핵심을 찔렀어.***
기존에 작성하려던 db 마이그레이션 이름을
V4__simplify_outbox_audit_columns.sql 에서
V4__standardize_and_simplify_audit_fields.sql 로 바꿔 진행하기로 했다.
-- ============================================================
-- 1. p_user_nickname_histories 컬럼명 통일
-- ============================================================
ALTER TABLE p_user_nickname_histories
RENAME COLUMN changed_at TO created_at;
ALTER TABLE p_user_nickname_histories
RENAME COLUMN changed_by TO created_by;
COMMENT ON COLUMN p_user_nickname_histories.created_at IS '닉네임 생성/변경 이벤트 발생 시각';
COMMENT ON COLUMN p_user_nickname_histories.created_by IS '닉네임 생성/변경 이벤트 행위자';
-- ============================================================
-- 2. p_outbox 에서 의미 약한 audit 컬럼 정리
-- ============================================================
-- BaseEntity 필드 제거
ALTER TABLE p_outbox DROP COLUMN created_by;
ALTER TABLE p_outbox DROP COLUMN updated_at;
ALTER TABLE p_outbox DROP COLUMN updated_by;
ALTER TABLE p_outbox DROP COLUMN deleted_at;
ALTER TABLE p_outbox DROP COLUMN deleted_by;
-- 인덱스 재설정
DROP INDEX IF EXISTS idx_outbox_unpublished;
CREATE INDEX idx_outbox_unpublished
ON p_outbox (created_at)
WHERE published = FALSE;
DROP INDEX IF EXISTS idx_outbox_aggregate;
CREATE INDEX idx_outbox_aggregate
ON p_outbox (aggregate_type, aggregate_id);
-- 코멘트
COMMENT ON COLUMN p_outbox.created_at IS 'Outbox row 생성 시각';