🍊

디스플레이 광고 시스템 분석하기 3편

디스플레이 광고 시스템 분석하기 2편에서 겪었던 문제들을 해결하기 위한 개편 작업을 진행했다. 초기 시스템을 만들 때와는 다르게 깊은 이해도를 가지고 더 안정적인 시스템을 구축하려고 했다. 이번에는 카프카(Kafka)를 통한 이벤트 스트리밍과 스파크(Spark)를 이용한 대용량 분산 처리를 활용항 시스템 개편 작업에 대해 알아보자.

앞서 겪었던 문제 3가지는 다음과 같이 요약할 수 있다.

  • 필터링 작업 전의 셀렉팅 작업으로 인한 정상적인 카탈로그 광고 불가능
  • 피드 확장 및 변경에 대해 요구와 시스템 코드 사이의 강한 결합으로 인한 부정적 영향
  • 데이터 볼륨 확장에 따른 시스템 성능 저하

먼저 개편 작업 이후의 시스템에 대해 알아보고 각 문제에 대해서 어떻게 해결했는지 살펴보자.

카프카와 스파크를 활용한 시스템 개편

file

개편한 시스템의 구성은 위와 같다. 각각의 애플리케이션과 저장소에 대해 설명을 간단히 해보자.

  1. Redis: 실시간으로 업데이트 된 카탈로그의 데이터의 프라덕트 아이디를 저장하고 있다. 프라덕트 아이디를 중복으로 저장하지 않기 위해 set을 사용한다. 
  2. ES(Elasticsearch): 카탈로그 데이터를 저장하고 있다. 프라덕트, 아이템, 벤더 아이템, 할인 데이터를 가지고 있다.
  3. MySQL: 카탈로그 데이터 중 실제로 마케팅에 활용할 마케팅 카탈로그 데이터를 저장하고 있다. 위의 나누어져 있던 프라덕트, 아이템, 벤더 아이템, 그리고 할인 데이터를 하나로 합쳐 저장한다.
  4. S3: 마케팅 카탈로그 데이터에 대해 필터링 그리고 셀렉팅을 거친 후의 데이터를 저장한다.

애플리케이션은 다음과 같다.

  1. 맨 왼쪽에 있는 애플리케이션은 컴포징을 담당한다. 카탈로그 데이터를 읽어 마케팅 카탈로그 데이터를 저장한다. 데이터를 저장한 이후, 저장한 데이터의 프라덕트 아이디를 카프카를 통해 발행한다. 이 애플리케이션은 스프링 부트로 구성했고 이름은 컴포저(composer)다.
  2. 중간에 위치한 애플리케이션은 필터링과 셀렉팅을 담당하고 있다. 카프카를 통해 프라덕트 아이디를 소비하여 마케팅 카탈로그 데이터를 읽는다. 이후의 필터링과 셀렉팅 처리를 한 후에 데이터를 S3에 파일로 저장한다. 이 애플리케이션도 스프링 부트로 구성했고 이름은 제너레이터(generator)다.
  3. 마지막 맨 오른쪽 아래에 있는 애플리케이션은 스파크 애플리케이션이다. S3에 저장된 파일들을 읽어 실제 피드 파일을 생성한다. 컨버팅을 담당하고 있으며 이름은 피드 라이터(feed writer)다.

디스플레이 광고 시스템 분석하기 1편에서 다뤘던 4단계를 위의 시스템에서 볼 수 있다.

컴포저(Composer)

앞서 설명했듯 컴포저는 컴포징을 담당한다. 이 애플리케이션은 30분 주기로 레디스로부터 업데이트된 카랄로그의 프라덕트 아이디 데이터를 가져온다. 30분 주기로 데이터를 가지고 오는 이유는 시스템 부하를 줄이기 위해서이다. 실시간으로 업데이트 되는 카탈로그 데이터를 실시간으로 처리하기보단 버퍼(buffer)를 두어 처리하고 있다. 실제 업데이트 된 카탈로그를 외부 업체로 제공하기 위해서는 피드 파일을 통해 이루어진다. 이 피드 파일을 생성하는 시점이 시스템의 맨 마지막인 피드 라이터에서 이뤄지는데, 이 피드 라이터는 배치 애플리케이션이다. 컴포저에서 데이터를 실시간으로 처리하여도 마지막에는 배치로 처리하기 때문에 실시간으로 처리하는 의미가 크지 않다. 

