AWS 기술 블로그

CJ 올리브영의 서버리스 랭킹 시스템 구축기

글로벌 K-뷰티 트렌드를 선도하며 성장해 온 CJ올리브영은 1400만 명 이상의 멤버십 회원을 보유하고 있는 대한민국 대표 옴니채널 플랫폼입니다. 주력 카테고리인 뷰티를 넘어, 최근에는 웰니스(Wellness) 트렌드 선도 차원에서 건강, 라이프스타일 상품을 전국 매장과 온라인몰을 통해 선보이며 고객들에게 차별화된 리테일 경험을 제공하고 있습니다.

특히 올리브영 온라인몰에는 수만여 개의 상품이 전시돼 있습니다. 이를 돋보이게 해주는 ‘랭킹 시스템’은 고객의 최종 구매 의사결정까지 결정적인 영향을 미치는 중요한 기능입니다. 기존 랭킹 시스템은 데이터베이스 자체 집계를 활용했으나, 비즈니스 요구사항을 즉각적으로 반영하기에 유연성과 확장성이 부족했습니다. 최근 올리브영에서는 서버리스 기반의 신규 랭킹 시스템 설계, 구축 등을 통해 유연성과 확장성을 개선했으며, 이후 온라인몰 내 다양한 전시 영역으로 랭킹 기능을 확장하였습니다.

이 블로그에서는 올리브영의 기존 랭킹 시스템을 서버리스 아키텍처로 전환했던 과정을 기술합니다.

랭킹 시스템 개편의 배경

기존 랭킹 시스템은 일부 카테고리 정보만 필터로 제공하고 있어, 소비자들에게 다양한 탐색 기능을 제공할 수 없었습니다. 소비자의 쇼핑 경험 개선을 목표로 다양한 필터 기능을 기획했으나, 기존의 데이터베이스 기반 집계에는 다음과 같은 문제들이 있었습니다.

  • 낮은 확장성: 랭킹 집계를 위한 연산 작업이 데이터베이스 자원에 종속되어, 다양한 집계 로직을 독립적으로 기획하는 것이 제한적이었습니다.
  • 긴 수행 시간: 정보 수집과 랭킹 집계를 모두 데이터베이스에서 처리 하면서, 적은 수의 랭킹 집계에도 10분 이상의 긴 시간이 소요되었습니다. 랭킹 필터를 늘릴 경우, 특히 세일 기간에 1시간 이상의 랭킹 집계 시간이 소요될 것으로 예상됐습니다.
  • 집계 데이터의 분석 활용 제한: 데이터베이스에는 최신의 랭킹 정보가 보관되지만, 집계된 랭킹을 시계열로 누적하여 추이를 분석하는 등 데이터의 2차 활용에 제한이 있었습니다.

올리브영은 위 문제점들을 개선하기 위해 AWS 서버리스 서비스들을 활용한 아키텍처로 현대화를 진행했습니다. 아래 그림은 아키텍처현대화 이후에 적용된 온라인몰 랭킹 필터입니다.

신규 랭킹 시스템의 아키텍처

올리브영 온라인몰의 신규 랭킹 시스템은 Amazon EventBridge에서 정의한 이벤트 규칙에 따라 일정 주기로 시작됩니다. 이후 데이터 수집부터 집계 데이터 산출, 최종 데이터 전달까지 전체 과정을 서버리스 기반으로 수행합니다.

  • 데이터 수집: 랭킹에 필요한 데이터베이스 데이터를 집계 영역으로 수집하기 위해 JDBC 연결 기반의 AWS Glue 작업을 사용합니다. 수집되는 데이터는 S3에 Parquet 파일 형식으로 저장되고, 이에 대한 스키마 정보를 Glue Catalog로 정의합니다.
  • 집계 데이터 산출: Athena에서 S3의 데이터를 조회하여 랭킹을 집계합니다. 집계 결과는 지정된 S3 경로에 다시 저장합니다.
  • 최종 데이터 전달: S3의 특정 경로에 Event Notifications을 생성하여, 집계 데이터가 산출되는 즉시 Lambda 서비스가 이를 Amazon ElastiCache 영역에 전달하여 저장합니다. 랭킹 데이터는 랭킹 조회 API를 이용해 다른 서비스에 전달됩니다.

추가 고려사항

