.NET 레거시를 건드리지 않고 외부 VOD 솔루션을 연동한 방법
동영상 광고 타입을 고려하지 않은 레거시 .NET 광고 시스템에 Shoplive VOD 솔루션을 연동하면서, 레거시 안정성에 영향 없이 수십억 원 규모의 신규 광고 상품을 런칭한 과정을 공유합니다.
참고: 이 글의 코드는 실제 업무에서 경험한 내용을 바탕으로 개념적으로 재작성한 것입니다. 실제 회사 코드와는 무관합니다.
연동 흐름
들어가며
레거시 시스템에 새 기능을 추가하는 작업은 항상 긴장됩니다. 특히 수천 개의 광고 상품이 실시간으로 노출되고 있는 프로덕션 .NET 시스템에서는 더욱 그렇습니다.
이 글은 이미지 광고만을 위해 설계된 레거시 광고 어드민 시스템에, 동영상(VOD) 광고 상품을 최소한의 영향으로 추가한 경험을 공유합니다. 결과적으로 이 작업으로 Gmarket에 새로운 동영상 광고 상품이 출시됐고 최소 수십억 원 규모의 신규 매출에 기여했습니다.
문제 정의: 무엇이 어려웠나
기존 시스템의 한계
기존 .NET 광고 어드민 시스템은 이미지 배너 광고를 전제로 설계되어 있었습니다. 데이터 모델부터 UI까지 모든 것이 이미지 소재(Creative)를 기준으로 만들어졌습니다.
동영상 광고는 근본적으로 다릅니다:
- 파일 크기: 이미지는 수백 KB, 동영상은 수십~수백 MB
- 처리 방식: 이미지는 바로 쓸 수 있지만, 동영상은 인코딩/트랜스코딩 과정이 필요
- 메타데이터: 재생 시간, 해상도, 코덱 정보 등 이미지에는 없는 속성들
- 상태 관리: 동영상 처리가 완료됐는지, 실패했는지 외부 시스템에서 확인 필요
왜 레거시를 완전히 바꾸지 않았나
가장 간단한 해법은 새로운 동영상 광고 시스템을 별도로 만드는 것이겠지만, 현실적으로 불가능했습니다. 광고주 관리, 상품 등록, 심사 프로세스 등 이미 잘 동작하는 수십 개의 기능을 다시 만들어야 하는 공수가 너무 컸습니다.
결론은 기존 레거시를 최소한으로 수정하고, 외부 VOD 솔루션(Shoplive)과 연동하는 방향이었습니다.
설계: 웹훅 기반 단방향 동기화
핵심 설계 결정은 웹훅(Webhook) 기반 단방향 동기화였습니다.
[광고주] → [.NET 어드민] → [Shoplive API] → [동영상 업로드/인코딩]
↓
[인코딩 완료 웹훅]
↓
[.NET 어드민 상태 업데이트]
레거시 .NET 시스템이 Shoplive의 상태를 주기적으로 폴링(Polling)하는 대신, Shoplive가 처리 완료 후 웹훅으로 알려주는 방식을 선택했습니다. 이 방식의 장점은:
- 레거시 시스템의 부하 최소화: 폴링 없이 이벤트 기반으로 동작
- 실시간에 가까운 상태 동기화: 인코딩 완료 즉시 반영
- 결합도 최소화: 레거시가 외부 시스템에 의존하지 않음
구현 과정
1. .NET 어드민 최소 수정
레거시 .NET 시스템에는 최소한의 변경만 가했습니다:
- 광고 소재(Creative) 타입 필드에
VIDEO추가 - 동영상 파일 업로드 처리 로직 추가
- Shoplive API 호출 모듈 추가 (콘텐츠 생성, 정보 동기화)
- 동영상 처리 상태 필드 추가 (
PENDING/PROCESSING/READY/FAILED)
// .NET 레거시에 추가한 Creative 타입 확장
public enum CreativeType
{
IMAGE = 1,
VIDEO = 2 // 신규 추가
}
// Shoplive 연동 서비스 추가
public class ShopliveService
{
public async Task<string> CreateContent(VideoCreativeDto dto)
{
var response = await _httpClient.PostAsync(
"/api/v1/contents",
JsonContent.Create(dto)
);
// 외부 콘텐츠 ID 반환
return (await response.Content.ReadFromJsonAsync<ShopliveResponse>()).ContentId;
}
}
2. 연동 안정화: 실패 처리가 핵심
외부 API 연동에서 가장 중요한 것은 실패 처리입니다. 네트워크 오류, Shoplive 서버 일시 장애, 타임아웃 등 다양한 이유로 연동이 실패할 수 있습니다.
public async Task SyncVideoStatus(string creativeId, string shopliveContentId)
{
const int maxRetries = 3;
for (int attempt = 0; attempt < maxRetries; attempt++)
{
try
{
var status = await _shopliveService.GetContentStatus(shopliveContentId);
await UpdateCreativeStatus(creativeId, status);
return;
}
catch (HttpRequestException ex) when (attempt < maxRetries - 1)
{
_logger.LogWarning("Shoplive 연동 실패, 재시도 {attempt}: {message}",
attempt + 1, ex.Message);
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt))); // Exponential backoff
}
}
// 최종 실패 시 알람
await _alertService.SendAlert($"Shoplive 동기화 최종 실패: {creativeId}");
}
3. 광고 상세 조회 시 실시간 상태 반영
광고주가 등록한 동영상 광고를 조회할 때, DB의 상태와 실제 Shoplive의 처리 상태가 다를 수 있습니다. 이를 방지하기 위해 상세 조회 시 Shoplive의 실제 상태를 확인하고 내부 상태를 최신화했습니다.
public async Task<CreativeDetailDto> GetCreativeDetail(string creativeId)
{
var creative = await _repository.GetById(creativeId);
if (creative.Type == CreativeType.VIDEO &&
creative.Status != VideoStatus.READY)
{
// 실시간 상태 확인 및 업데이트
var latestStatus = await _shopliveService.GetContentStatus(creative.ShopliveContentId);
if (latestStatus != creative.Status)
{
await UpdateCreativeStatus(creativeId, latestStatus);
creative.Status = latestStatus;
}
}
return creative.ToDetailDto();
}
결과
레거시 시스템 안정성을 유지하면서:
- 동영상 광고 상품 신규 런칭 성공
- 런칭 이후 최소 수십억 원 규모의 신규 광고 매출 기여
- 외부 VOD 솔루션 연동 과정에서 발생한 실패율 0% 유지 (재시도 로직 덕분)
마치며
레거시 시스템은 "고치기 무서운 것"이 아니라 "영향 범위를 정확히 파악하고 최소한으로 개입해야 하는 것"입니다.
이 프로젝트에서 배운 가장 큰 교훈은 영향 범위를 철저히 정의하는 것이었습니다. 어디까지 건드리고 어디서부터는 새로 만드는지의 경계를 명확히 하고, 그 경계를 철저히 지켰기 때문에 레거시 안정성을 해치지 않고 신규 기능을 추가할 수 있었습니다.