공개 데이터를 20여 개 플랫폼에서 가져와야 한다면, 처음에는 거의 반드시 추상화부터 시작하게 된다. 통합 Post 하나, 통합 User 하나를 만들고 모든 플랫폼 응답을 거기에 매핑하는 방식이다. bilibili 동영상, tiktok 동영상, zhihu 답변, linkedin 게시물은 얼핏 보면 모두 ‘콘텐츠 하나와 작성자 한 명’으로 보인다. 처음 세 플랫폼까지는 이 설계가 깔끔하게 느껴진다. 하지만 20번째 플랫폼쯤 가면 그 추상화가 오히려 발목을 잡는다.
결국 나는 통합 모델을 만들지 않았다. 20여 개 플랫폼과 200여 개 명령어를 다루면서 끝까지 살아남은 구조는, 겉으로는 더 단순해 보이는 ‘플랫폼은 각자 알아서 처리한다’는 분리 방식이었다.
통합 모델은 어떻게 무너지는가
한순간에 깨지지는 않는다. 여덟 번째 플랫폼쯤 붙이면 Post에는 선택 필드가 열두 개쯤 달려 있다. 어떤 플랫폼에는 danmaku 수가 있지만 어떤 곳에는 없고, ‘게시 시각’도 어떤 곳에서는 초 단위 타임스탬프지만 다른 곳에서는 ‘3 days ago’ 같은 문자열이다. 20여 개까지 늘어나면 통합 모델은 사실상 기능을 잃는다. 컴파일 오류가 나는 식의 붕괴가 아니다. 더는 아무것도 절약해 주지 못한다는 뜻이다. 이후 코드는 모두 ‘이 플랫폼이 이 필드를 채웠는가’를 먼저 판별해야 한다. 그 분기 로직은 원본 응답을 직접 읽는 것보다 길어지고, 통합 계층은 일을 쉽게 만드는 대신 우회해야 하는 장애물이 된다. 쓰기 쪽도 마찬가지다. 새 플랫폼을 붙일 때마다 기존 플랫폼에 맞춰 만든 상자에 필드를 억지로 끼워 넣으려고 다시 그 계층으로 돌아가게 된다.
플랫폼별로 독립된 바운디드 컨텍스트를 둔다
유지되는 구조는 반대 방향이다. 통합 모델을 추상화하지 말고, 플랫폼이 자기 영역을 책임지게 한다. 카탈로그에서 각 플랫폼은 <platform>_reverse/ 컨텍스트 하나이며, 다음 네 가지를 소유하고 외부에 넘기지 않는다.
입력 검증. 이 플랫폼의 ID 형식이 무엇인지, 어떤 파라미터 조합이 유효한지는 해당 플랫폼만 안다.
프로토콜. HTTP 직접 호출인지, 로컬 JS 서명 코드 일부를 실행해야 하는지, 어떤 도메인과 헤더를 써야 하는지는 모두 플랫폼 내부 사항이다.
서명. 플랫폼마다 서명 방식은 크게 다르다. 이를 하나의 공통 signer에 밀어 넣으면 if-else로 가득한 괴물만 만들어진다.
응답 정규화. 원본 응답은 해당 플랫폼이 소유한 구조로 정리한다. 전역 통합 구조에 억지로 맞추지 않는다.
가장 오해하기 쉬운 것은 네 번째다. ‘통합 모델이 없다’는 말은 ‘정규화하지 않는다’는 뜻이 아니다. 모든 플랫폼은 당연히 응답을 정규화한다. 다만 목표 형태를 각 플랫폼이 직접 정의할 뿐, 공통 모델이 강제하지 않는다. 진짜 같은 대상일 때만 통합하면 된다. 한 플랫폼 안의 두 엔드포인트가 게시물 구조를 공유한다면 그것은 실제로 같은 도메인 객체이므로 통합해도 된다. 문제는 플랫폼 내부의 통합을 플랫폼 경계 밖까지 끌고 가는 데 있다.
공유 계층에는 진짜 공통인 기능만 남긴다
그렇다면 공유 계층에는 무엇을 넣어야 할까. 겉모습만 비슷한 기능이 아니라, 모든 플랫폼에서 실제로 동일하게 동작하는 기능만 둬야 한다. 내 공유 계층에는 세 가지뿐이다.
인터페이스 읽기 모델. 각 플랫폼의 argparse 선언에서 통합 기능 카탈로그를 만든다. 여기서 통합하는 것은 명령어를 발견하고 설명하는 방식이지, 명령어가 반환하는 데이터가 아니다. 전자는 플랫폼을 가로질러 정말 공통이지만 후자는 플랫폼별로 다르다. (‘선언이 곧 인터페이스’라는 관점은 interface-as-code 글에서 더 자세히 다뤘다.)
로컬 루프백 전송. 인증된 요청은 로컬 WebSocket 세션 서비스를 거친다. 이 계층은 모든 플랫폼을 동일하게 다루며, 어느 플랫폼의 비즈니스 필드도 건드리지 않는다.
디스패치 진입점. 플랫폼을 찾아내고 명령어를 해당 컨텍스트로 넘긴다. 그 이상은 하지 않는다.
판별 기준은 간단하다. 공유 계층에 들어가려면 모든 플랫폼에서 정말 똑같이 동작해야 한다. 전송, 디스패치, 인터페이스 설명 생성은 그렇다. 반면 bilibili의 ‘콘텐츠 하나’와 linkedin의 ‘콘텐츠 하나’는 행동 면에서 전혀 같지 않으므로 넣지 않는다. ‘비슷해 보인다’는 추상화에서 가장 위험한 함정이다. 동영상과 동영상은 닮아 보이니 통합하고 싶어진다. 그러나 표면적 유사성은 행동의 동일성이 아니며, 이를 공유 가능한 도메인 모델로 취급하는 순간 통합 모델은 무너지기 시작한다.
명령어 분포를 보면 어디에 추상화가 이득인지 보인다
통합 모델이 여전히 필요하다고 생각한다면 실제 명령어 분포를 보면 된다. 플랫폼은 22개, 명령어는 241개지만 분포는 극단적으로 고르지 않다.
플랫폼 | 명령어 수 |
|---|---|
tiktok | 34 |
bilibili | 26 |
18 | |
zhihu | 18 |
douyin | 17 |
xiaohongshu | 16 |
나머지 16개 플랫폼 | 각 1~13개 |
상위 6개 플랫폼의 명령어는 합계 129개로 전체의 절반을 넘는다. 나머지 절반은 16개 롱테일 플랫폼에 나뉘며, 그중 상당수는 명령어가 두세 개뿐이고 하나만 있는 곳도 있다.
이 분포가 추상화의 경제성을 결정한다. 통합 모델의 비용은 고정이다. 모든 통합 작업자는 필드를 채워야 하고, null을 확인해야 하며, 모델을 우회하는 코드를 작성해야 한다. 반면 이득은 플랫폼별로 분산된다. 명령어가 두세 개뿐인 롱테일 플랫폼에서는 추상화의 이득이 오히려 음수가 된다. 통합 모델에 맞추기 위해 작성한 어댑터 코드가 해당 플랫폼의 모든 비즈니스 코드보다 길어지기 때문이다.
구현체 하나를 위해 추상화를 미리 만들지 않는다
이 분포를 따르다 보면 규칙이 하나 더 나온다. 구현체가 하나뿐이라면 추상화를 예약하지 말아야 한다. 플랫폼 구현이 하나뿐인데도 ‘나중에 다른 구현이 생길 수 있다’며 repository, factory, 인터페이스 계층을 추가하지 않는다. 플랫폼을 추가한다는 것은 <platform>_reverse/ 컨텍스트 하나를 추가하는 일이다. 먼저 공통 베이스 클래스를 수정할 필요가 없다.
인터페이스 계층의 가치는 여러 구현체를 서로 교체할 수 있게 하는 데 있다. 구현체가 하나라면 그 가치는 0이고 유지보수 비용은 양수다. 존재하지 않는 두 번째 구현을 위해 자리를 비워 두는 일과, 존재하지 않는 크로스 플랫폼 공통성을 위해 자리를 비워 두는 일은 같은 실수다. 크로스 언어 마이그레이션에서도 이를 다시 확인했다. 기존 레지스트리의 수백 개 명령어 중 일부 묶음은 의도적으로 마이그레이션하지 않았고, 빈 스텁이나 호환 프록시도 남기지 않았다. 빈 껍데기가 공백보다 비용이 크기 때문이다. 다음 사람이 무언가 이미 존재한다고 착각하게 만든다. 미리 만들어 둔 추상화도 다르지 않다.
크로스 플랫폼 정규화도 모델에 플랫폼별 맥락을 준다
‘플랫폼별 분리, 통합 모델 없음’이라는 원칙은 모델로 정규화할 때도 그대로 적용된다. 20여 개 플랫폼의 원본 응답을 분석 가능한 구조로 정리하려면 모델에 맡기고 싶어진다. 이때도 코드 계층에서와 똑같은 실수를 하기 쉽다. 통합 스키마를 정의한 뒤, 각 플랫폼의 원본 JSON에 ‘이 스키마로 매핑하라’고 덧붙이는 방식이다. 이 방법은 잘 작동하지 않는다. 모델은 bilibili의 재생 수 필드와 tiktok의 재생 수 필드가 같은 의미인지 알지 못한다. 최소 공통분모 스키마로 강제하면 플랫폼에 중요한 필드를 버리거나, 절반만 맞는 값으로 채우게 된다.
올바른 방식은 플랫폼별 맥락을 함께 제공하는 것이다. 모델에 ‘이것은 bilibili 응답이고, 각 필드는 이런 의미이며, 이 플랫폼에서는 이런 형태를 원한다’고 알려 준다. 정규화는 한 번에 한 플랫폼씩 처리하고, 플랫폼 간 병합은 분석 계층에 맡긴다. 이 과정은 여러 단계로 나뉘며, 각 단계는 모델에 서로 다른 능력을 요구한다.
단계 | 필요한 역량 | 선택 | model id |
|---|---|---|---|
플랫폼 하나의 전체 원본 응답 구조 읽기 | 긴 컨텍스트, 전체 응답과 필드 설명을 한 번에 수용 | Kimi K3 |
|
정규화 경계 설정: 실제 공통 필드와 플랫폼별 필드 구분 | 강한 추론 능력, 과도한 통합을 억제 | Claude Opus 5 |
|
플랫폼별 필드 대량 추출, 항목 단위 매핑 | 저렴한 비용, 높은 동시성으로 수백~수천 회 호출 | Claude Sonnet 5 |
|
두 플랫폼에서 같은 이름을 가진 필드가 왜 일치하지 않는지 설명 | 중간 수준의 추론, 필드를 근거로 차이 설명 | GPT-5.6 Sol |
|
모델을 바꿨을 때 결과 차이가 눈에 띄는 단계는 두 번째뿐이다. 여기서 검증하는 것은 두 필드가 실제로 같은 대상이 아니라는 사실을 인정할 수 있느냐다. 알고리즘 계열 식별의 반증 섹션과도 같다. 약한 모델은 ‘통합하라’는 힌트를 그대로 따르지만, 강한 모델은 경계가 어디인지 짚어 낸다.
진짜 장벽은 모델 전환 비용이다
네 가지 티어는 세 벤더, 세 SDK, 세 인증 방식, 세 오류 형식에 걸쳐 있다. 단계별로 모델을 바꾸기 위해 클라이언트를 세 번 다시 작성할 가치는 없다. 그래서 대부분은 전체 과정에 한 티어만 사용한다. 특히 ‘경계 설정’ 단계에서는 쉽게 무너지는 티어를 쓰게 되고, 20여 개 플랫폼에서 다시 붕괴할 스키마를 만들게 된다.
AIReiter는 이 계층을 평탄화한다. 키 하나와 OpenAI 호환 인터페이스 하나로 네 티어를 모두 쓸 수 있으며, 요청 본문의 model 필드만 바꾸면 전환된다.
# Set the normalization boundary: the reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
-H "Authorization: Bearer $AIREITER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-5",
"messages": [{"role": "user", "content": "<one platform response sample + have it mark which fields are platform-specific>"}]
}'
# Extract fields per platform in bulk: change the model field, leave the rest
# "model": "claude-sonnet-5"
# Field-difference attribution:
# "model": "gpt-5.6-sol"
이미 OpenAI SDK를 쓰고 있다면 base_url만 https://aireiter.com/api/v1로 지정하면 되고, 나머지는 바꿀 필요가 없다. Anthropic SDK에서는 같은 키로 POST /api/v1/messages를 호출하면 된다.
가격 기준으로 Claude 모델은 정가 대비 30% 할인되고, GPT 모델은 반값이며, Kimi K3도 같은 키로 호출할 수 있다. 이 할인은 비용이 가장 큰 구간에 적용된다. 플랫폼별 필드 대량 추출은 호출 밀도가 가장 높은 단계다. 20여 개 플랫폼에 각각 수백~수천 개 레코드가 있고, 레코드당 한 번씩 호출하며, 가장 저렴한 Sonnet에 다시 30% 할인을 적용한다. Kimi K3로 긴 전체 응답을 읽는 작업도 입력당 수십만 토큰이 필요하므로 또 다른 비용 구간이다. 반면 경계를 설정하는 추론 티어는 호출 수가 적어 비용이 거의 들지 않는다.
가입 없이 사용해 보기: 먼저 플랫폼 하나의 응답을 직접 넣어 보고, 모델이 차이를 솔직하게 표시하는지 아니면 성급하게 평탄화하는지 확인한 뒤 연동 여부를 결정하면 된다.
정리
크로스 플랫폼 수집에서 통합 Post/User를 추상화하는 선택은 작은 규모에서는 매력적이지만, 20여 개 플랫폼에 이르면 필연적으로 무너진다. 비용은 고정인데 이득은 플랫폼별로 분산되고, 명령어 분포도 극단적인 롱테일 구조이기 때문이다. 유지되는 설계는 플랫폼마다 바운디드 컨텍스트를 하나씩 두고, 각 컨텍스트가 입력 검증·프로토콜·서명·응답 정규화를 책임지게 하는 방식이다. 공유 계층에는 전송, 디스패치, 인터페이스 설명 생성처럼 모든 플랫폼에서 실제로 동일하게 동작하는 것만 둔다. 겉보기만 비슷한 도메인 모델은 넣지 않으며, 구현체 하나나 존재하지 않는 공통성을 위해 추상화를 미리 마련하지도 않는다. 모델을 사용할 때도 원칙은 같다. 통합 스키마를 던지는 대신 플랫폼별 맥락으로 정규화하고, 크로스 플랫폼 병합은 분석 계층에서만 수행한다. 전체 4단계 워크플로에서는 네 티어 분리를 더 자세히 다루며, 하나의 통합 인터페이스로 연결하면 전환 비용은 더 이상 이를 쓰지 않을 이유가 되지 않는다.