아키텍처 설계 과정에서 Amazon Data Firehose 및 Amazon SQS 등의 스트리밍 서비스를 활용해 실시간 주문 이벤트를 수집하고, 이를 랭킹에 즉각 반영하는 것이 고려되었습니다. 하지만, 주문 후 취소/반품/교환을 반복하는 비정상적 주문 활동이 랭킹에 반영되는 것을 방지하고자, AWS Glue를 활용하는 준 배치 방식의 수집을 선택했습니다. 또한, 비즈니스 요구사항 변화로 집계 로직이 변경되는 경우, 이를 독립적으로 개발 및 배포할 수 있도록 Amazon Athena 쿼리를 집계 도구로 활용했습니다.

단계별 구현 방법

랭킹 집계 파이프라인은 Amazon EventBridge의 이벤트로 시작됩니다. 각 데이터 로드 및 집계 작업 흐름은 작업 주기에 따라 여러 AWS Step Functions 작업 흐름으로 정의됩니다. 집계가 모두 완료되면 AWS Lambda에서 완성 데이터를 메모리 계층에 전달합니다. 전체 과정이 병렬처리 가능한 방식으로 구현되어, 수백 개 랭킹 집계가 동시에 일어나도 수 분내 처리할 수 있는 유연성과 확장성을 갖출 수 있었습니다.

각 단계의 구현 방법에 대해 더 자세히 알아보겠습니다.

AWS Glue를 활용한 병렬 데이터 수집

AWS Glue는 Apache Spark 기반의 서버리스 ETL 엔진을 활용하므로, 작업 정의 단계에서 병렬 처리를 위한 고려가 필요합니다. 신규 랭킹 시스템의 Glue ETL 작업은 데이터베이스로부터 증분 주문 데이터를 읽어오는데, ‘올영세일’ 기간처럼 단기간에 주문이 집중된다면 데이터 읽기 작업에 시간 지연이 발생할 수 있습니다. 이를 짧은 시간 내에 처리하기 위해서는 병렬 처리를 활용하는 것이 중요합니다. 뿐만 아니라, AWS Glue는 서버리스 엔진으로 사용 시간만큼 과금되기 때문에, 주어진 병렬 유닛(vCPU)을 높은 효율로 활용하는 것이 비용면에서도 바람직한 방법입니다.

랭킹 시스템의 Glue ETL 작업에서는 Glue Context의 Sample Query 기능을 활용해 데이터베이스에 접근하도록 했습니다. 기본적으로, 아래 세 가지 옵션을 활용함으로써 작업 유닛을 병렬화 할 수 있습니다.

  • enablePartitioningForSampleQuery : 이 옵션을 True로 설정하여 샘플 쿼리에 대한 파티셔닝을 활성화 합니다.
  • hashfield : 해시 기반 데이터 파티셔닝에 활용할 JDBC 테이블의 컬럼입니다.
  • hashpartitions : JDBC 테이블을 읽을 때 활용할 병렬 읽기 수를 지정합니다.

아래는 이를 반영하는 샘플 코드입니다.

sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)
 
query = """SELECT ..... WHERE"""
 
# Script generated for node JDBC Connection
JDBCConnection_node = glueContext.create_dynamic_frame.from_options(
 connection_type="oracle",
 connection_options={
 "useConnectionProperties": "true",
 "connectionName": "ranking-connection",
 "dbtable": "ORDER",
 "sampleQuery": query,
 "hashfield": "{Field Name}",
 "hashpartitions": "{Number of Partitions}",
 "enablePartitioningForSampleQuery": "{True or False}"
 },
 transformation_ctx="JDBCConnection_node"
)
 
...
 
job.commit()

Glue ETL Job에 병렬 처리를 적용한 후 Glue에서 제공하는 Spark UI를 보면, 원하는 만큼의 병렬 유닛이 활용되었는지 확인 가능합니다. 특히 많은 주문이 발생하여 읽을 데이터가 많을 때에는 병렬화 수준에 따라 선형적 시간 단축을 경험할 수 있었습니다.

Amazon Athena 쿼리를 활용한 병렬 집계

Amazon Athena는 S3에 저장된 데이터에 직접 접근하고, 표준 SQL을 사용해 간편하게 분석할 수 있는 쿼리 서비스입니다. Athena는 주로 대화형 분석에 활용되지만, DBeaver와 같은 SQL 클라이언트 도구에서 Athena 작업 그룹에 직접 연결하여 개발하는 것도 가능합니다. Glue Job으로 수집된 데이터는 컬럼 기반인 Parquet 파일로 저장하도록 했기 때문에, Athena로 집계 작업을 빠르게 수행할 수 있습니다.

