728x90
설계 동기
앞서 1편에서는 boolean 기반 sent 필드로 Kafka 전송 여부를 관리하는 기존 구조의 한계를 진단했습니다. 특히 다음 세 가지 문제를 해결하고자 리팩토링이 시작되었습니다:
- 전송 결과의 모호함: 실패와 성공을 구분할 수 없음
- 재시도 전략 부재: Kafka 전송 실패 시 복구 불가
- 배치 최적화 미흡: Kafka로 단일 이벤트를 순차 처리 → TPS 병목
설계 목표
이번 개선의 핵심 목표는 다음과 같습니다.
| 목표 항목 | 개선 전 | 개선 후 |
|---|---|---|
| 상태 표현 방식 | boolean sent |
enum OutboxStatus |
| Kafka 전송 전략 | 단일 전송 | 배치 전송 + 상태 기반 |
| 오류 처리 | 실패 감지 불가 | 실패 정보 저장 및 재시도 가능 |
| 전송 주기 | 없음 | 스케줄러로 관리 |
| 삭제 정책 | 수동 삭제 | SENT + 시간 조건 기반 자동 삭제 |
상태 모델링 - OutboxStatus
Kafka 전송 결과에 따라 상태를 분기해야 하므로 아래와 같이 enum 타입을 도입합니다.
public enum OutboxStatus {
PENDING, // 초기 상태, 전송 대기
SENT, // Kafka 전송 성공
FAILED, // 실패했지만 재시도 가능
DEAD // 5회 이상 실패 시 재시도 중단
}
→ 이제 “왜 안 보냈는지, 몇 번 실패했는지”까지 상태 기반으로 관리할 수 있습니다.
엔티티 구조 리팩토링
이전 구조:
private boolean sent;
리팩토링 후:
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private OutboxStatus status;
private int retryCount;
private String lastErrorMessage;
private LocalDateTime lastAttemptAt;
→ 이 필드들을 통해 재시도 로직, 장애 모니터링, dead-letter 감지가 가능해졌습니다.
전송 스케줄러 구조
스프링의 @Scheduled 기능을 통해 5초마다 전송 시도하도록 구성합니다.
@Scheduled(fixedDelay = 5000)
public void relayPendingEvents() {
List<OutboxEventEntity> events = repository.findByStatus(OutboxStatus.PENDING);
for (OutboxEventEntity event : events) {
try {
kafkaProducer.send(event); // Kafka 발송
event.markSuccess(); // SENT 전환
} catch (Exception e) {
event.markFailure(e.getMessage()); // FAILED 또는 DEAD 전환
}
}
repository.saveAll(events);
}
전송 전 status를 변경하지 않고, 실제로 Kafka에 보낸 이후에만 SENT로 변경합니다. 이게 정합성 보장의 핵심입니다.
상태 전이 흐름도
[PENDING]
│
├── Kafka 전송 성공 ───────▶ [SENT]
│
└── Kafka 전송 실패 ───────▶ [FAILED] (retryCount++)
│
└── retryCount ≥ 5 ───▶ [DEAD]
→ DEAD 상태는 장애를 외부 시스템에서 감지할 수 있는 실패 임계선 역할을 합니다.
자동 삭제 스케줄러
@Scheduled(cron = "0 0 * * * *") // 매 시 정각
public void deleteOldSentEvents() {
LocalDateTime cutoff = LocalDateTime.now().minusHours(1);
repository.deleteSentBefore(cutoff);
}
→ SENT 상태이면서 1시간 이상 경과한 이벤트는 자동 클린업 대상이 됩니다.
테스트 전략 요약
| 시나리오 | 기대 결과 |
|---|---|
| Kafka 전송 성공 시 | status → SENT |
| Kafka 전송 실패 시 | retryCount 증가, status → FAILED |
| 5회 이상 실패 시 | status → DEAD |
| SENT 1시간 후 삭제 스케줄러 | DB에서 제거됨 |
이 테스트들은 다음 편에서 코드와 함께 자세히 다룹니다.
운영 고려 사항
- Kafka 장애 시 메시지 유실 없음 →
status = PENDING으로 유지 - 전송 중단 감지 →
status = DEAD이벤트를 통해 모니터링 가능 - 관리자용 Dead Letter 조회 API 도 추후 고려 가능
향후 확장 방향
- Kafka 전송 재시도 backoff 정책 도입
- 이벤트 우선순위 처리 (예: 결제 → 우선 전송)
- Dead 상태 자동 Slack 알림 또는 관리자 대시보드 연결
- 1인 백엔드 개발이라... 뭔가... 맘대로 할수있다는게 ... 좋으면서도 좋지않은...
기능만 어거지로 구현하는것보단... 차근차근 고쳐나가면서 서비스를 유지하는게 개발자의 덕목이니까!
힘들고 뭔가... 자괴감들어도,... ㅠㅠ 파이팅!
728x90
'BindProject' 카테고리의 다른 글
| [Backend] 유저 활동 로그 수집 모듈 개발기 3편 : 로그 메타데이터 구현 (0) | 2025.06.22 |
|---|---|
| [Backend] 유저 활동 로그 수집 모듈 개발기 2-3편 : Outbox 설계 이후: 테스트 전략과 실제 구현 결과 (0) | 2025.06.21 |
| [Backend] 유저 활동 로그 수집 모듈 개발기 2-1편 : Kafka Outbox 패턴 기반 유저 활동 로그 수집기 리팩토링기(문제 확인) (0) | 2025.06.21 |
| [Backend] 유저 활동 로그 수집 모듈 개발기 2편 – 메모리 버퍼 기반 로그 수집기 설계와 구현 (0) | 2025.06.21 |
| [Backend] 유저 활동 로그 수집 모듈 개발기 1편 유저 활동 로그 수집기 구상하기 (0) | 2025.06.21 |