BindProject

[Backend] 유저 활동 로그 수집 모듈 개발기 3편 : 로그 메타데이터 구현

dding-shark 2025. 6. 22. 13:54
728x90

1. 도입: 단순 로그를 넘어서

서비스에서 유저가 어떤 행동을 했는지를 기록하는 건 단순한 기능 같지만, 실제로는 시스템의 인사이트와 회복력을 좌우하는 핵심 관측 지표가 된다. 예를 들어 다음과 같은 시나리오를 상상해보자.

  • 어떤 유저가 로그인에 실패한 후 탈퇴 페이지로 이동한다.
  • 게시글을 클릭했는데 500 에러가 발생한다.
  • 예약 시스템에서 요청이 들어왔으나 Kafka에 전송되지 않았다.

이런 상황을 파악하기 위해선 단순한 “누가 무엇을 했다” 수준의 로그로는 부족하다. 우리는 유저의 행동을 시간, 플랫폼, 처리 결과 등 **맥락(Context)**과 함께 수집해야 한다.


2. 로그 시스템 설계 목표

이번 로그 수집 시스템은 다음의 목표를 달성하고자 설계되었다:

항목 설명
표준화 로그 데이터에 공통적으로 포함되어야 할 필드를 정의
확장성 행동마다 필요한 상세 정보를 유연하게 수용
전송 안전성 Outbox 패턴을 활용해 Kafka 전송의 신뢰성 확보
구조적 분리 BFF, 도메인 로직과 독립된 이벤트 설계 유지

3. 로그 메타데이터 구조 설계

모든 로그는 기본적인 맥락 정보를 가져야 한다. 이를 위해 LogMetadata 객체를 설계했다:

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LogMetadata {

    private String userAgent;
    private String ip;
    private String referer;
    private String source;             // "WEB", "MOBILE" 등
    private long durationMillis;       // 처리 시간 (ms)
    private boolean success;           // 처리 결과
}

이 구조는 BFF에서 쉽게 추출할 수 있는 정보를 담아 유저의 환경과 요청 결과를 파악하는 데 도움을 준다.


4. 로그 이벤트 구조

행위 단위의 로그는 LogEvent 클래스로 정의한다. 이 클래스는 CustomEvent를 구현해 Kafka 전송 및 Outbox 저장에 사용된다.

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LogEvent implements CustomEvent {

    private Long userId;
    private LogActionType actionType;
    private LocalDateTime timestamp;
    private LogMetadata metadata;

    private Object payload; // 행동마다 다른 정보 (선택 사항)

    @Override
    public String name() {
        return actionType.name();
    }

    @Override
    public String getTopic() {
        return LogTopic.ACTIVITY_LOG;
    }
}
  • payload: 자유로운 데이터 확장성을 위해 Object 타입 사용. Jackson 직렬화 전제하에 Map, DTO 모두 수용 가능.

5. 로그 행동 타입과 토픽

public enum LogActionType {
    LOGIN, LOGOUT, VIEW_PAGE, RESERVE, CANCEL, UPDATE_PROFILE
}

public class LogTopic {
    public static final String ACTIVITY_LOG = "activity.log.save";
}
  • 로그 종류마다 actionType으로 구분되고, Kafka 전송 시에는 고정된 토픽 activity.log.save로 발송된다.

6. 사용 흐름 예시

예: 유저 로그인 성공 시

LogEvent loginEvent = LogEvent.builder()
    .userId(42L)
    .actionType(LogActionType.LOGIN)
    .timestamp(LocalDateTime.now())
    .metadata(LogMetadata.builder()
        .userAgent(request.getHeader("User-Agent"))
        .ip(request.getRemoteAddr())
        .referer(request.getHeader("Referer"))
        .source("WEB")
        .durationMillis(120)
        .success(true)
        .build())
    .payload(Map.of("method", "email"))
    .build();

Outbox 발행

outboxEventPublisher.publish(loginEvent);

→ 이 이벤트는 OutboxEventEntity로 직렬화되어 저장되고, 이후 스케줄러를 통해 Kafka에 안전하게 전송된다.


7. 장점 및 설계 당위성

구조적 강점

설계 요소 효과
LogMetadata 별도 구성 모든 로그에 공통 정보 통합
Object payload 다양한 로그 시나리오 수용 (e.g. 게시글 ID, 실패 이유 등)
CustomEvent 인터페이스 통합 Outbox 패턴과 일관성 유지
Kafka 토픽 자동 지정 서비스별 메시지 라우팅 간소화

확장성 고려

  • 향후 행동별 payload DTO 정의 가능
  • Slack 연동, Dead Letter 처리 등도 구조적으로 대응 가능

8. 회고: 의미 있는 로그란?

이 시스템은 단순한 “이벤트 전송”을 넘어서, 서비스에서 발생한 일을 다각도로 관찰할 수 있는 기반을 만든다.

특히 다음과 같은 효과를 기대할 수 있다:

  • 모니터링: 특정 이벤트 누락이나 지연 발생 시 추적 가능
  • 분석: 행동별 유저 사용 패턴 파악
  • 복구성 향상: Kafka 장애에도 Outbox로 인해 로그 손실 방지

필요하면 위 글을 .md 포맷으로도 정리해줄 수 있어. 구조나 방향이 마음에 들면 이어서 2편도 정리해줄게.

728x90