피처링 리포트의 수집부터 분석, 리포트로 나타나기까지 백엔드부터 프론트엔드의 기술을 모두 살펴봅시다.
안녕하세요, 피처링에서 리포트 개발의 모든 부분을 담당하고 있는 백엔드 개발자 해리입니다. 많은 사용자분들이 피처링 리포트를 통해 영향력 있는 인플루언서를 찾고 마케팅 효율을 높이고 계십니다. 이번 글에서는 피처링 서비스의 아키텍처가 어떤 식으로 구성되어 있고 각 아키텍처가 어떤 역할을 하고, 어떤 기술을 사용하는지 간단하게 소개해 보겠습니다.
1. 아키텍처 구성
1-1. Bigdata? Realtime?
피처링에서는 데이터가 배치성(시계열) 데이터인지 실시간성 데이터인지에 따라 아키텍처 구성을 다르게 가져가고 있습니다.
배치성(시계열) 서비스(출시 예정인 콘텐츠 분석, 키워드 분석)의 경우 AWS Glue와 AWS Kinesis를 활용해서 ETL 파이프라인을 구축하고 있고, 실시간성 서비스(인플루언서 찾기, 리포트, 통합검색 등)의 경우 MSA 패턴으로 설계된 아키텍처로 구성되어 있습니다.

피처링에서는 특히 오래전부터 IT 분야뿐 아니라 모든 분야에서 빼놓을 수 없는 핵심과 같은 자원인 빅데이터를 효과적으로 사용하기 위한 고민을 끊임없이 하고 있고 피처링에서 구축한 ETL 파이프라인이 피처링의 핵심 기술 중 하나라고 말할 수 있습니다. 해당 기술에 대한 내용은 다음 블로그 글에서 다룰 예정이며, 이번 글에서는 현재 피처링의 근간이 되는 실시간성 서비스 중에서도 피처링 리포트 **아키텍처에 대해 간략하게 다뤄보도록 하겠습니다.
1-2. Realtime 처리
피처링에서는 리포트의 유지 보수와 확장성을 고려하여 MSA(Micro Services Architecture) 방식으로 설계했습니다.
MSA 패턴에 대해 간략하게 설명하자면 MSA는 전통적인 모놀리스 아키텍처에서의 단점을 극복하기 위해 탄생했습니다.
전통적인 모놀리스 아키텍처는 애플리케이션을 단일 단위로 개발하고 배포합니다. 이러한 방식은 개발과 배포를 간단하게 만들어주지만, 큰 규모의 애플리케이션에서는 유지 보수와 확장성에 어려움이 있습니다. 한 개의 큰 애플리케이션이기 때문에, 코드를 변경하거나 업데이트하면 전체 시스템에 영향을 미치게 되며, 성능 이슈나 장애가 발생할 경우 전체 시스템에 영향을 받게 됩니다.
반면에 MSA는 서비스 간의 결합도를 낮추고, 각 서비스가 독립적으로 개발, 배포, 확장이 가능하도록 합니다. 각 서비스는 API를 통해 통신하며, 이를 위해 HTTP, RESTful API 등의 프로토콜이 사용됩니다. 이러한 아키텍처 패턴은 애플리케이션을 작은 단위로 나누어 관리할 수 있게 해주기 때문에, 유연성과 확장성이 높아지고, 개발 및 배포 과정도 더욱 쉬워집니다.
하지만 MSA는 모놀리스 아키텍처보다 복잡한 구조를 가지고 있으며, 서비스 간의 통신이 지연될 수 있다는 문제가 있습니다. 또한, 서비스 간의 데이터 일관성을 유지하는 것도 어렵습니다. 따라서 MSA를 도입하기 전에, 서비스 분리, API 설계, 서비스 검색 등 다양한 측면에서 고려해야 할 사항이 많습니다.
피처링 리포트 아키텍처는 크게 5개로 나뉠 수 있는데, 리포트 분석에 필요한 데이터를 수집하는 수집부, 수집한 Raw 데이터를 DB에 저장하는 저장부, 저장한 Raw 데이터를 피처링 리포트 분석 지표로 만들어주는 분석부, 분석한 리포트 데이터를 HTTP 통신을 통해 프론트엔드에 전달하는 백엔드, 백엔드로부터 분석된 리포트 데이터를 받아와 피처링 리포트 서비스 화면에 보여주는 프론트엔드가 있습니다.