첫 번째 과정으로 원하는 비즈니스 로직에 맞춰 Athena 집계 쿼리를 작성한 후, 이를 저장된 쿼리(Saved Queries)로 등록합니다. 이후 Step Functions의 Parallel State를 활용해 여러 Branch로 연결합니다. 각 Branch는 개별 집계 로직을 나타내며, GetNamedQuery와 StartQueryExecution API를 순차 호출하도록 구현됩니다. 먼저 저장된 쿼리 이름을 입력으로 GetNamedQuery API를 호출하면 쿼리 ID를 얻어낼 수 있고, 이 ID를 StartQueryExecution의 입력으로 전달하도록 Step Function의 입출력을 설정할 수 있습니다.

위 그림에는 네 개의 Branch만 예시로 표현되지만, 올리브영 온라인몰에서는 랭킹 필터로 제공하는 수만큼의 Branch를 병렬로 연결합니다. Athena의 저장된 쿼리 수가 많아지면, 이를 작업 그룹 단위로 분리하여 운영 쿼리를 체계적으로 관리할 수 있습니다. 동시 실행되는 쿼리의 수가 많다면 계정 당 GetNamedQuery API 호출에 대한 계정 당 할당량을 사전에 조정하는 것도 필요합니다.

Lambda를 활용한 최종 데이터 전달

Athena의 쿼리의 집계 결과는 ‘시간대’ 및 ‘집계 로직’에 따라 지정된 Amazon S3 경로에 저장됩니다. 랭킹시스템에서는 집계의 후속작업을 수행하기 위해, S3의 저장 경로에 Event Notifications를 생성하고 이벤트 기반으로 후속 작업을 수행하는 Lambda 함수를 정의했습니다. 아래의 코드 구성으로 각 파일의 집계 결과를 ElastiCache에 저장할 수 있습니다.

public String handleRequest(S3Event s3event, Context context) {
    try {   
       // ElastiCache 연결
        String redisCusterNodes = "...cache.amazonaws.com"; 
        Set<HostAndPort> redisClusterNodes = new HashSet<>();
        redisClusterNodes.add(new HostAndPort(redisClusterNodes, 6379));
        RedisCluster redisCluster = new RedisCluster(redisClusterNodes);

       // S3 이벤트 오브젝트 확인
        S3EventNotificationRecord record = s3event.getRecords().get(0); 
        String srcBucket = record.getS3().getBucket().getName();
        String srcKey = record.getS3().getObject().getKey();
        srcKey = URLDecoder.decode(srcKey, "UTF-8");

       // S3의 집계 결과 파일(CSV) 읽기 (*getObject는 별도 함수로 정의 됨)
        S3Client s3Client = S3Client.builder().build();
        InputStream s3Object = getObject(s3Client, srcBucket, srcKey); 
        InputStreamReader isr = new InputStreamReader(s3Object);
        BufferedReader br = new BufferedReader(isr);

        // 이 파일에 저장된 랭킹 결과를 Redis에 적재
        String nextLine;
        while ((nextLine = br.readLine())!=null) {
            ...  // 파일 Parsing
            redisCluster.set(key,value);
        }
        return "ok";
    } catch (Exception e) {
        logger.info(e.getMessage());
        throw new RuntimeException(e);
    }
   ...
}

랭킹 시스템의 운영 및 모니터링

신규 랭킹 시스템은 파이프라인의 각 단계가 여러 서비스에 분리되어 있기 때문에, 성능이나 기능 문제가 발생했을 때 개별 서비스 로그에서 원인을 찾아낼 수 있습니다. 예를 들어, ‘올영세일’ 기간동안 랭킹 집계가 지연된 적이 있었는데, 이 때에도 Glue 서비스의 수행 시간이 길어진 것을 확인하고 앞서 소개한 방식으로 병렬화 수준을 조절해 해결했습니다.

신규 랭킹 시스템에는 운영 중 발생할 수 있는 다양한 이상상태를 실시간으로 확인하고 즉각적으로 대응하기 위한 알림 기능을 구현했습니다. 이를 위해 Amazon SNS에 메시지 게시를 위한 주제(Topic)를생성하고, 이에 대한 구독 엔드포인트로 Slack Webhook을 설정합니다.

