BE-commerce 프로젝트
약 5분 분량 검색·추천

홈을 여러 쪽으로 나눠 만들었다: 커서가 보여 준 것을 들고 다니고, 다음 쪽 순서는 방금 본 상품이 정한다

목차

홈을 몇 행씩 나눠 만들고 다음 쪽이 앞 쪽에서 보여 준 것과 그 사이의 세션 활동을 입력으로 받게 했습니다. 사용자 30명 실측에서 쪽 사이 중복은 0개였고(커서가 없었다면 2쪽에 평균 8.6개가 다시 나왔습니다), 1쪽을 본 뒤 본 상품의 대분류가 2쪽 첫 행이 된 비율은 96.7~100%였습니다. 1차 측정은 60%였고, 놓친 원인 가운데 하나는 설계 결함이었습니다. 인기 표에 아동복이 하나도 없어서 아동복을 본 사용자의 세션 신호가 오류 없이 버려지고 있었습니다.

결론부터

  • 앞 페이지에서 보여 준 상품 ID를 서명하지 않은 커서에 담아도 페이지 간 중복은 0개가 됐습니다.
  • 앞 페이지 이후의 세션 활동을 다시 읽자 다음 행 반영률은 96.7~100%까지 올라갔습니다.
  • 첫 측정의 60%는 모델 문제가 아니라 인기 표에 해당 대분류가 없어 신호가 조용히 버려진 데이터·폴백 문제였습니다.

다음 페이지에 무엇을 넘길 것인가

Netflix GenPage는 홈을 한 번에 다 만들지 않습니다. 몇 행씩 만들고 다음 요청 때 이미 만든 행과 사용자의 최근 행동을 프롬프트에 붙여 다음 행을 만듭니다. 이 저장소의 홈은 세 행(최근 본 상품, 추천, 인기)을 만들고 끝났습니다. 더 내려도 볼 것이 없었고 보는 동안 한 행동이 화면에 반영되지 않았습니다.

이 글은 그 구조를 홈에 넣으면서 정한 것 하나(쪽 상태를 어디에 둘지)와 실측에서 드러난 결함 하나를 다룹니다. 다음 쪽의 행은 아직 모델이 아니라 규칙(대분류별 인기)으로 만듭니다.

다음 쪽은 앞 쪽과 그 사이의 세션을 입력으로 받는다. 1쪽 커서에 보여 준 상품 id를 싣고, 그 사이 사용자가 아동복을 보면 2쪽 첫 행이 아동복이 된다. 세션 반영 96.7~100%, 쪽 사이 중복 0

무엇을 만들었나

  • 홈 응답에 page와 nextCursor가 붙습니다. 다음 쪽은 ?cursor=로 받습니다. 필드를 더한 것이라 기존 화면은 그대로 동작합니다.
  • 2쪽부터는 대분류별 인기 행입니다. 순서는 그 요청 시점에 다시 읽은 최근 활동이 먼저이고 나머지는 인기 표에 처음 나오는 순서입니다.
  • 세션이 순서를 정한 행은 CATEGORY_POPULAR_SESSION으로 표시합니다. 응답만 보고 세션이 작동했는지 알 수 있고 측정도 이 값으로 셉니다.
  • 한 대분류는 한 번만 행이 되고 시도할 대분류가 남지 않으면 nextCursor가 비어 끝납니다.

쪽 상태를 서버에 둘까, 커서에 실을까

다음 쪽을 만들려면 “앞 쪽에서 무엇을 보여 줬는가”를 알아야 합니다. 둘 중 하나를 골라야 했습니다.

안얻는 것잃는 것
서버(Redis)에 쪽 상태 저장커서가 짧다쪽마다 Redis에 쓴다. TTL이 지나면 중복이 돌아온다. Redis 장애가 홈 아래쪽을 건드린다. 모든 인스턴스가 같은 상태를 봐야 한다
커서에 보여 준 상품을 싣는다서버 상태가 없다. 어느 인스턴스로 가도 같은 답이 나온다커서가 쪽마다 커진다. 클라이언트가 커서를 고칠 수 있다