2. 아키텍처 소개
2-1. Fast하게 수집하기
수집부는 말 그대로 리포트 분석에 필요한 데이터를 수집하는 곳입니다.
수집부에서 데이터를 제대로 가져오지 못하면 그 뒤의 프로세스도 사실상 제대로 진행되지 않기 때문에 리포트 분석의 전체 프로세스 중 가장 중요한 부분이고 가장 중요한 아키텍처라고 볼 수 있습니다.
수집부는 저장부, 분석부, 백엔드에서 Django Framework를 사용하는 것과 달리 FastAPI를 사용합니다.
수집부에서 가장 중요한 부분이 빠르고 정확하게 수집이 가능해야 하고 동시에 많은 수집 요청이 들어와도 병목 현상이 생기지 않아야 하기 때문에 ASGI (Asynchronous Server Gateway Interface)를 지원하며, 비동기적으로 작동함으로써 높은 성능을 보장하는 FastAPI를 선택하게 됐습니다.

속도 측면에서 월등한 성능을 보여주는 FastAPI입니다.
출처 – MagicStack
수집부에서는 수집 요청만 하는 것이 아니라 수집한 데이터를 저장부에 전달하는 역할도 동시에 수행하는 데 이 과정에서 Celery와 Redis를 사용합니다.
Celery는 분산 작업 큐를 제공하는 Python 기반의 비동기 작업 라이브러리이고, Redis는 메모리 기반의 데이터 저장소로, 매우 빠른 응답 속도와 처리 속도를 보장합니다. Celery와 함께 사용하면, 비동기 작업 처리 속도가 더욱 빨라지고, 대규모 데이터 처리에 용이하기 때문에 높은 성능을 보장합니다. 또한, Redis는 분산 환경에서도 높은 성능을 보장합니다. Celery와 함께 사용하면, 여러 대의 서버에서 분산 작업 처리를 할 수 있습니다. 이를 통해 시스템의 가용성과 확장성을 높일 수 있습니다.
수집부에서는 동시에 많은 수집 요청이 들어오기도 하고 수집 요청 한 번에 수십 개의 데이터를 저장부에 전달하게 되는 경우가 많은데 이러한 과정을 동기적으로 처리하게 되면 서비스에 병목이 생기게 됩니다. 그렇기 때문에 위에서 설명한 Celery와 Redis를 사용하여 데이터를 분산 처리하고 병목을 방지하고 있습니다.
다음은 Youtube Data API에서 데이터를 수집하고 수집한 데이터를 수집부로 전달하는 과정입니다.
위 그림을 간단하게 설명하면, 수집부에서 Youtube Data API(유튜브 공식 API)에 수집 요청을 하고, 수집한 데이터를 저장부와의 HTTP 통신을 통해 전달하는데, 이 과정을 Celery Task에서 정의하고 Worker에서 동작하게 됩니다.