아래 그림과 같이, CloudWatch에서 각 서비스의 핵심 지표를 대상으로 경보(Alarm) 상태를 지정하여, 경보가 트리거 되었을 때 알림을 보낼 대상으로 앞서 구성한 SNS Topic을 선택합니다.

Glue, Athena, Step Functions 등 여러 서비스에서 발생하는 이벤트를 감지하고 알림을 전달하려면, 아래와 같이 EventBridge에 이벤트 패턴으로 정의할 수 있습니다.

이 과정을 마친 후 Lambda 함수가 에러를 리턴하도록 하면, 아래와 같은 Slack 알림이 전달되는 것을 확인할 수 있었습니다.

신규 랭킹 시스템의 적용 효과

  • 비즈니스 성과 : 랭킹 시스템이 개편된 후, 고객에게 더욱 다양한 랭킹 집계 필터를 제공할 수 있게 되었습니다. 랭킹 페이지내 사용자 체류시간 및 구매 전환 비율이 증가했으며, 유의미한 비율의 사용자가 새로 추가된 집계 필터를 활용해 상품을 조회하고 있습니다.
  • 시스템의 확장성 : 랭킹 집계 시 자원에 대한 확장성 제한이 사라지면서 여러 서비스 영역으로 랭킹 기능을 확장했습니다. 신규 랭킹 시스템에 집계 로직만 추가함으로써 아래 그림의 1/ 헬스 카테고리, 2/ 브랜드 랭킹, 3/ 선물하기(예정) 등으로 단기간 내 확장할 수 있게 됐습니다. 더욱 다양한 필터의 랭킹 집계를 동시 수행하면서도 실질적 집계 시간이 줄어든 것 역시 중요한 성과였습니다.

  • 데이터 기반 지속적 서비스 개선 : 신규 랭킹 시스템에서는 시간대별 랭킹 집계 결과를 S3에 파일로 적재하여, 이를 사후분석에 활용하기에 용이합니다. 분석 결과를 바탕으로 랭킹 주기를 조절하거나, 비정상 거래활동이 랭킹에 반영되지 않도록 판별하는 기준도 고민하게 됐습니다.
  • 합리적 비용 : 랭킹 집계를 위해 활용되는 모든 서비스가 이벤트 기반 호출 및 서버리스로 동작하며, 사용한 시간 만큼만 과금됩니다. 올리브영에서는 다양한 랭킹 집계 워크로드를 짧은 주기마다 수행하면서도, 전체 운영 파이프라인에 월 50달러 미만의 매우 낮은 비용만 지불하게 되었습니다.

결론

올리브영에서 신규 구축한 랭킹 시스템은 데이터베이스 기반 집계 방식에서 탈피하여, 완전한 AWS 서버리스 기반 파이프라인으로 구현되었습니다. 그리고 이를 통해 확장성, 비용, 성능측면 등의 성과를 확인할 수 있었습니다. 올리브영에서는 향후에도 안정적인 랭킹 서비스를 사용자에 제공하기 위해, 지속적으로 서비스 개선방법을 모색하고 확장 및 발전시켜 나갈 계획입니다.

    Junho Eho

Junho Eho

어준호 백엔드 엔지니어는 CJ올리브영 버티컬서비스개발팀 소속으로 설계 및 개발을 포함한 팀 리딩 업무를 담당하며, 사용자 경험을 개선하기 위해 온라인몰 성능 최적화에 힘쓰고 있습니다.

    Changsoo Kwon

Changsoo Kwon

권창수 백엔드 엔지니어는 CJ올리브영 커머스서비스개발팀 소속으로 온라인몰 사용자들에게 더 나은 쇼핑 경험을 제공하기 위해 성능 최적화와 시스템 설계에 주력하고 있습니다.

    Seol Choi

Seol Choi

최설 백엔드 엔지니어는 CJ올리브영 커머스서비스개발팀 소속으로 온라인몰 사용자들에게 더 나은 쇼핑 경험을 제공하기 위한 전시 서비스를 개발하고 있습니다.

Kihyeon Myung

Kihyeon Myung

명기현 솔루션즈 아키텍트는 다양한 분야의 엔지니어 경험을 바탕으로, 고객이 AWS 클라우드에서 효율적 아키텍처를 구성하고, 원하는 비즈니스 목표를 달성할 수 있도록 지원하고 있습니다.