728x90
1. 도입 – 로그를 Kafka로 바로 보내면 안 되는 이유
1편에서 우리는 왜 유저 활동 로그를 수집해야 하는지, 그리고 Kafka를 도입하기로 결정한 이유를 설명했다.
하지만 Kafka를 도입한다고 해서 모든 게 해결되진 않았다.
유저 요청이 올 때마다 Kafka로 바로 전송한다면?
- 요청 응답 속도에 영향을 줄 수 있다.
- Kafka에 대한 의존성이 높아지며, 장애에 취약해진다.
- 로그가 수백 TPS로 들어오는 경우, Kafka 자체도 부하를 받는다.
그래서 우리는 메모리 버퍼 기반 로그 수집기를 직접 만들기로 했다.
2. 설계 목표 – 단순히 "버퍼에 쌓기"가 아니다
우리가 만든 수집기는 단순한 List 저장소가 아니다.
아래 세 가지 요구사항을 만족해야 했다.
1. 로그 유실 없이, 빠르게 저장
→ 버퍼는 비동기로 데이터를 받되, Thread-safe 해야 한다.
2. 기준 도달 시 Kafka로 flush
→ 버퍼 크기 또는 주기 기준으로 flush 트리거가 작동해야 한다.
3. flush 중에도 로그를 받는 구조
→ flush 중인 버퍼와 새로 수집되는 버퍼는 분리돼야 한다.
3. 구조 설계 – DoubleBuffer 구조를 사용하자
단일 버퍼로는 flush 중에 write가 막힐 수 있다.
우리는 Double Buffer 구조를 도입했다.
구조 다이어그램
User Request
│
▼
LogBufferManager (write)
├── activeBuffer ← 계속 write
└── flushBuffer ← 일정 조건에서 Kafka 전송흐름 요약
- 로그는 activeBuffer에 쌓인다.
- 크기 혹은 시간 기준에 도달하면, 버퍼를 swap한다.
- 이전 activeBuffer였던 flushBuffer는 별도 스레드에서 Kafka로 전송된다.
- 새로운 activeBuffer는 계속 로그를 받는다.
4. flush 조건 – 언제 버퍼를 Kafka로 보내는가?
우리는 두 가지 조건 중 하나라도 만족하면 flush를 실행하도록 설계했다.
최대 로그 개수예: 100건 도달최대 수집 시간예: 3초 주기
if (buffer.size() >= MAX_SIZE || lastFlushTime + 3초 < now) {
flush();
}
이 조건은 단순하지만 유실 방지와 응답 지연 최소화에 효과적이었다.
5. 임시 구현 코드 예시
로그 DTO
public class UserActivityLog {
private String userId;
private String action;
private LocalDateTime timestamp;
// ... 기타 정보
}
LogBufferManager
@Component
public class LogBufferManager {
private final List<UserActivityLog> activeBuffer = new CopyOnWriteArrayList<>();
private final ExecutorService flushExecutor = Executors.newSingleThreadExecutor();
public synchronized void save(UserActivityLog log) {
activeBuffer.add(log);
if (activeBuffer.size() >= MAX_BUFFER_SIZE) {
flush();
}
}
@Scheduled(fixedRate = 3000)
public synchronized void periodicFlush() {
flush();
}
private void flush() {
if (activeBuffer.isEmpty()) return;
List<UserActivityLog> toFlush = new ArrayList<>(activeBuffer);
activeBuffer.clear();
flushExecutor.submit(() -> kafkaProducer.send(toFlush));
}
}
Kafka Producer Stub
@Component
public class KafkaActivityProducer {
public void send(List<UserActivityLog> logs) {
for (UserActivityLog log : logs) {
kafkaTemplate.send("logsave", serialize(log));
}
}
}
6. 테스트 전략 – 동시성과 유실 여부 검증
- 10,000건 이상의 로그를 10개의 스레드로 동시에
save()호출 → 유실 없음 검증 - flush 중에도 save가 차단되지 않음 → 블로킹 없는 구조 확인
- Kafka stub로 전달된 데이터 일관성 검증
실제 테스트는 3편에서 상세히 다룬다.
7. 회고 및 확장 고려사항
잘된 점
- flush 중에도 로그를 받을 수 있는 구조로 설계
- Kafka 장애가 발생하더라도 로그 손실 최소화
- 확장성 고려한 구조
고민할 점
- JVM이 죽으면 메모리에 쌓인 로그는 유실됨 → Outbox 패턴 도입 고려
- flush 실패 시 재시도 정책 필요
- Buffer 크기/주기 조절은 추후 실시간 모니터링 기반으로 튜닝할 예정
다음 편 예고
3편에서는 이 구조를 실제 Kafka와 연결하고,
단위 테스트, 통합 테스트, 성능 테스트까지 포함한 전체 흐름 검증을 다룬다.
728x90
'BindProject' 카테고리의 다른 글
| [Backend] 유저 활동 로그 수집 모듈 개발기 2-2편 :Kafka 전송 최적화를 위한 배치 전송 구조 개선기 (0) | 2025.06.21 |
|---|---|
| [Backend] 유저 활동 로그 수집 모듈 개발기 2-1편 : Kafka Outbox 패턴 기반 유저 활동 로그 수집기 리팩토링기(문제 확인) (0) | 2025.06.21 |
| [Backend] 유저 활동 로그 수집 모듈 개발기 1편 유저 활동 로그 수집기 구상하기 (0) | 2025.06.21 |
| [Backend] Image모듈 개발기 3편. 이미지 서비스 흐름과 테스트 전략 (0) | 2025.06.21 |
| [Backend] Image모듈 개발기 2편: 이미지도 생명주기가 있다면 – 도메인 모델과 상태 설계 (0) | 2025.06.21 |