커서에 싣는 쪽을 골랐습니다. 커서 길이를 재 보니 2쪽 커서가 120자, 3쪽 커서가 395자였고 상품 하나에 약 10자가 듭니다. 대분류가 5개라 3쪽에서 끝나고 길이 상한 4,096자는 약 10쪽 분량입니다. 클라이언트가 커서를 고쳐서 얻을 수 있는 것은 자기 화면에 중복이 생기는 것뿐이라 서명은 하지 않았습니다. 모양이 틀리거나 상한을 넘는 커서는 400으로 거절합니다.

측정: 기준을 먼저 적었다

이슈에 측정 전에 적은 기준은 두 가지였습니다. 커서가 있으면 쪽 사이 중복이 0일 것, 1쪽을 본 뒤 특정 대분류의 상품을 보면 2쪽 첫 행이 그 대분류인 비율이 90% 이상일 것.

평가 스크립트는 사용자마다 이렇게 돌았습니다. 가입하고 1쪽을 받은 뒤, 세션이 없을 때 2쪽 첫 행이 될 대분류를 먼저 확인하고 그것이 아닌 대분류의 상품 하나를 봅니다. 그다음 같은 커서로 2쪽을 받아 첫 행이 방금 본 대분류인지 셉니다. 중복의 대조군으로는 같은 2쪽을 “보여 준 상품을 비운 커서”로 한 번 더 받았습니다.

1차는 60%였고 원인이 셋이었다

조건세션 반영커서 있을 때 중복커서 없을 때 중복
동기 반영(IN_REQUEST)60%09.3
Kafka · 대기 0ms0%09.6
Kafka · 대기 50ms0%09.7

중복 기준은 충족했지만 세션 반영이 크게 모자랐습니다. 놓친 경우를 대상 대분류별로 나눠 보니 원인이 셋이었습니다.

  • Kafka 0%는 실험 설정의 실수였습니다. Kafka 전달은 kafka 프로파일이 있어야 발행과 소비가 도는데 프로파일 없이 띄웠습니다. 게다가 실제로 나가는 기본값은 Kafka가 아니라 앱 안 비동기 전달(IN_PROCESS)입니다. 조건을 기본값으로 바꿨습니다.
  • 동기 반영의 놓친 12건 가운데 6건은 평가 스크립트의 결함이었습니다. 대분류가 NULL인 상품 묶음까지 대상으로 골랐습니다.
  • 나머지 6건(아동복)은 설계 결함이었습니다. 다음 쪽 행은 인기 표에서 대분류별로 상품을 골라 만드는데 인기 표에 여성복 266, Divided 106, 스포츠 16, 남성복 12가 있고 아동복은 0개였습니다. 사용자가 아동복을 봐도 그 행을 만들 재료가 없어 행이 서지 않았고 오류도 없어서 세션 신호가 조용히 버려졌습니다.

인기 표에 모자란 대분류는 그 대분류의 신상품으로 채우게 했습니다. 채운 항목은 이유를 “대분류 신상품”으로 적어 인기와 구분되게 했습니다.

2차 결과

조건세션 반영커서 있을 때 중복커서 없을 때 중복다음 쪽 p95
앱 안 비동기(기본값) · 대기 0ms96.7%08.681ms
앱 안 비동기 · 대기 50ms100%08.786ms
동기 반영100%08.685ms

두 기준을 모두 충족했습니다. 대기 0ms에서 놓친 1건은 비동기 반영이 다음 요청보다 늦은 경우이고 50ms를 기다리면 사라집니다. 앞선 신선도 실험에서 반영 대기의 무릎이 50ms였던 것과 같은 결과입니다.

대가도 있습니다. 신상품으로 채우는 조회가 붙어 다음 쪽 p95가 54ms에서 81~86ms로 늘었습니다.

이 측정이 말하지 않는 것

  • 대분류가 5개라 쪽이 3쪽에서 끝납니다. 중분류 행을 넣으면 쪽이 늘고 커서도 길어집니다.
  • 합성 계정이고 활동을 하나만 심었습니다. 실제 세션은 여러 대분류를 섞어 봅니다.
  • 쪽을 넘기는 사이 카탈로그나 인기 표가 바뀌는 경우는 재지 않았습니다.
  • 다음 쪽 행의 순서는 규칙입니다. 모델이 행을 생성하게 붙인 결과는 작은 GenPage를 만들어 서빙까지 붙였다에 있습니다.

참고

프로필 사진
작성자 @범수

오늘의 노력이 내일의 전문성을 만든다고 믿습니다.

댓글

댓글 수정/삭제는 GitHub Discussions에서 가능합니다.