주기를 30분으로 정한 이유는 업데이트 피드 파일 제공하는 주기가 최소 1시간 단위다. 이 보다 적은 값이 필요했고중복 처리의 효과를 보기 위해서는 너무 짧은 주기는 선택할 수 없었다. 그래서 1시간의 반인 30분으로 선택하여 진행했었다. 

업데이트 된 카탈로그 데이터는 이 컴포저가 다 처리한다. 그러나 일부 데이터를 강제로 재처리해야 했다. 카탈로그와 마케팅 카탈로그 사이의 데이터 불일치 또는 마케팅 카탈로그의 새로운 필드를 할 때, 컴포징을 다시 진행했어야 했다. 별도의 배치 애플리케이션으로 프라덕트 아이디를 레디스로 넣는 작업을 진행하여 마케팅 카탈로그를 업데이트 할 수 있도록 구성했다.

제너레이터(Generator)

제너레이터는 앞서 컴포징 이후의 데이터를 처리한다. 카프카로부터 업데이트 된 프라덕트 아이디를 소비하여 마케팅 카랄로그 데이터를 읽는다. 카프카로부터 프라덕트 아이디가 아닌 마케팅 카탈로그 데이터를 소비하면 MySQL 읽기 작업을 줄일 수 있겠지만, 마케팅 카탈로그 하나의 데이터 크기가 너무 큰 경우가 있어 하나의 카프카 이벤트로 다 담지 못하는 상황이 발생했다. 

제너레이터는 필터링과 셀렉팅을 담당하고 있다. 실시간으로 데이터를 처리하여 S3에 저장한다. 여기의 핵심은 외부 업체마다 서로 다른 파일을 저장한다는 점이다. 왜냐하면 외부 업체마다 적용할 필터링과 셀렉팅이 다르기 때문이다. 하나의 공용의 데이터를 남기지 않고 서로 다른 데이터를 남겨 외부 업체마다의 종속성도 끊었다. 어떤 외부 업체의 필터링, 셀렉팅이 올바르지 않게 이뤄져도 다른 외부 업체에 대한 데이터는 정상으로 유지할 수 있다.

필터링과 셀렉팅을 적용하기 위해서는 추가 데이터가 필요하다. 데이터베이스에 저장한 피드에 대한 셀렉팅과 필터링에 대한 설정 값을 이용한다. 당연히 필터링과 셀렉팅 설정은 별도의 백오피스 애플리케이션을 두어 비즈니스 팀에서 직접적으로 관리할 수 있도록 구성했다.

피드 라이터(Feed Writer)

피드 라이터는 S3에 남겨진 마케팅 카탈로그 데이터를 읽어 컨버팅을 처리하고 피드 파일을 생성한다. 실시간으로 남겨진 마케팅 카탈로그는 수 천개의 파일로 이뤄져있다. 이 피드 라이터는 스파크 애플리케이션으로 수 천개의 파일을 읽어 하나의 피드 파일로 저장한다. 피드 파일이 클 경우 많게는 10개까지의 피드 파일을 만들기 하지만 수 천개의 파일에 있는 데이터를 합치는 작업도 같이 진행한다. 

피드 라이터는 단순히 S3에 남겨진 피드 파일을 활용하기 때문에 앞의 어떠한 시스템과도 관계가 없다. 물론 데이터의 스키마는 어느정도 일치해야하지만 앞의 시스템이 일시적으로 다운되었다고 피드 라이터까지 동작하지 않지는 않는다. 

문제 해결 과정

필터링 작업 전의 셀렉팅 작업으로 인한 정상적인 카탈로그 광고 불가능

개편한 시스템에서는 필터링 작업을 셀렉팅 작업 전에 하도록 변경했다. 이 과정에서 데이터 구조에 대한 많은 변경이 이뤄졌다. 이전 데이터 구조와 새로운 데이터 구조를 병행하는 필터링 로직을 구현해야하기도 했다. 

피드 확장 및 변경에 대해 요구와 시스템 코드 사이의 강한 결합으로 인한 부정적 영향

피드에 대한 설정을 데이터베이스에 저장하고 이를 관리할 수 있도록 백오피스를 도입했다. 시스템은 피드에 대한 설정을 읽어 이를 활용하여 피드 파일을 생성한다. 결과적으로 시스템과 피드 설정의 강한 결합은 제거했다. 피드 확장도 추가적인 배포 없이 진행할 수 있도록 했다. 백오피스에 새로운 피드 설정을 생성하면 이를 기반으로 피드 생성을 할 수 있도록 했다.

