Big Project · AI's EYE 21
비동기 분산 온보딩 구조의 비밀: 왜 이렇게 복잡해야만 할까?
사용자가 영상을 올리면 API 서버가 그 요청 안에서 바로 YOLO 분석까지 끝내는 구조가 가장 단순해 보인다. 왜 job queue와 별도 Worker, auto replay가 필요한지 궁금하다.
근거 · 프로젝트 문서 · 코드 · 테스트
1장: 단일 API 처리: 무엇이 문제일까?
솔라는 모니터에 떠 있는 한 문장 앞에서 미간을 찌푸렸다. 기술 블로그에서 발견한 아키텍처 사례 연구의 일부였다. “신규 매장 온보딩: 미니 PC의 작업 큐와 외부 GPU 워커로 분리된 비동기 분산 구조.” 문장 속 단어들은 대부분 아는 것이었지만, 조합된 그림은 어색했다. 왜 이렇게까지 해야 하는 걸까?
“언니, 이것 좀 봐.”
솔라의 부름에 거실에서 책을 읽던 루나가 다가왔다. 솔라는 화면의 문장을 가리켰다.
“매장에 새로 설치하는 시스템에 관한 글이야. 사용자가 영상을 올리면 그걸 분석해서 처리하는 과정인데, 구조가 너무 복잡해. 미니 PC가 있고, 작업 큐라는 게 있고, 심지어 분석은 외부 GPU 워커라는 곳에서 따로 한대.”
솔라는 의자를 돌려 루나를 마주 보며 자신의 생각을 덧붙였다.
“내 생각엔 이거 너무 돌아가는 길 같아. 그냥 사용자가 영상을 올리면, 그걸 받은 API 서버가 바로 분석까지 끝내고 ‘처리 완료!’하고 응답해주면 되잖아. 그게 가장 직관적이고 단순한 방법 아닐까?”
솔라의 말은 타당해 보였다. 중간 단계 없이 입력과 출력이 직선으로 이어지는 구조. 군더더기 없고, 이해하기 쉬웠다. 루나는 솔라가 가리킨 문장과, 그 문장을 비판적으로 바라보는 솔라의 얼굴을 차례로 잠시 살폈다.
“가장 단순한 구조. 맞는 말이야. 그렇게 한번 직접 만들어보면 어떨까? 아주 작은 버전으로.”
루나는 솔라의 컴퓨터 앞에 자신의 노트북을 펼쳐 놓았다. 복잡한 코드를 작성하려는 게 아니었다. 아주 간단한 웹 서버 예제를 화면에 띄웠다.
# 간단한 웹 서버 예제
from http.server import BaseHTTPRequestHandler, HTTPServer
import time
class SimpleServer(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/process':
# 영상 분석 작업을 흉내 내기 위해 일부러 시간을 지연시킨다
time.sleep(1)
self.send_response(200)
self.end_headers()
self.wfile.write(b'Processing complete.')
else:
self.send_response(404)
self.end_headers()
httpd = HTTPServer(('localhost', 8000), SimpleServer)
httpd.serve_forever()
“여기 /process 라는 주소로 요청을 보내면, 1초 동안 무언가 처리하는 척하다가 ‘처리 완료’라고 응답을 주는 서버가 있어. 네가 말한 ‘API 서버가 바로 분석까지 끝내는 구조’를 흉내 낸 거지. 여기서 time.sleep(1) 이 부분이 영상 분석에 걸리는 시간이라고 상상해 봐.”
솔라는 터미널을 열어 curl http://localhost:8000/process 명령어를 입력했다. 커서가 한 번 깜빡이더니, 1초 뒤 ‘Processing complete.‘라는 문구가 깔끔하게 출력되었다.
“이것 봐. 잘 되네. 1초 정도는 괜찮잖아.”
“응, 1초는 괜찮지. 그런데 매장에서 올리는 영상이 수십 기가바이트짜리 고화질 영상이라면? YOLO 분석 모델이 아주 복잡한 연산을 해야 한다면? 처리 시간이 1초가 아니라 5분, 10분이 걸릴 수도 있어.”
루나는 코드에서 숫자 1을 300으로 바꿨다. 5분을 의미하는 300초였다.
“자, 다시 한번 요청해 볼래?”
솔라는 아까와 같은 명령어를 다시 입력했다. 엔터 키를 누르자, 터미널은 아무런 반응 없이 커서만 깜빡이고 있었다. 1초, 5초, 10초… 시간이 흐를수록 솔라의 표정이 점점 불안해졌다. 손가락으로 책상을 톡톡 두드리던 솔라는 결국 참지 못하고 물었다.
“이거… 지금 작동하고 있는 거 맞아? 아니면 그냥 멈춘 건가?”
“서버는 열심히 ‘분석’ 작업을 하고 있겠지. 300초 동안.”
“사용자 입장에서는 알 수가 없네. 그냥 하염없이 기다려야 하잖아. 그리고 웹브라우저에서 이렇게 오래 걸리면 보통 ‘연결 시간 초과’ 에러가 뜨지 않아?”
솔라의 말대로였다. 대부분의 웹 환경은 이렇게 긴 응답 대기를 허용하지 않는다. 서버 앞단에 있는 게이트웨이나 로드 밸런서는 정해진 시간(보통 30초나 60초) 안에 응답이 오지 않으면, 서버에 문제가 생겼다고 판단하고 연결을 스스로 끊어버린다. 사용자에게는 ‘504 Gateway Timeout’ 같은 차가운 에러 메시지만 보일 뿐이다.
한참이 지났을까, 솔라의 터미널에 드디어 에러 메시지가 나타났다. 서버가 응답하기도 전에, 요청을 보낸 쪽에서 먼저 기다림을 포기한 것이다.
솔라는 잠시 화면을 응시하다가, 길게 한숨을 내쉬며 의자에 등을 기댔다.
“알겠다. ‘단순한 구조’는 맞는데, 이건 그냥 쓸 수 없는 구조였네.”
처음에는 명쾌하고 효율적이라고 생각했던 ‘API 서버가 직접 모든 것을 처리하는’ 모델의 치명적인 약점을 몸소 체험한 순간이었다. 사용자는 작업이 성공했는지 실패했는지도 모른 채 하염없이 기다리거나, 결국 에러를 마주하게 된다. 서버 리소스는 긴 작업 시간 동안 하나의 요청에 묶여 다른 요청을 처리하지 못한다.
솔라는 다시 기술 블로그의 문장을 바라보았다. “사용자가 영상을 올리면 API 서버가 그 요청 안에서 바로 YOLO 분석까지 끝내는 구조가 가장 단순해 보인다.” 아까와 같은 문장이었지만, 이제는 전혀 다른 의미로 읽혔다. ‘단순하지만, 결정적인 문제가 있다.’
“좋아, 그럼 사용자를 이렇게 기다리게 만들면 안 된다는 건 알겠어. 요청을 받으면 일단 ‘네, 접수했습니다’ 하고 바로 응답을 주고, 오래 걸리는 분석 작업은 나중에 따로 처리해야 한다는 거겠지.”
솔라는 잠시 생각에 잠겼다.
“그런데… 그렇게 하면 그 ‘나중에 할 일’은 어떻게 관리하지? 만약 일을 처리하기 전에 서버가 갑자기 꺼지기라도 하면, 그 작업은 그냥 공중에서 사라져 버리는 거 아냐?“
2장: 작업 큐: 요청을 안정적으로 처리하는 비법
솔라의 마지막 질문이 허공에 맴도는 동안, 루나는 말없이 테이블 위를 정리하기 시작했다. 노트북과 컵을 한쪽으로 밀어내고 깨끗한 공간을 확보했다. 그러고는 서랍에서 작은 골판지 상자와 포스트잇 한 묶음을 꺼내 테이블 중앙에 놓았다. 솔라는 언니가 무엇을 하려는지 알 수 없어 잠자코 지켜보기만 했다.
루나는 펜으로 골판지 상자 겉면에 ‘작업 요청함’이라고 썼다. 그리고 포스트잇 한 장을 떼어 ‘고화질 영상 분석 #1’이라고 적어 솔라에게 건넸다. 이 작은 종이가 방금 전까지 이야기하던 ‘오래 걸리는 작업’을 대신하는 듯했다. 이 간소한 소품들이 만들어낸 새로운 풍경 앞에서, 솔라가 품었던 ‘사라져 버릴지 모르는 작업’에 대한 불안감이 구체적인 형태로 떠올랐다.
“자, 솔라 네가 사용자야. 나한테 분석을 요청해 봐.”
루나가 말했다. 솔라는 어리둥절한 표정으로 손에 든 포스트잇을 루나에게 내밀었다.
“영상 분석해 주세요.”
루나는 솔라에게서 포스트잇을 받자마자, 뒤도 돌아보지 않고 ‘작업 요청함’이라고 쓰인 상자 안에 툭 던져 넣었다. 그러고는 곧바로 솔라를 향해 환하게 웃으며 말했다.
“네, 요청 접수되었습니다!”
모든 일이 1초도 안되어 끝났다. 이전 장에서 겪었던 기약 없는 기다림과는 전혀 다른 경험이었다. 솔라가 다시 ‘영상 분석 #2’라고 적힌 포스트잇을 건네자, 루나는 이번에도 똑같이 상자에 넣고 즉시 “접수 완료!”라고 답했다. 요청이 쌓여도 루나(API 서버 역할)는 전혀 바빠지지 않았다. 그냥 상자에 넣기만 하면 끝이었으니까.
“잠깐.”
루나가 갑자기 자리에서 일어났다.
“API 서버에 장애가 발생해서 재부팅해야 해.”
루나는 그렇게 말하고는 잠시 테이블에서 떠나 거실을 한 바퀴 돌고 다시 자리로 돌아왔다. 솔라의 눈은 자연스럽게 테이블 위의 상자로 향했다. 루나가 자리를 비웠을 때도, 상자는 그 자리에 얌전히 놓여 있었다. 그 안에는 솔라가 요청했던 ‘영상 분석 #1’, ‘#2’ 포스트잇이 그대로 담겨 있었다.
“아…!”
솔라는 짧은 탄성을 터뜨렸다.
“알겠다. 요청을 받는 서버가 꺼지더라도, 이 ‘작업 요청함’은 다른 곳에 있으니까 요청 자체가 사라지진 않는구나.”
“바로 그거야.”
루나가 상자를 가리키며 말했다.
“이 상자가 바로 그 기술 블로그에 나왔던 analysis_jobs queue의 역할이야. 요청을 받는 즉시, 해야 할 일을 안전한 곳에 기록해두고 사용자에게는 ‘잘 받았다’고 바로 알려주는 거지. 그럼 서버에 갑자기 문제가 생기거나, 동시에 수백 개의 요청이 몰려와도 일단 모든 요청을 빠짐없이 받아둘 수 있어.”
단순히 작업을 줄 세워 놓는 곳이라고 생각했던 ‘큐(Queue)’의 진짜 의미가 보이는 순간이었다. 큐는 단순한 대기열이 아니었다. 요청을 받는 부분(API 서버)과 실제로 일을 처리하는 부분(워커) 사이를 갈라놓는 견고한 벽이자, 둘 사이를 안전하게 이어주는 конвейер 벨트였다. 한쪽이 멈추거나 느려져도 다른 쪽은 영향을 받지 않고 자기 할 일을 계속할 수 있게 만드는 핵심 장치.
솔라는 다시 블로그의 문장을 떠올렸다. ‘미니PC는 미디어 업로드, analysis_jobs queue, 설정과 결과 저장을 담당한다.’ 이제 이 문장은 다르게 읽혔다. 미니PC가 단순히 파일을 받고, 목록을 만들고, 결과를 저장하는 기계가 아니었다. 전체 시스템에서 가장 중요한 약속, 즉 ‘고객의 요청은 절대 잃어버리지 않는다’는 약속을 지키기 위해, analysis_jobs queue라는 금고를 책임지는 역할이었던 것이다.
“좋아, 이제 작업이 안전하게 보관된다는 건 알겠어. 갑자기 요청이 몰려도 일단 다 받아둘 수 있다는 것도.”
솔라는 만족스러운 표정을 짓다가, 문득 한 가지 의문이 들었다. 그녀는 포스트잇이 차곡차곡 쌓여있는 ‘작업 요청함’ 상자를 턱으로 가리켰다.
“그런데 언니, 저기 쌓여만 가면 어떡해? 저 일들은 결국 누가, 어떻게 처리하는 건데? 저 상자가 직접 분석을 하진 않잖아.”
3장: GPU 워커: 전문 리소스로 무거운 작업 분리하기
솔라의 질문이 끝나기 무섭게, 루나는 솔라의 노트북에서 시스템 ‘활성 상태 보기(Activity Monitor)’ 창을 화면 가득 띄웠다. 알록달록한 그래프와 쉴 새 없이 오르내리는 숫자들의 목록이 나타났다. CPU, 메모리, 에너지, 디스크, 네트워크… 컴퓨터의 모든 장기가 자신의 상태를 보고하는 관제실 같았다. 이전 장에서 역할 놀이에 쓰였던 ‘작업 요청함’ 상자는 여전히 테이블 한쪽에 놓여 있었지만, 이제 두 사람의 시선은 그 상자 안의 포스트잇이 아니라, 그 일을 실제로 수행할 기계의 내부로 향했다.
루나는 마우스 커서로 CPU 사용량 그래프를 가리켰다. 대부분의 시간 동안 그래프는 바닥에 가까운 낮은 상태를 유지하고 있었다.
“자, 솔라. 이 노트북이 바로 ‘미니 PC’라고 생각해 봐. 평소에는 웹 요청을 받고, ‘작업 요청함’에 포스트잇을 넣고, 자잘한 설정들을 관리하지. 지금처럼 아주 한가해.”
루나는 미리 준비해 둔 스크립트 파일을 하나 실행했다. 영상 인코딩이나 복잡한 이미지 필터를 흉내 내는, 오직 CPU 연산만으로 3D 이미지를 렌더링하는 프로그램이었다. 스크립트가 실행되자마자, 잠잠하던 노트북 팬이 갑자기 굉음을 내며 돌기 시작했다.
“으악, 시끄러워!”
솔라가 귀를 살짝 막으며 인상을 썼다. 더 심각한 문제는 다른 곳에 있었다. 활성 상태 보기 창의 CPU 그래프가 순식간에 100%까지 치솟아 천장에 달라붙었다. 솔라가 무심코 마우스를 움직여 보려 했지만, 커서는 뚝뚝 끊기며 간신히 움직였다. 백그라운드에서 재생되던 음악마저 더듬거리기 시작했다.
“이거… 내 컴퓨터 왜 이래? 마우스도 안 움직여.”
“지금 이 노트북, 즉 ‘미니 PC’는 영상 분석이라는 무거운 작업을 처리하느라 모든 힘을 쏟아붓고 있어. CPU가 100%라는 건, 다른 어떤 일에도 신경 쓸 여력이 없다는 뜻이야. 마우스 움직임을 처리하거나, 음악을 재생하거나, 심지어 새로운 사용자의 접속 요청을 받는 아주 기본적인 일조차 버거워진 거지.”
솔라는 그제야 깨달았다. ‘작업 요청함’에 작업을 쌓아두는 것만으로는 부족했다. 그 작업을 처리하는 과정이 다른 중요한 기능들을 마비시킨다면, 시스템은 결국 멈춰버린 것이나 다름없었다. 미니 PC가 영상 분석까지 직접 처리해야 한다는 생각은, 마치 접수 데스크 직원이 갑자기 주방에 들어가 요리까지 하려는 것과 같았다. 데스크는 텅 비고, 주방 일은 서툴러 모든 것이 엉망이 되는 그림이었다.
루나가 스크립트를 강제 종료하자, 미친 듯이 돌던 팬 소리가 잦아들고 CPU 그래프가 다시 바닥으로 내려왔다. 노트북은 언제 그랬냐는 듯 평온을 되찾았다.
“그럼 분석 작업은 대체 누가 해? 미니 PC가 못 한다면… 아!”
솔라는 무언가 떠오른 듯 눈을 반짝였다. 그녀는 다시 기술 블로그의 문장을 찾아 화면에 띄웠다.
미니PC API가 미디어와 analysis job을 관리하고 GPU의 upload_job_worker.py가 claim·download·analysis·result upload를 맡는다.
이전에는 그저 역할을 나열한 것으로만 보였던 문장이, 방금 겪은 경험을 통해 선명한 설계도로 보이기 시작했다.
“알겠다! 미니 PC는 그냥 관리만 하는 거였어. ‘analysis job’을 만들어서 큐에 넣기까지. 그리고 진짜 힘든 일은… ‘GPU’라는 전문가가 따로 있었던 거네!”
“바로 그거야.” 루나가 고개를 끄덕였다. “CPU가 다용도 일꾼이라면, GPU는 복잡한 그래픽 처리나 병렬 연산에 특화된 전문가야. 영상 분석처럼 똑같은 계산을 수백만 번 반복해야 하는 작업에 훨씬 효율적이지. 그래서 처음부터 값비싼 GPU가 없는 작은 ‘미니 PC’와, 분석을 전담할 ‘GPU 워커’를 물리적으로 분리한 거야. 각자 가장 잘하는 일에만 집중하도록.”
솔라는 이제 upload_job_worker.py라는 이름 뒤에 숨은 작업의 흐름을 상상할 수 있었다.
- claim: GPU 워커가 ‘작업 요청함’(큐)에서 새 작업(포스트잇)을 하나 가져온다. “이 일은 제가 맡겠습니다.”
- download: 작업에 필요한 영상 파일을 미니 PC로부터 다운로드한다.
- analysis: 자신의 전문 장비인 GPU를 총동원해 영상을 분석한다. 이 시간 동안 미니 PC는 아무런 방해 없이 다른 요청을 처리한다.
- result upload: 분석 결과를 다시 미니 PC에게 돌려준다. “요청하신 작업 완료했습니다.”
역할과 책임이 완벽하게 분리된 공장의 생산 라인 같았다. 각자 자신의 자리에서, 자신의 전문 도구를 사용해, 약속된 결과물을 다음 단계로 넘긴다. 덕분에 한 공정이 병목이 되어도 전체 라인이 멈추지 않는다.
“그렇구나. 자원을 분리해서 각자 제일 잘하는 걸 시키는 거구나. 정말 효율적이다.”
솔라는 만족스러운 표정으로 고개를 끄덕였다. 이제 시스템의 복잡한 구조가 왜 필요했는지 명확히 이해되었다. 하지만 그녀의 시선이 ‘analysis’ 단계에 머무는 순간, 새로운 질문이 고개를 들었다.
“그런데 언니, 이 분석 작업은 GPU가 해도 몇 분씩 걸릴 수 있잖아. 만약 분석을 한참 하던 중에 이 GPU 워커가 갑자기 고장 나거나 재부팅되면 어떡해? 10분 걸리는 작업이었는데 9분째에 멈췄다면… 처음부터 다시 시작해야 하는 거야?”
4장: Auto Replay: 끊김 없는 작업 재개와 복구
솔라의 마지막 질문은 이전 장에서 겪었던 CPU 과부하의 소음만큼이나 선명하게 공간을 채우고 있었다. 10분 걸리는 작업을 9분째에 실패했을 때, 그 시간은 전부 허공으로 사라지는 걸까? 생각만 해도 아찔했다. 솔라의 굳은 표정을 본 루나는 대답 대신 조용히 자리에서 일어나 창고로 향했다. 잠시 후, 루나는 먼지가 뽀얗게 앉은 커다란 상자를 들고 와 테이블 위에 ‘쿵’ 내려놓았다. 유명 건축물의 레고 모델이었다. 상자에는 ‘부품 수: 3,829개’라고 적혀 있었다.
루나는 말없이 상자를 열어 수십 개의 비닐봉지에 담긴 부품들을 테이블 위에 쏟아냈다. 전화번호부만큼 두꺼운 조립 설명서도 함께였다. 눈앞에 펼쳐진 플라스틱 부품의 산 앞에서, 솔라가 방금 전까지 가졌던 ‘사라져 버린 9분’에 대한 걱정은, 이제 ‘앞으로 사라질 몇 시간’에 대한 막막함으로 바뀌었다.
“자, 지금부터 이걸 조립해 봐. 내가 시간을 잴게.”
“뭐? 이걸 다? 지금?”
“아니, 다는 말고. 딱 15분만.”
솔라는 어이가 없었지만, 언니의 의도를 파악하기 위해 마지못해 설명서의 첫 장을 펼쳤다. 1번부터 10번 과정까지는 쉬웠다. 비슷한 모양의 기초 블록들을 찾아 뼈대를 만드는 작업이었다. 20번, 30번 과정을 넘어가자 점점 복잡한 부품들이 등장하기 시작했다. 솔라는 작업에 완전히 몰두했다. 설명서의 그림과 눈앞의 부품들을 번갈아 보며 손을 빠르게 놀렸다.
한창 집중해서 57번 과정을 막 끝냈을 때였다. 루나가 갑자기 손뼉을 한번 ‘짝’ 쳤다.
“아, GPU 워커에 예상치 못한 에러가 발생해서 재부팅해야겠어.”
루나는 그렇게 말하며 솔라의 손에서 조립 설명서를 휙 빼앗아 들고는, 솔라가 다음 단계를 위해 미리 골라두었던 작은 부품 몇 개를 다시 부품 더미 속으로 흩어버렸다.
“뭐 하는 거야, 언니! 나 방금 57번까지 했는데!”
“알아. 근데 워커가 재부팅됐으니 어쩔 수 없어. 자, 설명서 다시 줄게. 어디서부터 다시 시작해야 할까?”
솔라는 눈앞이 캄캄해졌다. 완성된 부분은 전체 모델의 극히 일부에 불과했다. 자신이 어디까지 만들었는지 기억은 나지만(57번), 그게 설명서의 몇 페이지에 있었는지, 58번 과정에 필요한 부품은 무엇이었는지 전혀 알 수 없었다. 기억에 의존해 더듬더듬 다음 단계를 찾으려 해도, 흩어진 부품 더미 속에서 다시 시작해야 할 판이었다.
“모르겠어… 그냥 처음부터 다시 하는 게 빠를지도 몰라. 기껏 만든 거 다시 부수고…”
솔라가 힘없이 중얼거렸다. 바로 그 순간이었다. ‘처음부터 다시 시작.’ 솔라의 입에서 나온 그 말이, 조금 전 자신이 던졌던 질문과 정확히 겹쳐졌다. 9분 동안의 작업이 수포로 돌아가는 상황. 지금 이 막막함과 똑같았다.
“그럴 줄 알았어.”
루나는 웃으며 작은 메모지와 펜을 솔라에게 건넸다.
“여기, ‘상태 기록지(StoreState)‘야. 그리고 네 스마트폰은 ‘스냅샷’을 찍는 도구고. 이제부터는 설명서 10페이지를 넘길 때마다, 지금 만들고 있는 부분의 사진을 찍고, 메모지에는 ‘몇 페이지, 몇 번 과정 완료’라고 적어둬.”
솔라는 루나가 시키는 대로 다시 조립을 시작했다. 이번에는 10페이지를 완성할 때마다 진행 상황을 사진으로 남기고 메모지에 기록했다. 다시 15분이 흘렀을 때, 루나는 또 한 번 “워커 재부팅!”을 외치며 설명서를 가져갔다. 하지만 이번에 솔라는 당황하지 않았다.
그녀는 자신의 스마트폰을 들어 마지막으로 찍은 사진을 확인했다. ‘130페이지, 82번 과정 완료.’ 라고 적힌 메모도 보였다. 솔라는 루나에게서 설명서를 돌려받자마자 망설임 없이 130페이지를 펼쳤다. 그리고 사진 속 모습과 똑같이 만들어졌는지 확인한 뒤, 자연스럽게 83번 과정에 필요한 부품을 찾기 시작했다. 처음부터 다시 시작할 필요도, 어디까지 했는지 고민할 필요도 없었다. 단지 마지막 저장 지점(checkpoint)부터 다시 이어가면 그만이었다.
“아…! 알겠다!”
솔라의 눈이 빛났다.
“워커가 꺼지기 직전의 상태를 어딘가에 계속 기록해두는 거구나! 레고 조립 과정을 사진과 메모로 남기는 것처럼!”
솔라는 흥분하며 다시 기술 블로그의 문장을 찾아냈다. 이전에는 무심코 지나쳤던 한 줄이 눈에 들어왔다.
Worker 재시작 시
outputs/upload-replay/<store_id>/에서 작업을resume한다.
이제 이 문장은 단순한 파일 경로로 보이지 않았다. 솔라에게는 레고 조립 사진과 메모를 차곡차곡 모아둔 ‘앨범’의 위치로 보였다. outputs/upload-replay/<store_id>/라는 약속된 장소에 작업의 중간 결과물(스냅샷)과 상태(StoreState)를 꾸준히 저장했기에, 워커는 재부팅 후 그 앨범을 펼쳐보고 어디서부터 일을 다시 시작해야 하는지 정확히 알 수 있었던 것이다. ‘resume’이라는 단어는 단순히 ‘재개하다’라는 뜻이 아니라, 이런 견고한 복구 메커니즘 전체를 함축하는 말이었다.
“결국, 이 복잡한 구조는 ‘절대 처음부터 다시 시작하지 않기 위해’ 만들어진 거구나.”
솔라는 중얼거렸다. 요청을 잃어버리지 않기 위한 작업 큐, 자원 고갈을 막기 위한 GPU 워커 분리, 그리고 작업 과정을 잃어버리지 않기 위한 Auto Replay와 재개 기능까지. 시스템의 모든 부분이 가장 비효율적인 상황, 즉 ‘재시작’을 피하기 위해 유기적으로 맞물려 있었다.
솔라는 비로소 이 시스템의 견고함에 감탄했다. 하지만 동시에 새로운 의문이 스멀스멀 피어올랐다.
“좋아, 우리 공장 내부는 이제 정말 튼튼해진 것 같아. 작업이 중간에 끊겨도 알아서 복구하고. 그런데… 이 공장에 원자재를 공급하고, 완성품을 밖으로 내보내는 ‘도로’ 자체에 문제가 생기면 어떡하지? 예를 들어, 한 번에 너무 큰 트럭은 지나갈 수 없다는 규칙 같은 게 있다면 말이야.”
5장: Cloudflare 제한: 외부 제약이 아키텍처에 미치는 영향
솔라의 마지막 비유는 테이블 위에 보이지 않는 지도를 그려냈다. 견고하게 지어진 공장, 그리고 그 공장으로 이어지는 도로. 이전 장까지의 대화로 공장 내부의 생산 라인은 완벽하게 이해됐다. 요청을 안전하게 보관하는 작업 큐, 무거운 작업을 전담하는 전문가(GPU 워커), 그리고 작업이 중단돼도 처음부터 다시 시작하지 않게 만드는 복구 시스템까지. 모든 것이 실패를 용납하지 않으려는 치밀한 설계로 보였다.
하지만 솔라의 질문은 그 공장 밖을 향하고 있었다. “한 번에 너무 큰 트럭은 지나갈 수 없다는 규칙 같은 게 있다면 말이야.” 이 말은 시스템을 둘러싼 외부 환경, 즉 우리가 통제할 수 없는 제약에 대한 것이었다.
루나는 말없이 솔라의 노트북 옆에 놓인 빈 노트를 자기 앞으로 가져왔다. 그리고 펜을 들어 간단한 그림을 그리기 시작했다. 네모난 상자로 표현된 ‘우리 시스템(공장)’, 그리고 그 앞으로 길게 뻗은 하나의 ‘도로’. 도로는 외부 세상과 공장을 잇는 유일한 통로였다.
“네 말이 맞아. 공장이 아무리 튼튼해도, 원자재가 들어오고 완성품이 나가는 이 도로가 막히면 소용이 없지.”
루나는 도로 위에 두 종류의 차량을 그렸다. 하나는 작고 날렵한 ‘승용차’였고, 다른 하나는 거대한 ‘화물 트럭’이었다.
“승용차는 ‘설정값 좀 바꿔줘’ 같은 작고 빠른 API 요청이야. 화물 트럭은 수십 기가바이트짜리 ‘대용량 영상 파일’이고. 지금까지 네가 생각한 가장 단순한 모델은, 이 모든 차가 하나의 도로를 쓰는 거였지. 효율적으로 보이니까.”
솔라는 고개를 끄덕였다. 모든 교통을 한 길로 모아 처리하는 것. 직관적인 생각이었다. 바로 그때, 루나는 도로 중간에 톨게이트처럼 ‘차단기’를 그려 넣었다. 그리고 그 위에 큼지막하게 글씨를 썼다.
[ Cloud-Gate: 100MB 초과 화물차 통행금지 ]
“이게 네가 말한 도로의 규칙이야. 이 톨게이트는 ‘클라우드플레어(Cloudflare)’ 같은 서비스라고 생각하면 돼. 외부의 공격으로부터 우리 시스템을 보호하고, 전 세계 어디서든 빠르게 접속할 수 있도록 도와주는 고마운 존재지. 그런데 이 서비스는 자체적인 규칙이 있어. 한 번에 너무 큰 데이터를 처리하는 걸 좋아하지 않아. 그래서 보통 100MB 같은 크기 제한을 둬.”
그림을 보던 솔라의 얼굴이 굳어졌다. 문제점이 한눈에 들어왔다.
“아… 그럼 저 화물 트럭(대용량 영상)들은 톨게이트를 통과하지 못하겠네. 그냥 막히는 거잖아.”
“그것도 문제지만, 더 최악의 상황이 있어.”
루나는 펜으로 화물 트럭 한 대가 톨게이트 앞에서 옴짝달싹 못하는 모습을 그렸다. 그리고 그 뒤로 승용차들이 길게 꼬리를 물고 늘어선 정체 상황을 추가했다.
“하나의 화물 트럭이 톨게이트를 막아서면, 뒤따라오던 모든 승용차가 멈춰 서게 돼. 그냥 ‘내 정보 보기’ 같은 간단한 요청을 하려던 사용자까지, 대용량 파일 하나 때문에 아무것도 못 하고 기다려야 하는 거야. 시스템 내부는 멀쩡한데, 입구가 마비돼서 아무도 들어오지 못하는 상황이지.”
내부의 견고함만으로는 해결할 수 없는 문제. 외부 환경이 가하는 제약이 시스템 전체를 마비시킬 수 있다는 사실을 보여주는 명확한 그림이었다. 모든 것을 하나의 경로로 처리하려던 단순한 생각이 얼마나 위험한지 깨닫는 순간이었다.
“그럼… 길을 새로 만들어야겠네.”
솔라가 중얼거리듯 말했다. 그녀는 루나의 펜을 가져와, 기존 톨게이트를 우회하는 새로운 ‘비포장도로’를 그렸다. 이 길은 오직 화물 트럭만을 위한 전용 도로처럼 보였다.
“승용차들은 빠르고 안전한 기존 고속도로를 쓰게 하고, 크고 무거운 화물 트럭은 톨게이트가 없는 별도의 길로 들어오게 하는 거야. 그럼 서로 방해할 일이 없잖아.”
솔라의 그림을 본 루나는 조용히 고개를 끄덕이며, 다시 기술 블로그의 한 문장을 가리켰다.
Cloudflare의 대용량 요청 제한 때문에 작은 UI·API 조회와 미디어 업로드 경로도 분리했다.
이 문장은 더 이상 복잡한 시스템의 일부를 나열하는 건조한 설명이 아니었다. 솔라가 방금 그림으로 그려낸 문제와 해법, 즉 외부 제약에 대한 명쾌한 ‘대응 전략’ 그 자체였다. 시스템을 설계한다는 것은 단순히 내부의 논리를 쌓는 것뿐만 아니라, 시스템이 발 딛고 있는 외부 세계의 규칙과 제약을 이해하고, 그에 맞춰 현명하게 길을 내는 일임을 깨달았다.
솔라는 잠시 생각에 잠기더니, 빈 노트의 새 페이지를 펼쳤다.
“이제 전부 연결되는 것 같아.”
그녀는 스스로에게 설명하듯, 새로운 다이어그램을 그리기 시작했다. 더 이상 루나의 도움이 필요하지 않았다.
- 사용자가 요청을 보낸다. 하지만 길은 두 갈래다. 작은 API 요청은
api.our-service.com으로, 거대한 영상 파일은upload.our-service.com으로. (외부 제약 대응) api.our-service.com으로 온 요청은 미니 PC의 API 서버가 받는다. 서버는 요청 내용을analysis_jobs큐에 던져 넣고, 즉시 사용자에게 “접수 완료!”라고 응답한다. 사용자는 더 이상 기다릴 필요가 없다. (동기 API의 한계 극복)analysis_jobs큐는 요청이 사라지지 않도록 안전하게 보관하는 금고 역할을 한다. (비동기 작업 큐)- 한가해진 GPU 워커가 큐에서 새 작업을 꺼내(claim),
upload.our-service.com을 통해 들어온 원본 영상을 가져와 분석을 시작한다. 이 과정은 미니 PC에 아무런 부담을 주지 않는다. (자원 특화 워커) - 워커는 분석 중간중간 자신의 작업 상태를
outputs/upload-replay/폴더에 기록한다. 혹시라도 전원이 나가도, 처음부터가 아니라 마지막 지점부터 작업을 재개할 수 있다. (작업 복구 메커니즘)
솔라는 펜을 내려놓고 자신이 그린 그림을 바라보았다. 처음에는 이해할 수 없었던 복잡한 구조의 모든 조각들이, 각자의 ‘왜’를 가지고 제자리에 완벽하게 맞춰져 있었다. 그것은 더 이상 남의 기술 블로그에 있는 난해한 아키텍처가 아니었다. 끈질긴 질문 끝에 얻어낸, 스스로 납득할 수 있는 견고한 시스템 설계 원칙이었다.