Spring Batch로 구현한 랭킹 시스템, 메모리 지키기
·
카테고리 없음
들어가며매일 쌓이는 대량의 메트릭 데이터를 집계해서 주간/월간 랭킹을 실시간으로 산출하는 과정을 쉽지 않다. 단순히 "데이터를 읽어서 계산한다"는 직관적인 접근으로 시작한다면, 대량의 데이터를 안정적으로 처리할 수 없을 것이다. 이 글에서는 Spring Batch를 사용한 랭킹 배치 시스템 구현 과정과, 메모리를 지키기 위한 리팩토링 경험을 공유하고자 한다. 요구사항과 초기 설계사용자들에게 인기 상품을 추천하기 위해 주간/월간 랭킹 기능을 제공한다. 요구사항은 다음과 같다. 주간 랭킹 : 매주 월요일부터 일요일까지의 기간 동안 Top 100 상ㅍ품월간 랭킹 : 매월 1일부터 말일까지의 기간 동안 Top 100 상품배치 실행 : 매일 새벽 3시에 전날 데이터를 기반으로 주간/월간 랭킹 계산 데이터 구조..
랭킹 시스템에서 고려해야 할 점은?
·
카테고리 없음
들어가며이커머스 서비스에서 상품 랭킹은 사용자에게 인기 상품을 추천하고, 판매까지 이어질 수 있게 만드는 중요한 기능이다. 하지만 실시간으로 쏟아지는 많은 요청을 매번 RDB에서 대량 집계해서 보여주는 것은 성능상 한계가 있을 수 있다. 이번 글에서는 Redis ZSET을 활용해 일별 랭킹 시스템을 구현한 경험을 공유하고자 한다. 특히 키 설계 전략과 콜드 스타트 문제 해결을 중점으로 글을 쓰려고 한다. 어떤 랭킹 시스템인가구현하고자 하는 랭킹 시스템은 단순히 누적 판매량순 또는 누적 조회수순으로 정렬하는 것이 아닌, 다음과 같은 요구사항을 가진 랭킹 시스템이다. 일별 랭킹 가장 먼저 고려한 것은 "시간의 범위"이다. 전체 기간의 누적 점수만 사용하면 이미 인기가 많은 상품이 계속 상단을 차지하는..
Kafka 컨슈머 멱등성 처리로 중복 없애기
·
카테고리 없음
들어가며카프카를 도입해서 서비스 간 결합도를 낮추는 작업을 진행했다. 전송 방식은 데이터 유실 없이 안정적인 전송을 위해 At-Least-Once(적어도 한 번 전송) 방식으로 구현했다. 그래서 중복 전송에 대한 대비가 필요했다. 프로듀서가 메시지를 보내고 브로커로부터 ACK을 받지 못하면, 프로듀서는 메시지가 유실됐다고 판단하고 재전송을 한다. 이때 실제로는 브로커에 저장이 되었지만 ACK만 유실된 경우라면? 컨슈머는 똑같은 메시지를 두 번 받게 된다. 그래서 컨슈머 쪽에 멱등성 처리를 해주어야 한다. 즉, 카프카가 "정확히 한 번 전송"을 보장해주기 어렵다면, 컨슈머가 "여러 번 받아도 한 번만 처리"하도록 만들면 된다. 결과적으로 시스템 전체는 Exactly-Once 효과를 낼 수 있다. 이..
리스너는 이벤트가 발행된 걸 어떻게 알았을까? - Application Event 내부 동작
·
카테고리 없음
TL;DR- ApplicationEvent는 이벤트 발행 시 타입에 맞는 리스너를 자동으로 실행하여, 트랜잭션 시점에 맞춘 후속 처리와 메인 로직을 분리한다. 들어가며서비스 간 강함 결합을 끊기 위해 이벤트 방식을 사용하곤 한다. 기능을 구현하면서 리스너는 이벤트가 발행됐다는 사실을 어떻게 아는 걸까? 라는 궁금증이 생겼다. '좋아요' 기능을 통해 이벤트 발행과 수신 사이에 내부 동작이 어떻게 되는지 공부해봤고, 이 내용을 공유하고자 한다. 좋아요 기능 설명 사용자가 상품 좋아요를 누르면 사용자와 상품 간의 좋아요 관계를 저장할 뿐만 아니라 상품의 좋아요 수를 증가시켜야 한다.즉, 상품 좋아요 등록 후 후속 처리로 상품 좋아요 수 집계 처리를 해줘야 한다. 이런 후속 작업을 메인 로직에 포함시키면..
PG사가 응답하지 않을 때: 테스트 코드로 보는 Circuit Breaker
·
카테고리 없음
TL;DR- 외부 시스템 장애로 인한 연쇄 장애를 막기 위해 Resilience4j를 활용한 Circuit Breaker 전략 도입 들어가며PG와 같은 외부 시스템과의 연동은 불안정성을 가지고 있다. 네트워크 지연, 알 수 없는 타임아웃 등 다양한 장애 상황이 발생할 수 있다.이때 우리의 서버가 무작정 응답을 기다리거나 계속해서 재요청을 보낸다면, 결국 전체 시스템의 장애로 이어질 수 있다. 이번 글에서는 Resilience4j를 사용해서 PG 결제 연동 시 발생할 수 있는 장애를 처리하는 방법과, 테스트 코드를 통해 Circuit Breaker가 어떻게 동작하는지에 대한 내용을 공유하고자 한다. Resilience4j 설정Circuit Breaker가 언제 열리고(Open), 언제 다시 닫힐지(Cl..
10만 건의 상품 데이터, 조회 성능 높이기 - 인덱스, 캐시(Redis) 도입
·
카테고리 없음
TL;DR- 10만 건의 상품 데이터를 효율적으로 처리하기 위해 DB 인덱스를 최적화하고, Redis 캐시를 도입했다. 들어가며'상품 목록 조회'와 '상품 상세 조회'는 이커머스 서비스에서 가장 빈번하게 호출되면서도 성능에 민감한 기능이다. 사용자가 원하는 상품을 찾기 위해 브랜드별로 필터링하고, 가격순/최신순/좋아요순으로 정렬한다. 데이터가 적을 떄는 문제가 없지만, 데이터가 10만 건, 100만 건으로 늘어난다면 조회 성능에 문제가 될 것이다. 이번 글에서는 10만 건의 상품 데이터 환경에서 DB 인덱스 튜닝과 Redis 캐싱 전략을 전용해서, 조회 성능을 개선하는 과정과 시행착오를 공유하고자 한다. 성능 개선 대상 API 및 테스트 데이터 설명 1. 상품 목록 조회 (/products)필터링..
주문 시스템의 동시성 이슈 고민하기 : 비관적 락 vs 낙관적 락
·
카테고리 없음
TL;DR- 조회가 많고 트랜잭션이 긴 이커머스 주문 시스템에 낙관적 락을 도입했다. - 재시도 로직과 비즈니스 트랜잭션을 분리해 충돌 시 안전하게 처리한다. 들어가며주문 생성 로직은 이커머스 서비스의 핵심이자 가장 민감한 구간이다. 재고, 포인트, 쿠폰 등 여러 자원이 동시에 변경되어야 하기 때문에 동시성 제어가 필수이다. 사용자가 몰리는 상황에서 재고가 1개 남은 상품을 2명이 동시에 주문한다면? 내가 보유한 포인트를 동시에 두 건의 주문에서 사용하려고 한다면? 이번 포스팅에서는 주문 시스템에서 데이터의 정합성을 지키기 위해 고민했던 과정과, 낙관적 락을 통해 이를 해결한 경험을 공유한다. 주문 프로세스와 동시성 위험 구간주문 생성 프로세스는 아래와 같다. 1. 주문 요청 2. 비즈니스 검증 ..
좋아요 등록/취소 기능 멱등성 처리하기
·
카테고리 없음
TL;DR좋아요 API의 멱등성을 Application -> Domain -> DB 3단계에서 보장했다. 좋아요 기능을 락을 쓰지 않고 멱등성을 처리했다. 들어가며좋아요 등록/취소 기능은 단순해보이지만 반드시 멱등성을 고려해야 한다. 이번 포스팅에서는 좋아요 등록/취소 API에 멱등성을 어떻게 적용했는지, 어떤 문제를 해결할 수 있었는지, 그리고 실제 코드 구조는 어떻게 되어 있는지에 대해 글을 쓰고자 한다. 멱등성이란동일한 요청을 여러 번 보내도 결과가 한 번 보낸 것과 동일하게 유지되어야 한다. 왜 좋아요 API에 멱등성이 필요할까?좋아요 기능은 다음과 같이 동작하고, 두 개의 테이블을 사용한다. 좋아요 등록likes 테이블에 (user_id, product_id) 레코드 INSERTprod..
도메인과 값 객체, 어떻게 나눌까?
·
카테고리 없음
들어가며도메인 모델을 설계할 때 고민이 되는 것 중 하나는 "어떤 것을 도메인으로 둘 것인가, 어떤 것을 VO로 둘 것인가"이다. 단순히 DB 관점에서 ID 존재 여부로 나누는 것이 맞을까? 이커머스 서비스를 설계하면서 이런 고민을 깊이 있게 해봤다. 특히 주문과 배송 사이에서 어떻게 나눌지에 대한 고민을 해봤다.그래서 이 사례를 예시로 들어 도메인과 값 객체의 분리 기준과 설계 고민에 대해 글을 쓰고자 한다. 요구사항 사용자는 상품을 주문할 때 배송 정보를 함께 입력해야 한다. - 수령인 이름, 수령인 전화번호- 기본 주소, 상세 주소 도메인과 VO로 나누기 배송 정보를 VO로 추출처음에는 주문 객체에 배송 정보(수령인 이름, 수령인 전화번호, 주소)를 몰아넣는 설계를 생각했다. class Order..
통합 테스트는 어디까지 테스트해야 할까
·
카테고리 없음
⁉️ Facade vs Service 테스트 범위에 대한 고민 포인트 조회 기능을 구현하면서 다음과 같은 통합 테스트 요구사항을 받았다.해당 ID의 회원이 존재하지 않을 경우, null이 반환된다. 초기 코드 구조포인트 조회 기능은 PointFacade가 담당했고, 그 내부는 다음과 같은 흐름을 갖고 있다. public class PointFacade { private final PointService pointService; private final UserService userService; public PointInfo getPointInfo(String loginId) { User user = userService.getUserByLoginId(loginId) ..