← 목록으로
로그 수집 파이프라인 2026.03.13 ✍️ 최하영

로그 파이프라인 4- 최근 본 상품 조회 결론

최근 본 상품 기능은 처음에는 Redis 기반 실시간 캐시 구조를 검토했지만, 최종적으로는 Kafka Consumer + PostgreSQL Upsert 방식이 요구사항 대비 더 단순하고 충분히 빠르다고 판단하여 채택했습니다.

로그 파이프라인 4- 최근 본 상품 조회 결론

0. 하지만 Redis는 과하다고 판단했다

초기 설계는 꽤 그럴듯해 보였지만, 실제 구현 관점에서 다시 검토했을 때 최근 본 상품 조회를 위해 Redis를 도입하는 것은 과하다고 판단했습니다.

그 이유는 크게 세 가지였습니다.

1) 최근 본 상품은 생각보다 복잡한 실시간 캐시가 아니었다

최근 본 상품 기능은 “매우 짧은 시간 안에 수천 TPS를 처리해야 하는 초고속 캐시”라기보다는, 사용자별로 최근 본 몇 개의 상품을 조회하는 기능이었습니다.

즉, Redis를 써야만 할 정도로 극단적인 성능 요구사항은 아니라고 판단했습니다.


2) 추천 시스템 가중치 활용도 PostgreSQL로도 충분하다고 보았다

초기에는 최근 본 상품과 태그 선호도를 Redis에 두고, LLM 추천 시 바로 가져와 가중치로 반영하는 방안을 생각했습니다.

하지만 다시 보니, 이 정보는 결국

형태로 PostgreSQL에 적재해도 충분히 활용 가능하다고 판단했습니다.

즉, 추천 시스템에서 “실시간 Redis 조회”가 아니어도, PostgreSQL 기반 context 조회만으로도 충분히 서비스 요구사항을 만족할 수 있다고 본 것입니다.


3) 운영 복잡도가 증가한다

Redis를 추가하면 다음을 함께 관리해야 합니다.

반면 최근 본 상품 기능은 PostgreSQL만으로도 충분히 구현 가능했고, 이미 Kafka Consumer와 RDS를 운영하고 있었기 때문에 시스템을 단순하게 가져가는 편이 더 낫다고 판단했습니다.


2. 최종 결정: Kafka Consumer가 PostgreSQL에 직접 적재

최종적으로는 Redis를 사용하지 않고, Kafka Consumer가 최근 본 상품 로그만 필터링하여 PostgreSQL에 직접 적재하는 방식으로 결정했습니다.

핵심 전략은 다음과 같습니다.

즉, 최근 본 상품 기능은 “이벤트 전체 저장”이 아니라 “최근 상태 저장” 문제로 보았습니다.


3. 최종 적재 대상 이벤트

최종적으로 PostgreSQL에 적재하는 이벤트는 click_product_detail만 선택했습니다.

로그 템플릿은 다음과 같습니다.

{
  "event_id": "uuid-1234-5678",
  "timestamp": "2026-03-02T16:30:00.000Z",
  "event": "click",
  "event_name": "click_product_detail",
  "member_id": 45,
  "event_properties": {
    "page_url": "https://api.holliverse.site/api/v1/customer/plans?category=mobile&page=0&size=50&bestCount=5>",
    "product_id": 45,
    "product_name": "5G 요금제",
    "product_type": "mobile",
    "tags": ["영상OTT", "구독결제", "인기"]
  }
}

이 이벤트에서 필요한 필드만 추출하여 PostgreSQL 테이블에 적재합니다.


4. 최종 테이블 설계: product_view_history

최종적으로 설계한 테이블은 아래와 같습니다.

CREATE TABLE product_view_history (
    member_id BIGINT NOT NULL,
    product_id BIGINT NOT NULL,
    product_name VARCHAR(100) NOT NULL,
    product_type VARCHAR(50) NOT NULL,
    tags JSONB,
    viewed_at TIMESTAMPTZ NOT NULL,
    last_event_id BIGINT NOT NULL,

    CONSTRAINT pk_member_product PRIMARY KEY (member_id, product_id)
);

CREATE INDEX idx_pvh_member_viewed
ON product_view_history (member_id, viewed_at DESC);

컬럼 의미

컬럼명 의미
member_id 로그의 member_id
product_id event_properties.product_id
product_name event_properties.product_name
product_type event_properties.product_type
tags event_properties.tags
viewed_at 로그의 timestamp
last_event_id 로그의 event_id

5. 왜 복합키와 Upsert 전략을 썼는가

가장 중요한 설계 포인트는 member_id + product_id를 **복합 기본키(Composite PK)** 로 사용한 것입니다.

이렇게 한 이유는 분명했습니다.

1) 동일 상품 중복 클릭을 원천적으로 차단

사용자가 같은 상품을 여러 번 클릭하더라도 테이블에 row가 계속 쌓이는 것이 아니라, 기존 row가 업데이트되도록 만들고 싶었습니다.

즉, “최근 본 상품 목록”에서 중요한 것은 클릭 이력 전체가 아니라 지금 시점의 최신 상태였습니다.

그래서 INSERT가 아니라 UPSERT 전략을 사용했습니다.


2) 최신 시각만 유지하면 된다

최근 본 상품은 사용자가 가장 최근에 본 순서대로 보여주면 되므로, 동일 상품을 다시 클릭하면 viewed_at만 갱신하면 충분합니다.

즉, 이 기능은 “이력 로그 테이블”이 아니라 최신 상태 테이블로 보는 것이 더 적합했습니다.


3) 조회 성능을 고려한 인덱스

member_id, viewed_at DESC 인덱스를 둔 이유는 사용자별 최근 본 상품을 최신순으로 빠르게 가져오기 위해서입니다.

예를 들어 아래와 같은 조회가 자주 발생합니다.

SELECT *
FROM product_view_history
WHERE member_id = :memberId
ORDER BY viewed_at DESC
LIMIT 3;

이 쿼리는 마이페이지 “최근 본 상품” 기능과, 추천 시스템의 “최근 본 상품 가중치” 계산에도 활용될 수 있습니다.


6. 최종 구조 정리

최종적으로 최근 본 상품 처리 구조는 아래와 같이 정리할 수 있습니다.

초기안

최종안

즉, 최근 본 상품 기능은

“Redis 기반 실시간 캐시”에서 “PostgreSQL 기반 최신 상태 저장” 구조로 단순화되었습니다.


7. 이번 의사결정의 핵심 요약

이번 설계에서의 핵심 의사결정은 다음과 같습니다.


9. 결론

최근 본 상품 기능은 처음에는 Redis 기반 실시간 캐시 구조를 검토했지만, 최종적으로는 Kafka Consumer + PostgreSQL Upsert 방식이 요구사항 대비 더 단순하고 충분히 빠르다고 판단하여 채택했습니다.

#postgres