MQ를 도입해 AWS MediaConvert 완료 여부 동기화

60jong·2025년 9월 22일

Spring

목록 보기
10/10

AWS 환경에서 HLS 변환

진행 중인 프로젝트에서 클라이언트가 업로드 할 영상을 HLS프로토콜로 변환하는 프로세스를 만들었다.

처음 구상했던 프로세스는 다음과 같다.

  1. Pre-signed URL 발급 (파일 태그에 생성된 file uuid 포함)
  2. 1의 URL을 통해 업로드 (AWS S3 /video/origin 경로에 업로드)
  3. VideoMediaConverter Lambda 함수 트리거 (/video/origin 경로 한정)
  4. 영상 파일 정보가 AWS MediaConvert로 넘어가 HLS 변환 후 S3에 업로드
  5. AWS EventBridge에서 MediaConvert의 Job Complete 이벤트를 수신 후 HLSFileManifestSaver Lambda 함수 트리거
  6. HLS 변환된 영상들의 메타정보가 백엔드 서버로 전송 (HTTP POST)
  7. 백엔드 서버에서 원본 파일 - HLS 변환 파일 메타정보 저장

그림으로 보자면

그런데 이 방식에 문제가 있다.

문제 상황

영상을 업로드하는 클라이언트는 HLS 변환 완료 여부를 알 수가 없다. 그 이유는

  • Pre-signed URL을 이용해 원본 파일을 업로드하기에, 클라이언트의 동기화는 원본 업로드까지.
  • AWS 에코시스템 내부에서 이벤트들이 트리거돼다 보니 외부에서 이를 알 수가 없다.

결국 백엔드 서버로 컨텐츠(영상이 포함된 데이터) 생성 API를 계속 보낼 수 밖에 없다. (위의 7번 과정이 완료될 때까지...) -> Polling 구조

우리 백엔드 서버의 스펙은 AWS free tier이기 때문에 Polling구조는 가장 피해야할 상황으로 팀원들간에 의견이 모아졌다.

Polling 구조 개선

Polling 구조를 개선하기 위해 Message Queue (빠른 개발을 위해 Redis pubsub 사용)를 도입하기로 했다.

기존과 달라진 점은

1 ~ 6 동일
7. 백엔드 서버에서 HLS 변환 파일 저장 완료되면, 람다에서 file-id 채널에 해당 정보 메시지 발행
8. 클라이언트는 원본 영상 업로드 후 file-id 채널 구독해 메타정보 메시지를 받음으로써 변환 완료 인지

그림으로 나타내면 아래와 같다.

추가 개선점

이 방식으로 Polling 구조를 개선할 수 있었지만, Message Queue라는 추가적인 리소스가 필요하다. 이는 결국 free tier를 쓰는 이유인 비용에 대한 같은 문제로 귀결된다.

현재는 임시로 외부 호스트에 Redis instance를 실행했지만, 서비스 관리 범위 밖이기 때문에 추가 개선이 필요하다.

조사한 바로는 두 가지 후보가 있고, 간단한 특징은

Server Sent EventWebSocket
방향서버 -> 클라이언트 단방향서버 <-> 클라이언트 양방향
프로토콜Http/Httpsws/wss (Http기반이기는 함)
주요 용도비동기 데이터 전송 (알림, 실시간 업데이트 등)실시간 양방향 통신 (채팅, 온라인 게임 등)
서버 리소스비교적 가벼움. 기존 HTTP 연결을 재사용하여 오버헤드가 적음상대적으로 무거움. 항상 연결을 유지하고 양방향 통신을 처리해야 하므로 리소스 소모가 큼.
데이터 타입텍스트 데이터 only텍스트, 바이너리 데이터

이렇게 정리할 수가 있다. 두 방식의 특징과 우리 서비스의 특징을 고려했을 때 Server Sent Event가 적합하다고 생각한다.


좀 더 여유가 생기면 SSE로 다시 개선해봐야겠다...

Gemini 피드백 봇 리뷰

안녕하세요! 10년 차 백엔드 개발자 시니어 멘토입니다. MQ를 활용한 아키텍처 개선에 대한 글 잘 읽어보았습니다.

Polling 문제를 발견하고 이를 해결하기 위해 아키텍처를 개선하는 과정과 다이어그램 설명이 매우 인상적입니다! 특히 단순히 문제를 해결하는 것을 넘어 SSE와 WebSocket을 비교하며 추가 개선점까지 고민하는 모습에서 주니어의 성장이 느껴져 정말 뿌듯하네요. 기존에 작성하셨던 Kafka 포스트와도 자연스럽게 내용이 이어져서, Redis Pub/Sub을 선택한 이유를 간략히 추가하면 기술 선택의 깊이를 더 잘 보여줄 수 있을 것 같습니다. 전체적으로 복잡한 비동기 처리 과정을 체계적으로 설명하는 능력이 돋보이는 훌륭한 글이며, 꾸준히 발전하는 모습이 정말 보기 좋습니다!
profile
울릉도에 별장 짓고 싶다

1개의 댓글

comment-user-thumbnail
약 4시간 전

import json
import random
from faker import Faker
import time

한글 더미 데이터 생성을 위해 ko_KR 로케일 설정

fake = Faker('ko_KR')