2-2. 하나의 플랫폼 데이터를 저장할 때 얼마나 많은 I/O가 발생하게 될까?
저장부에서는 수집부에서 수집한 데이터를 필요한 데이터만 타입에 맞춰 저장하게 됩니다. 수집해온 데이터는 원형 데이터이기 때문에 필요하다면 데이터를 파싱 하기도 합니다.
다음은 유튜브 공식 API에서 1개의 채널을 데이터를 요청했을 때 넘어오는 Response 데이터입니다.
{
"kind": "youtube#channel",
"etag": etag,
"id": string,
"snippet": {
"title": string,
"description": string,
"publishedAt": datetime,
"thumbnails": {
(key): {
"url": string,
"width": unsigned integer,
"height": unsigned integer
}
}
},
"contentDetails": {
"relatedPlaylists": {
"likes": string,
"favorites": string,
"uploads": string,
"watchHistory": string,
"watchLater": string
},
"googlePlusUserId": string
},
"statistics": {
"viewCount": unsigned long,
"commentCount": unsigned long,
"subscriberCount": unsigned long,
"hiddenSubscriberCount": boolean,
"videoCount": unsigned long
},
"topicDetails": {
"topicIds": [
string
]
},
"status": {
"privacyStatus": string,
"isLinked": boolean
},
"brandingSettings": {
"channel": {
"title": string,
"description": string,
"keywords": string,
"defaultTab": string,
"trackingAnalyticsAccountId": string,
"moderateComments": boolean,
"showRelatedChannels": boolean,
"showBrowseView": boolean,
"featuredChannelsTitle": string,
"featuredChannelsUrls": [
string
],
"unsubscribedTrailer": string,
"profileColor": string
},
"watch": {
"textColor": string,
"backgroundColor": string,
"featuredPlaylistId": string
},
"image": {
"bannerImageUrl": string,
"bannerMobileImageUrl": string,
"backgroundImageUrl": {
"default": string,
"localized": [
{
"value": string,
"language": string
}
]
},
"largeBrandedBannerImageImapScript": {
"default": string,
"localized": [
{
"value": string,
"language": string
}
]
},
"largeBrandedBannerImageUrl": {
"default": string,
"localized": [
{
"value": string,
"language": string
}
]
},
"smallBrandedBannerImageImapScript": {
"default": string,
"localized": [
{
"value": string,
"language": string
}
]
},
"smallBrandedBannerImageUrl": {
"default": string,
"localized": [
{
"value": string,
"language": string
}
]
},
"watchIconImageUrl": string,
"trackingImageUrl": string,
"bannerTabletLowImageUrl": string,
"bannerTabletImageUrl": string,
"bannerTabletHdImageUrl": string,
"bannerTabletExtraHdImageUrl": string,
"bannerMobileLowImageUrl": string,
"bannerMobileMediumHdImageUrl": string,
"bannerMobileHdImageUrl": string,
"bannerMobileExtraHdImageUrl": string,
"bannerTvImageUrl": string,
"bannerExternalUrl": string
},
"hints": [
{
"property": string,
"value": string
}
]
},
"invideoPromotion": {
"defaultTiming": {
"type": string,
"offsetMs": unsigned long,
"durationMs": unsigned long
},
"position": {
"type": string,
"cornerPosition": string
},
"items": [
{
"id": {
"type": string,
"videoId": string,
"websiteUrl": string
},
"timing": {
"type": string,
"offsetMs": unsigned long,
"durationMs": unsigned long
},
"customMessage": string
}
]
}
}
위의 Response 데이터를 보시면 하나의 채널에 여러 개의 키가 존재하고 각 키마다 DB 테이블을 따로 관리하기 때문에 각 키값을 저장할 때마다 I/O가 발생하게 됩니다.
또 하나의 채널을 저장할 때 해당 채널의 50개의 비디오 데이터와 각 비디오마다 50개 댓글 데이터를 저장하기 때문에 1개의 채널에 대한 모든 데이터를 저장하는 과정에서 최소 1만 번 이상의 I/O가 발생한다는 것을 알 수 있습니다.
I/O가 발생할 때마다 복잡한 SQL을 작성하는 것은 매우 비효율적인 방법이며 성능 저하의 원인이 되기 때문에 보통 쿼리를 미리 작성하고 호출하는 방법을 사용하거나 ORM 라이브러리를 사용하게 되는데 피처링에서는 이러한 수많은 I/O를 처리하기 위해 ORM을 내장하고 있는 Django Framework를 사용합니다.
2-3. 리포트 아키텍처의 꽃 – 분석부
피처링 리포트 분석부는 2개의 프로젝트로 구성되어 있습니다. 하나의 프로젝트(Eg Report)는 수집부에 저장되어 있는 데이터를 불러오는 역할을 하면서 분석된 데이터를 백엔드에 전달하는 역할을 하고, 단순 계산 로직부터 리포트에 나오는 지표의 대부분 로직이 존재합니다. 나머지 한 개의 프로젝트(Eg Analy)는 카테고리 분류나 예측 분석, 키워드 분석, 이미지 분석과 같은 로직이 존재하고, 머신러닝에서 사용하는 학습 모델들이 존재합니다.
# 이미지 분석 요청 예시 코드 - FastAPI
@app.post("/image/analyze/face")
def analyze_face(parameter: JSONStructure = None):
from image.analyze_face import predict_age_gender
"""얼굴 나이,성별 예측"""
image_url = parameter[b"image_url"]
value = predict_age_gender(image_url, agender_model)
return value
@app.post("/image/analyze/object")
def analyze_object(parameter: JSONStructure = None):
from image.analyze_object import detection_object
"""사물 탐색하기"""
image_url = parameter[b"image_url"]
value = detection_object(image_url)
return value
@app.post("/image/analyze/colortone")
def analyze_colortone(parameter: JSONStructure = None):
from image.analyze_image import picker_feeling_color
"""컬러감 추출하기"""
image_url = parameter[b"image_url"]
value = picker_feeling_color(image_url)
return value
이렇게 프로젝트를 나눠놓은 이유는 머신러닝에 사용하는 모델까지 한 프로젝트에서 다루기에는 너무 무겁기 때문에 무거운 로직들은 한 프로젝트에 정리해놓고 FastAPI를 이용해 필요할 때마다 요청하는 방식을 사용하고 있습니다. 리포트에서 보여주는 지표들의 산정 과정이나 방법이 궁금하신 분들은 피처링 데이터 팀의 데이터 사이언티스트 주아가 작성한 블로그를 참고하시는 것도 좋을 것 같습니다.
다음은 수집부에서 가져온 데이터를 분석하는 과정입니다.