데이터 볼륨 확장에 따른 시스템 성능 저하

이전에 카탈로그 데이터가 증가하면 할수록 배치 처리 소요 시간이 증가했다. 병목이 컸던 부분이 마케팅 카탈로그에 대한 데이터베이스 읽기 작업을 제거했다. 마케팅 카탈로그에 대한 처리를 준-실시간으로 처리하여 S3에 파일로 남겼다. 앞서 설명했듯 외부 업체마다 파일을 독립적으로 남겼기 때문에 이후 작업에서 해당 파일만 읽어서 처리하면 된다. 

하나의 데이터를 다수의 배치에서 계속해서 읽었지만, 별도의 독립된 데이터를 읽을 수 있도록 변경했다.

문제점

제너레이터(Generator)

이 시스템에서 가장 큰 문제점은 제너레이터에 있었다. 바로 필터링과 셀렉팅을 위한 재처리 과정이 쉽지 않은 점이다. 애플리케이션이 구동될 때 필터링과 셀렉팅의 설정을 데이터베이스로부터 읽었다고 생각하자. 그리고 데이터의 최신성을 반영하기 위해 일정한 주기로 데이터를 갱신한다고 생각하자. 최신성을 반영하기 위함은 백오피스를 통해 비즈니스 팀에서 설정 값을 변경할 수 있기 때문이다.

최신 설정을 가지고 준-실시간으로 마케팅 카탈로그 데이터를 처리하고 파일로 저장한다. 그렇다면 이미 이전 설정을 가지고 처리된 데이터는 어떻게 할까. 만약 이전에 처리된 마케팅 카탈로그가 미래에 업데이트 되어 다시 재처리된다면 문제가 없다. 그러나 미래에 업데이트 되지 않으면 이전 설정으로 처리된 결과가 그대로 남아 있을 것이다. 이를 해결하기 위해서는 시스템은 전체 데이터를 재처리해야 한다. 

새로운 업체가 등장하여 피드를 하나 더 추가한다고 생각하자. 업체에 대한 피드 설정이 이뤄지면 애플리케이션은 이 업체 설정에 대한 데이터를 S3에 남길 것이다. 그렇다면 업데이트 되지 않은 데이터는 어떻게 할 것인가. 위의 필터링과 셀렉팅에서 가진 문제점과 같이 새로운 업체가 등장하면 시스템은 전체 데이터를 재처리해야 한다. 

전체 재처리 과정은 별도의 배치를 구성하여 카프카에 데이터를 발행하도록 했다. 그러나 전체 데이터를 재처리하는 것은 시스템 자원을 많이 사용했다. 재처리 데이터와 실시간으로 처리해야 할 데이터가 많을 수록 데이터 처리 성능이 느려지거나 시스템 자원을 많이 사용했다. 이 뿐만이 아니다. 재처리를 위해 전체 데이터를 읽어야하는데 MySQL에 있는 마케팅 카탈로그 테이블에 대한 전체 스캔(full scan)을 진행하여 많은 시스템 부하를 주었다.

번외

확실히 처음의 광고 시스템보다 많은 개선은 이뤄졌다. 업데이트 되는 데이터가 많아지면 스케일 아웃(scale out)으로 처리하여 어느 정도의 데이터 증가까지는 다룰 수 있었다. 더욱이 준-실시간으로 데이터를 처리하면서 기존의 배치로만 구성해서 발생했던 데이터 지연은 많이 줄일 수 있었다. 

데이터 파이프라인으로 많은 개선은 이뤘음에도 불구하고 이벤트 처리과 배치 처리가 공존하는 상황에서의 시스템 구성은 쉽지 않다. 배치 처리를 통해 다시 이벤트 처리 프로세스를 태워야만 했기 때문에 더 힘들었을지도 모른다. 만약 이벤트 처리 시스템은 그대로 두고 배치 처리르는 따로 진행한 다음에 데이터를 합치는 과정을 진행했다면 어땠을까. 이벤트 처리를 하는 시스템의 자원도 유지하면서 배치 처리를 병행할 수 있지 않았을까.

카프카 이벤트의 형식을 json에서 protobuf 형식으로 변경하면 어땠을까 싶다. 사용했던 json의 데이터 크기는 불필요하게 컸다. 형식을 protobuf를 변경하여 이벤트 크기를 획기적으로 줄였다면 데이터베이스의 읽기 작업을 줄일 수 있지 않았을까.