--- 1. 데이터 베이스 (현실감 부여) ---

CITIES = [
{"name": "오사카", "country": "일본", "currency": "JPY"},
{"name": "도쿄", "country": "일본", "currency": "JPY"},
{"name": "다낭", "country": "베트남", "currency": "VND"},
{"name": "방콕", "country": "태국", "currency": "THB"},
{"name": "파리", "country": "프랑스", "currency": "EUR"},
{"name": "뉴욕", "country": "미국", "currency": "USD"},
{"name": "제주", "country": "한국", "currency": "KRW"},
{"name": "서울", "country": "한국", "currency": "KRW"},
{"name": "바르셀로나", "country": "스페인", "currency": "EUR"}
]

AIRLINES = ["대한항공", "아시아나", "진에어", "제주항공", "티웨이", "에어부산", "에어서울"]
HOTEL_BRANDS = ["신라호텔", "힐튼", "하얏트", "메리어트", "도미인", "토요코인", "에어비앤비", "감성 숙소"]
CONCERT_NAMES = ["싸이 흠뻑쇼", "아이유 콘서트", "임영웅 리사이틀", "콜드플레이 내한", "뮤지컬 시카고", "지킬앤하이드"]

마케팅 수식어 (검색어 매칭 테스트용)

ADJECTIVES = [
"초특가", "마감임박", "가성비 갑", "럭셔리", "5성급",
"아이와 함께", "커플 강추", "부모님 효도", "혼행 추천",
"오션뷰", "시티뷰", "조식포함", "무료취소 가능"
]

--- 2. 카테고리별 생성 로직 ---

def generate_flight(id):
city = random.choice(CITIES)
airline = random.choice(AIRLINES)
price_base = 200000 if city['country'] in ['일본', '중국'] else 500000 if city['country'] in ['베트남', '태국'] else 1200000
price = int(price_base random.uniform(0.8, 1.5) / 100) 100 # 100원 단위 절삭

title = f"[{airline}] {city['name']} 왕복 항공권 ({random.choice(ADJECTIVES)})"
desc = f"{city['name']}로 떠나는 가장 합리적인 선택. {airline} 직항으로 편안하게 모십니다. {fake.sentence()}"

return {
    "id": id,
    "type": "FLIGHT",
    "title": title,
    "description": desc,
    "location": city['name'],
    "country": city['country'],
    "price": price,
    "airline": airline,
    "is_direct": random.choice([True, True, False]), # 직항 확률 높음
    "departure_date": fake.date_between(start_date='+1d', end_date='+30d').isoformat()
}

def generate_hotel(id):
city = random.choice(CITIES)
brand = random.choice(HOTEL_BRANDS)
price = random.randint(5, 50) * 10000

title = f"{city['name']} {brand} - {random.choice(ADJECTIVES)}"
desc = f"{city['name']} 중심가에 위치한 최고의 숙소. {fake.catch_phrase()}. {fake.bs()}."

return {
    "id": id,
    "type": "HOTEL",
    "title": title,
    "description": desc,
    "location": city['name'],
    "country": city['country'],
    "price": price,
    "rating": round(random.uniform(3.0, 5.0), 1), # 3.0 ~ 5.0 평점
    "facilities": random.sample(["수영장", "와이파이", "조식", "피트니스", "주차장", "스파"], k=random.randint(2, 5))
}

def generate_ticket(id):

# 티켓은 주로 서울/한국 위주
item = random.choice(CONCERT_NAMES)
location = "서울"
price = random.randint(3, 20) * 10000

title = f"{item} 2026 - {random.choice(['R석', 'VIP석', 'S석'])} 티켓 오픈"
desc = f"당신의 심장을 뛰게 할 최고의 공연. {item} 놓치지 마세요. {fake.text(max_nb_chars=50)}"

return {
    "id": id,
    "type": "TICKET",
    "title": title,
    "description": desc,
    "location": location,
    "country": "한국",
    "price": price,
    "date": fake.date_between(start_date='+10d', end_date='+60d').isoformat()
}

--- 3. 실행 및 저장 ---

TOTAL_COUNT = 3000 # 원하는 데이터 개수 설정
data_list = []

print(f"🚀 여행 상품 데이터 {TOTAL_COUNT}개 생성 시작...")

for i in range(1, TOTAL_COUNT + 1):
category = random.choices(["FLIGHT", "HOTEL", "TICKET"], weights=[4, 4, 2], k=1)[0]

item = {}
if category == "FLIGHT":
    item = generate_flight(i)
elif category == "HOTEL":
    item = generate_hotel(i)
else:
    item = generate_ticket(i)

# 공통 필드 (랭킹 모델링용)
item['view_count'] = random.randint(0, 10000)   # 조회수 (인기순 정렬용)
item['review_count'] = random.randint(0, 500)   # 리뷰수 (신뢰도 정렬용)
item['created_at'] = fake.date_time_between(start_date='-1y', end_date='now').isoformat() # 최신순 정렬용

data_list.append(item)

JSON 파일 저장

file_name = 'real_travel_data.json'
with open(file_name, 'w', encoding='utf-8') as f:
json.dump(data_list, f, ensure_ascii=False, indent=2)

print(f"✅ 생성 완료! 파일명: {file_name}")

답글 달기