BindProject

[Backend] 유저 활동 로그 수집 모듈 개발기 4편 : 로그 수집 서비스 구현

dding-shark 2025. 6. 22. 14:37
728x90

1. 도입

앞선 1, 2편에서는 로그 수집이 왜 필요한지, 그리고 이를 어떻게 Kafka 기반으로 아웃박스 패턴과 함께 구성했는지를 살펴봤습니다.
이번 편에서는 수신 측 서비스가 어떻게 Kafka 이벤트를 받아서 저장하고, 분석 가능한 형태로 가공하는지를 중심으로 다룹니다.


2. 설계 목표

항목 설계 의도

Kafka 이벤트 수신 실시간 로그 수신
데이터 정규화 분석 가능한 구조로 저장
확장성 다양한 action/payload 처리 가능
쿼리 최적화 URL, 사용자 기반 분석 가능

3. 저장 모델 설계

LogEventEntity

@Entity
@Table(name = "user_activity_log")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LogEventEntity {

    @Id
    private Long id;

    private Long userId;

    @Enumerated(EnumType.STRING)
    private LogActionType actionType;

    private String url;

    private LocalDateTime actionTime;

    private String clientInfo;

    private Long durationMillis;

    private boolean success;

    private String failureReason;

    @ElementCollection(fetch = FetchType.LAZY)
    @CollectionTable(name = "user_activity_log_context", joinColumns = @JoinColumn(name = "log_id"))
    @MapKeyColumn(name = "key")
    @Column(name = "value")
    private Map<String, String> context;
}
  • url, durationMillis, clientInfo 등을 모두 컬럼으로 분리
  • context는 유연한 key-value 구조로 확장 지원
  • 사용자 행동을 쿼리로 분석하기 쉽게 구조화

4. Kafka 수신 및 저장 흐름

KafkaListener 코드

@Component
@RequiredArgsConstructor
public class LogEventConsumer {

    private final LogEventRepository repository;
    private final Snowflake snowflake;

    @KafkaListener(topics = "ACTIVITY_LOG_SAVE", groupId = "log-consumer")
    public void consume(String message) {
        LogEvent event = DataSerializer.deserialize(message, LogEvent.class).orElseThrow();

        LogEventEntity entity = LogEventEntity.builder()
                .id(snowflake.nextId())
                .userId(event.getUserId())
                .actionType(event.getActionType())
                .url(event.getMetadata().getContext().get("url"))
                .actionTime(event.getTimestamp())
                .clientInfo(event.getMetadata().getClientInfo())
                .durationMillis(event.getMetadata().getDurationMillis())
                .success(event.getMetadata().isSuccess())
                .failureReason(event.getMetadata().getFailureReason())
                .context(event.getMetadata().getContext())
                .build();

        repository.save(entity);
    }
}

5. 의존성 및 구성 정보

build.gradle

implementation 'org.springframework.kafka:spring-kafka'
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'jakarta.persistence:jakarta.persistence-api'

application.yml

spring:
  kafka:
    bootstrap-servers: localhost:9092
    consumer:
      group-id: log-consumer
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer

6. 회고 및 확장 방향

  • Kafka 이벤트를 수신한 후, 정규화된 엔티티로 저장하면 SQL 기반 분석이 매우 쉬워짐
  • URL 기반 요청 수, 행동 타입별 사용자 분포 등 BI 도구 연동이나 대시보드 구성이 가능해짐
  • 추후 LogActionType에 따른 우선순위 처리, 별도 테이블 분기 저장도 가능

728x90