백엔드에서 분석 요청을 하면 분석부에서는 저장부에 저장되어 있는 데이터를 가져와 분석하여 리포트에 필요한 지표를 생성한다. 카테고리 분류나 키워드 분석 같은 경우 Eg Analy에 분석 요청을 해서 분석된 데이터를 가져옵니다. 이렇게 분석된 데이터를 백엔드에 리턴해주는 게 분석부의 역할입니다. 현재는 로직이 함수 단위로 되어 있고 여러 로직 함수가 순서대로 동작하는 구조이기 때문에 리포트가 분석되는 속도가 최적화되어 있지는 않기 때문에 추후 리포트 분석 속도를 올리기 위해서는 분석 로직 함수들을 AWS Lambda에 올려 비동기적으로 동작하게 하는 방법을 고려하고 있습니다.
2-4. 피처링 서비스 로직의 모든 것
백엔드는 프론트에서 요청한 리포트 분석뿐 아니라 피처링 서비스 로직의 모든 부분을 담당하고 있습니다. 백엔드에서는 인플루언서 찾기, 통합 검색, 인플루언서 관리, 캠페인 관리, 랭킹 등 모든 서비스의 로직이 정의되어 있습니다. 또한 다른 모든 아키텍처와의 HTTP 통신에 대한 프로토콜 정의가 되어 있기 때문에 백엔드를 통해 데이터 수집 요청을 하거나, 데이터 분석 요청, 저장부에 저장되어 있는 데이터 조회 등이 가능합니다.
class ProtocolManager:
"""피처링 백엔드의 여러 인프라의 프로토콜을 연결을 위한 클래스"""
def get_headers(self):
return self.headers
def get_host(self):
return self.host
def _get(self, path, *args, **kwargs):
now = datetime.now().strftime("%H:%M:%S")
start = datetime.now()
res_json = None
res = requests.get(self.get_host() + path, headers=self.get_headers())
if res.status_code == 200:
res_json = res.json()
return res_json
def _post(self, path, parameter, *args, **kwargs):
now = datetime.now().strftime("%H:%M:%S")
start = datetime.now()
res_json = None
res = requests.post(
self.get_host() + path, headers=self.get_headers(), json=parameter
)
if res.status_code == 200:
res_json = res.json()
return res_json
리포트 분석의 경우 백엔드에 저장되어 있는 인플루언서의 리포트 데이터 존재 여부를 판단하여 리포트 데이터가 있는 경우 분석한지 7일이 경과됐으면 데이터 수집부터 분석까지 모든 프로세스를 다시 거쳐 리포트를 생성하여 프론트에 전달하고 분석한지 7일 이내의 데이터라면 캐시 되어 있는 리포트 데이터를 전달합니다. 리포트 데이터가 없는 최초 분석의 경우에도 데이터 수집부터 분석까지 모든 프로세스를 거쳐 리포트를 새로 생성하고 분석된 리포트 데이터를 프론트에 전달합니다.
각 플랫폼마다 지표가 여러 개이기 때문에 지표 수만큼 분석부에 분석 요청을 보내야 하고 리포트를 한 번 생성할 때마다 분석부에 분석 요청을 여러 번 보내야 하기 때문에 분석 요청을 하는 모든 부분은 수집부나 저장부와 같이 Celery와 Redis를 사용하여 비동기적으로 처리하고 있습니다.
피처링 리포트 서비스에서는 리포트 분석이 실패하는 대표적인 케이스가 몇 가지 존재하고 각 케이스들을 백엔드에서 정의하여 해당하는 케이스에 맞는 에러 메시지를 프론트에 보내주고 있습니다. 대표적인 리포트 분석 실패 케이스로는 인플루언서 계정이 비공개인 경우, 인플루언서 계정이 삭제되거나 없는 계정인 경우, 인플루언서가 리포트 비공개 요청을 했을 경우, 리포트 분석 로직 상의 문제인 경우 등이 있습니다.
# 리포트 분석 실패 케이스 예시 코드
class ForbiddenInfluencerReport(FeaturingAPIException):
status_code = HTTPStatus.FORBIDDEN
default_detail = "접근금지된 인플루언서 리포트입니다."
default_code = "forbidden_influencer_report"
service_message = _("해당 채널은 인플루언서가 리포트를 비공개 요청했습니다. 리포트 확인이 불가능합니다.")
class PrivateInfluencer(FeaturingAPIException):
status_code = HTTPStatus.FORBIDDEN
default_detail = "해당 채널은 인플루언서에 의해 비공개 채널로 변경되어 데이터 확인이 불가능합니다."
default_code = "private_influencer"
service_message = _("해당 채널은 인플루언서에 의해 비공개 채널로 변경되어 데이터 확인이 불가능합니다.")
class HaveNotContents(FeaturingAPIException):
status_code = HTTPStatus.NOT_FOUND
default_detail = "해당 채널은 보유한 콘텐츠가 없어 데이터 분석이 불가능합니다."
default_code = "have_not_contents"
service_message = _("해당 채널은 보유한 콘텐츠가 없어 데이터 분석이 불가능합니다.")
2-5. 전역 상태 관리에 대한 고민
피처링 리포트 서비스 아키텍처 중 마지막으로 소개해 드릴 부분은 프론트엔드입니다.
프론트엔드에서는 모든 서비스 동작에 대한 요청을 백엔드에 보내는 역할을 하고 백엔드에서 받아온 데이터를 사용자에게 보내주는 역할을 하고 있기 때문에 어떻게 동작하는지에 대한 얘기보다는 피처링에서는 어떤 기술 스택을 사용하는지와 선정 이유에 대해서만 간략하게 설명하겠습니다.
피처링 프론트엔드에서는 React Framework, Next.js, Typescript를 사용하고 있습니다.
또한 상태 관리 라이브러리는 React Query, Zustand를 사용하고 있습니다.
React는 가장 많이 사용되는 프론트엔드 프레임워크이고, Next.js 같은 경우도 요새 워낙 많은 곳에서 검색 엔진 최적화(SEO) 및 초기 로딩 속도를 향상시키기 위해 사용하고 있고, Typescript도 변수, 함수, 객체 등에 대한 타입을 명시하여 오류를 미연에 방지하기 위해 많이 사용하고 있기 때문에 추가 설명은 하지 않겠습니다.
상태 관리 라이브러리 중 Redux를 사용하지 않고 React Query, Zustand를 사용하는 이유에 대해 설명하면, Redux는 가장 많이 쓰이는 상태 관리 라이브러리 중 하나이지만 코드 작성이 복잡한 반면 React Query, Zustand같은 경우 간단하고 직관적인 사용법 때문에 일정과 여러 부분을 고려해서 선택하게 되었습니다.
특히 Zustand의 경우 요새 가장 핫하게 뜨고 있는 상태 관리 라이브러리인데, 아래 그래프는 npm trends로 상태 관리 라이브러리 다운로드 추세를 비교한 그래프로 여전히 Redux의 사용이 압도적이지만 Zustand의 경우에도 꾸준하게 사용량이 증가하고 있는 점을 알 수 있습니다.

또한 1년 동안 받은 깃허브 스타 개수를 기반으로 인기 지수를 측정하는 risingstars를 확인하면 Zustand의 인기에 대해 확인하실 수 있습니다.


3. 마치며
지금까지 피처링 리포트의 전체적인 아키텍처 구성과 사용되는 기술 스택, 그리고 리포트 분석이 어떤 흐름으로 진행되는지 간단하게 설명드렸습니다.
다음번에는 위의 서론에서도 언급했듯이 ETL 파이프라인을 어떻게 구성했고 어떤 기술을 사용했는지에 대해 소개하는 글로 찾아뵙도록 하겠습니다.

등록된 댓글이 없습니다.