요즘 시대에 이정도 요구사항은 AI와 함께라면 전혀 두렵지 않지만, 개발을 접하고 지금까지 파일을 다루는 일을 크게 해오지 않았던 터라, 기록 형태로 남겨보려고 한다.
정의
사용자는 여러 명이 올린 영상들을 하나하나 클릭해서 다운로드 받아야 했다. 이에 불편함을 느껴 일괄 다운로드를 받고싶어 한다.
추가적인 정보는 다음과 같다.
숏폼 형태의 영상으로, 영상 하나당 최대 300MB 제한이다.
영상의 개수는 최대 100개, 약 30GB이다.
앞으로 개수는 더 늘어날 수 있다.
기존 단건 다운로드 방식은 public URL로 S3에서 직접 다운로드 받는 방식이다.
생각 정리
얼마 안되는 개발 짬빱으로 1차로 생각을 정리했다.
압축
스트리밍
멱등한 비동기 처리
여튼 효율적으로, 빠르게 다운로드가 가능하게 구현하고 요청과 분리해서 202를 떨어뜨리자. 그럼 effectively-once 하게 다운로드가 가능하지 않을까? (이탈·중복·실패에도 정확히 한 번 완료되도록)
영상도 파일이다
알다시피 파일 다운로드라고 딱히 특별하지는 않다. HTTP GET 요청의 응답 body로 온 바이트를 화면에 렌더링할지 디스크에 저장할지를 결정할 뿐이다. 응답 헤더를 통해서 말이다.
Content-Type을 통해 video/mp4 등의 inline 실행인지, application/octet-stream 등의 저장 방식을 결정하거나
Content-Disposition을 통해 다운로드를 강제할 수 있다. Content-Length 헤더가 있다면, 진행률 바를 그릴 수도 있고 말이다.
이 기본적인 매커니즘은 S3 저장, 다운로드 방식 위에 있는 기본적인 네트워크 개념이다.
촬영되는 영상의 성질
촬영 기기의 카메라 센서는 raw 단위의 프레임을 만들지만 촬영 기기는 raw 단위 프레임을 이어서 저장하기에 버겁다. 그렇기 때문에 하드웨어 인코더 칩 등으로 실시간 압축하여 파일로 쓴다. 글을 작성하는 2026년 기준 HEVC라는 비디오 코딩 기술이 안드로이드와 아이폰 기기에 탑재되어 있다.
HEVC(High Efficiency Video Coding/H.265)는 고효율 비디오 코딩 기술이다. 기존 대비 40~50% 수준의 파일 크기로 고품질 영상을 유지할 수 으며, 4K 및 8K와 같은 초고화질(UHD) 영상의 용량을 획기적으로 줄여 스트리밍과 저장에 널리 사용된다.
아하? 영상이라 파일 크기가 클 수 밖에 없겠지만, 이미 1차로 최대한 품질을 보존한 압축 기법을 통해 저장이 되는구나?
그렇다면 내가 이걸 더 압축한다면 별도 비디오 코딩 기술을 사용해야하고 그것을 유지보수하기 위한 러닝커브가 존재하겠다.
그리고 애초에 고품질 압축을 추가 압축했을 때 광고 집행이나 2차 활용을 보장할 수 있는 품질인가?
그래서 어떻게 개선?
S3도 사실 큰 틀에서 다르지 않다. 어떻게 저장소 따위(?)가 더 상위 개념인 네트워크의 본질을 벗어날 수 있을까.
S3의 코어는 분산 오브젝트 스토어에 있다. 인증, 멀티파트 등등..
다운로드 요청과 처리를 분리해서, 완료된 파일을 사용자가 이탈할 때에도 다운로드 할 수 있도록 지원하고
중복 요청 방지를 위해서 멱등키로 dedup, 같은 키의 zip이 있으면 S3에서 재생성 없이 presigned URL만 새로 서명했다.
백그라운드 워커가 S3 GET을 통해 영상을 읽는다. 다행히 같은 리전이라 트래픽이 무료이며
영상 파일을 묶어서 멀티파트 업로드를 통해 zip 파일로 업로드해서 presigned URL로 다운로드 시킨게 전부..
되게 별게 없다
글을 쓰다보니까 문득 되게 기술적으로는 별 것 안했구나? 라는 생각이 든다.
광고주의 문제 정의를 하고, 기존 지표를 통해 광고주들이 크리에이터의 소재들을 일일이 번거롭게 다운로드 하는 것을 확인했다.
KPI의 가장 큰 축이 광고주의 리텐션인데, 리텐션 지표 상승의 엄청난 임팩트가 있는 개선이라고 보기는 어렵다만...
임팩트가 높은 시도와 병렬적으로 유저의 행동을 데이터로 보고 부정적인 행동들을 빠르게 끊을 수 있는 시도를 계속 하다보면 목표 달성에 더 빠르게 가까워지지 않을까..?
뭔가 기술적인 내용을 쓰려고 시작했는데 제품적인 글로 마무리 되는 것 같아 찜찜하게 마무리되는 글..
두 방식 모두 복잡한 검색 조건을 표현하기에는 훨씬 편하다. 하지만 HTTP 의미론 관점에서는 찜찜한 지점이 남는다.
우리가 원하는 것은 서버 리소스를 생성하거나 변경하는 것이 아니다. 조건이 복잡해서 URL query string만으로 표현하기 어렵기 때문에 request body가 필요할 뿐이다. 그런데 HTTP method는 POST다.
POST가 항상 리소스 생성을 뜻하는 것은 아니지만, RFC 9110 기준으로 POST는 safe하지도, idempotent하지도 않은 메서드로 취급된다. 즉 애플리케이션 레이어에서는 읽기 요청이라고 알고 있어도, HTTP 레이어와 중간 인프라 입장에서는 이 요청이 읽기인지 쓰기인지 method만 보고 판단하기 어렵다.
HTTP Method의 safe와 idempotent에 대해 다룬 글이 있으니 필요하다면 참고하길 바란다.
POST /search와 GraphQL POST는 실무적으로 좋은 해법이다. 복잡한 조건을 JSON으로 표현할 수 있고, typed client나 schema validation을 얹기도 쉽다. GraphQL은 한 걸음 더 나아가 클라이언트가 필요한 field shape까지 operation으로 표현할 수 있다.
하지만 HTTP 관점에서는 어떨까?
요청한 애플리케이션의 의미는 읽기이고, 조건 표현을 위해 request body가 필요하다. HTTP method는 POST라서 safe, idempotent의 의미가 method에 드러나지 않는다. 문제는 조건을 어디에 담을 것인가 만이 아니라, 그 요청이 HTTP 레이어에서 어떤 의미로 보이는가를 같이 생각해보아야 한다.
GraphQL도 마찬가지로 애플리케이션 레이어에서는 읽기라고 말하지만 HTTP method만 보면 그냥 POST로 보인다. 그래서 GraphQL 생태계에서는 persisted query 같은 패턴도 생겼다. 긴 query document를 매번 보내는 대신 hash를 URL에 싣거나 CDN cache key를 직접 조정하거나 POST body를 해석하는 애플리케이션 레벨 캐시를 둔다. 하지만 이런 방법들이 HTTP method의 의미 자체를 바꿔주지는 않는다.
그럼 GET에 body는?
그럼 의미론적으로 읽기 요청인 GET에 body를 실어 보내는 방법은 왜 안됐을까?
GET에서도 당연히 실어 보낼 수는 있다. 하지만 RFC 9110에서 요청 content에 대해 일반적으로 정의된 의미가 없다고 설명한다. 서버가 GET body를 검색 조건으로서 해석하고 사용해야 하는지, 무시하고 거부해야 하는지 표준으로 정해주지 않는다.
클라이언트, 서버, 프록시, CDN, 브라우저, 캐시는 모두 method를 보고 이 요청을 어떻게 다룰지 판단하는데, GET body에 자체 규칙을 얹으면 내가 관리하는 서버에서는 동작할 수 있지만, 중간 경로 전체가 같은 의미를 공유한다고 기대하기 어렵다.
QUERY는 POST처럼 body를 실을 수 있지만, GET처럼 safe하고 idempotent한 query 요청을 표현할 수 있다.
RFC 10008
이런 특성을 가진 QUERY라는 method의 의미는 단순 GET 요청에 body를 실을 수 있는 것보다는 더 많은 가치가 있을 것으로 보인다.
연결이 끊겼을 때 재시도를 고려할 수 있다.
캐시가 safe한 query response로 판단될 수 있다.
gateway나 WAF 읽기 요청과 쓰기 요청을 더 명확하게 구분할 수 있다.
observability 관점에서 method 기준으로 요청의 성격을 해석하기 쉬워진다. (기존 POST의 오용 감소)
QUERY, 사용해 볼 수 있을까?
QUERY가 표준화됐다고 해서 내일부터 모든 API에 바로 적용하기는 어렵다.
우선 중간 경로가 method를 알아야 한다. 오래된 proxy, WAF, CDN, gateway는 알 수 없는 method를 거부하거나 별도 allowlist로 관리할 수 있다. 서버 framework가 QUERY 라우팅을 지원하더라도 실제 인터넷 경로에서는 막힐 수 있다.
또한 브라우저의 CORS에서 QUERY는 safelisted method가 아니다. RFC 10008도 CORS를 구현하는 user agent에서 QUERY 요청은 preflight가 필요하다고 언급한다. 브라우저 클라이언트에서 쓸 때는 OPTIONS 처리와 Access-Control-Allow-Methods 설정까지 같이 봐야 한다.
그리고 캐시가 request content를 cache key에 포함해야 한다. 이걸 모르는 캐시는 QUERY response를 아예 저장하지 않거나, 저장하더라도 잘못 재사용할 위험이 있다. QUERY의 장점을 제대로 얻으려면 cache layer가 RFC 10008의 규칙을 구현해야 한다. RFC 10008은 QUERY request의 cache key가 request content와 관련 metadata를 포함해야 한다고 명시한다.
마지막으로 지원 여부를 발견하는 절차가 필요하다. RFC 10008은 Accept-Query response header를 정의한다. 서버는 어떤 media type의 query content를 받을 수 있는지 알릴 수 있다.
또는 OPTIONS 응답의 Allow header로 QUERY 지원 여부를 드러낼 수 있다.
Allow: GET, QUERY, OPTIONS, HEAD
현실적인 도입 순서는 공용 브라우저 API보다 내부 API나 gateway-controlled API 쪽일 가능성이 높다. 클라이언트, 서버, proxy, cache 설정을 한 팀이 함께 통제할 수 있어야 QUERY의 의미와 이점을 끝까지 보존할 수 있기 때문이다.
마치며
QUERY는 POST이지만 GET으로 사용했던 기존의 레거시 API들을 대체할 수 있는 것처럼 보인다. 그렇다고 당장 기존 API를 바꿀 필요는 없다. POST는 여전히 실용적이며 모든 인프라가 POST를 알고 있고, framework와 client 지원도 안정적이다. 캐시가 꼭 필요하지 않거나, 애플리케이션 레벨에서 캐시/재시도/중복 방지를 이미 잘 처리하고 있다면 POST 검색 API를 유지하는 편이 더 현실적일 수 있다.
다만 RFC 10008이 의미 있는 이유는, 우리가 그동안 관습으로 처리하던 요구사항에 HTTP 차원의 이름을 붙였다는 데 있다. GET은 복잡한 request body를 표준 의미로 다룰 수 없으며, POST는 safe, idempotent하지 않다. 그 문제 정의를 표준으로 정의했다는 것에 의미가 있다고 본다.
그래서, POST지만 GET으로 사용되던 레거시를 완전 대체하기보다는 아직은 생태계 지원을 확인하면서 조심스럽게 봐야 하지만, 검색 API와 GraphQL, 분석 API를 설계할 때 앞으로 꽤 자주 언급되고, 고려할 대상으로 보이지 않을까? 싶다.
티빙, 패스트캠퍼스, 모두의 창업 등 이어지는 개인 정보 유출과 최근 개발 방식에 대한 회고
회고2026. 6. 22. 22:14
728x90
728x90
티빙, 패스트캠퍼스, 모두의 창업 등 최근 연달아 대규모 개인정보 유출이 발생하고 있다. 개인의, 조직의 실수로 데이터가 유출되는건 생각보다 빈번하겠으나, 어느정도 대규모 조직이나 개발 조직이 두터운 곳들에서도 정말 기본적인 것들을 지키지 않은데에서 오는 파장이 엄청나다고 느끼는 요즘이다.
오늘도 AI를 신뢰하지 않고 언제나 불매를 할 준비가 되어있다 라는 마인드를 가지고 약간의 원인 해부와 나도 개선할 점을 조금 느끼는 것 같아 회고를 남긴다.
시크릿 키 유출
티빙은 DB에 직접 침입해서 탈취당했다고 한다. 그에 따른 사후 조치로는 AWS 액세스 키를 폐기하고, 깃허브 자격증명을 교체했다고 했다. 정확한 원인을 밝히지 않았으니, 티빙은 감히 추측하지 않도록 하겠다.
패스트캠퍼스는 소위 인강 플랫폼이다. IT/테크 쪽의 플랫폼인줄 알았으나 최근 인프런의 행보와 유사하게 다양한 분야로 확장한 모습이었다.
패스트캠퍼스의 유출 경로는 깃허브 레포의 PAT인지, 깃허브 마스터 계정의 키가 탈취된 채로 한 달 가량 노출되어 있었다고 한다.
BOLA/BFLA
모두의 창업을 잘은 모르지만, 사용자의 창업 아이디어를 PMF로 삼는 플랫폼이다. 지인 분께서 창업 아이디어를 올렸으니 좋아요를 눌러달라고 했던 경험이 있다. 그 경험으로 미루어보아, 무언가 아이디어 경쟁을 할 수도 있겠다는 생각이다.
5,000개 가량의 아이디어 요약, 심사평 등이 유출되었다고 하는데, 원인은 명확하나 왜 그랬는지는 정말 모르겠다.
개발을 하다보면 기본적인 인증/인가 처리는 고려하지 않으려고해도 안할 수가 없는 기본적인 문제이기 때문에, 정말 빠르게 개발을 해야 했거나 소위 빌더라 불리우는 바이브 코딩의 영향이 아닌가 싶다.
헤더에도 써놨지만 Broken Object Level Authorization, Broken Function Level Authorization이라 불리는 API 보안 취약점이 원인이다. 요새 기술 스택이 python이니, python 웹 프레임워크를 예시로 간단하게 코드를 작성했다.
@app.get("/api/applicants/{id}")
def get_applicant(id: int):
a = db.get(Applicant, id)
return a.__dict__
<span>{maskEmail(applicant.email)}</span>
API를 위와 같이 구성하고, UI에서만 미노출하는 형태로 개발이 되었다고 한다.
직접 HTTP Request를 보이면 그대로 드러나게 구성을 해두었던 정말 말도 안되는 문제로 소중한 아이디어들이 유출되었다.
AI Slop
GPT5.5, Opus4.8 Fable4.8 시대에 무슨 AI Slop인가 싶겠지만, 모델의 성능에서 오는 조잡한 결과물에 대한 얘기가 아니다.
LLM이 컨텍스트를 충분하게 얻지 못한 이유로 여러 인과관계들을 고려하지 못한 단순 구현 코드를 싸질렀다고 해서 책임을 LLM이 지는 것은 아니다. 실제 업무에서 사람들과 협업 했을 때, 서로 상대방의 책임으로 떠넘긴 결과물이 과연 좋을까.
마찬가지로, LLM을 오케스트레이션 한 결과물을 내가 100% 검수할 수 있는 능력이 없다면, 과연 이 LLM을 가지고 기술적인 요구사항을 구현하는게 맞을까. 구현의 허들은 점점 낮아지지만 책임의 영역은 그대로 보존된다.
이 사고들이 모두 AI 때문에 벌어졌다고 말할 수는 없다. 다만 요즘 개발 환경에서는 구현의 허들이 낮아진 만큼, 검증하지 않은 결과물을 운영 환경에 올리는 속도도 같이 빨라졌다. LLM이 만든 코드든 사람이 급히 짠 코드든, 인증·인가·시크릿 관리의 책임은 결국 배포한 사람과 조직에 남는다. 일련의 사례들을 보고 주니어 개발자인 내가 내린 결론은, 본인이 책임질 수 있는 역량을 키우는 것이 요즘의 개발에서 개발자로 살아남는 가장 중요한 첫 단추이지 않을까 라는 생각이 들었다.
시도하면서 배우는 것들이 있기 때문에, 시도 자체에는 부정의 여지가 단 1%도 존재하지 않는다. 하지만, 나는 이런 레퍼런스들이 성공 사례라고 생각하지 않는다. 개인 혹은 본인이 속한 조직에서 여러 연관 관계에 따른 사이드 이펙트가 있음에도 이를 선택한 설명이나 도입 전후의 공개 가능한 지표들을 충분히 제공하지 않은 채로 무지성 AX를 외치고 있다고 생각한다. 이게 무지성 뇌동매매 후 화성 갈끄니께~~ 를 외치는 것과 뭐가 다를까?
나도 이런 부분에서 자유롭지 않다. 한 조직의 개발자로서 AI로 뭔가 빨라지긴 한 것 같은데, 정말 나아진 게 맞나? 라는 생각이 끊임 없이 든다.
정말 생산성이 향상 됐나? 조직에 어떤 영향을 어떻게 끼쳤나? 그래서 조직의 매출이나 기타 지표들은 상승 곡선을 그리고 있나?
그 무엇도 명쾌하게 설명할 수 없는 것 같은 답답한 상황인 것 같다. 과도기를 어느 정도 넘어가고 있다고 생각하는데, 이 시점에서 어느정도 정리할 수 있는 생각들은 정리할 필요가 있다고 판단이 섰다.
개인은 빨라진 것 같다. 그렇다면 조직은?
개인의 생산성이 올라간 건 부정하기 어렵다. 예전 같으면 누군가에게 부탁하거나 미뤄뒀을 일을, 각자가 필요한 걸 직접 만들기 너무 쉬워졌다. 거기까진 분명한 이득이다.
문제는 그 다음이다. 조직의 경우 직접 만든 그 무언가가 이미 운영되고 있는 제품에 닿는 순간, 결국 관련 담당자의 리뷰가 반드시 필요한 상황에 놓인다. 물론 이 부분들까지 관련 담당자들의 컨텍스트들을 잘 정리해서 자동화 할 수 있다. 리뷰, QA, 배포 등 모든 영역에서 가능하다. 하지만 그게 항상 옳지만은 않다는 것을 모두가 알고 있기 때문에 Human-Loof, Human-in-the-Loof 라는 용어도 생겨났다고 생각한다. 결국 필요 시에는 담당자가 들여다보고 검증하고 책임질 수 있는 형태로 다듬어야 한다는 말이다. 그럼 이건 생산성이 향상되었다고 할 수 있을까? 누군가는 생산성이 향상되었다고 할 수 있겠지만, 이걸 검증 해야하는 사람의 입장에서는 일이 더 늘어났다고도 볼 수 있다. 그렇다면 이게 조직 관점에서 더 이득일까? 라고 물으면 모두가 그렇다. 라고 할 수 있을까? 전체의 관점에서는 비슷한 자리에서 일의 모양만 바뀐 것 같기도 하다.
조직 내 개발자의 생산성은 정말 올랐나?
그럼 우리 조직으로 문제를 가져와서, 개발자 본인의 생산성은 정말 올랐을까?
우리와 비슷한 유사 스타트업들도 스프린트 단위의 업무를 진행하는데, 예나 지금이나 마찬가지로 스프린트의 일정을 준수하며 개발하는 건 똑같다. 달라진건 직접 코드를 작성하는 것에서, 코드를 LLM이 더 잘 작성할 수 있게 PRD를 다듬고, 산출물을 리뷰하는 역할의 차이가 전부이다.
도대체 어떤 부분이 진짜로 향상된 것일까? 개인이 LLM을 다루는 스킬이 향상된 것을 가지고 개인의 자산으로 남았으니 잘했다고 칭찬을 해야할까? 그것도 잘 모르겠다. 어쨋든 시대의 변화에 따라 LLM을 다루는 스킬도 역량이라고 했을 때, 그것을 다시 조직의 무언가로 환원할 수 있는 구조를 만들어가고 있는지에 대해 되돌아 볼 필요가 있다.
나 또한 마찬가지다. 결국 큰 컨텍스트에서 작은 단위로 쪼개어 개발을 할텐데, 그게 LLM Driven Development에서는 자연스럽게 작은 단위에서 발생하는 반복적인 무언가를 스킬 단위로 분리하고, 반복적인 작업에 필요한 것들을 컨텍스트화하고, Do-Not 행위들을 가드레일로 삼고 하는 등의 행위들을 자연스럽게 하고 있는데, 이게 조직 단위로 쌓일 수 있는 것들을 같이 만들어 나가고 있는가? 에 대해서는 세모이다. 전사적인 AX를 시도하고 있지만, 제품의 스프린트에서는 별개로 보았던 것 같다. 나의 개발 행위와 AX 행위에서 교집합을 찾지 않았던 것이 문제였던 것 같다.
그럼 조직으로 관점을 넓혀볼까? 어떻게?
스프린트 밖의 조직 AX로 넘어가서, 비개발자들의 업무 효율을 개선해보려고 이런저런 시도들을 했고, 하고 있다. 결과적으로 개선되는 부분이 생기다보니 생산성이 오르는 것 같고 조직의 형용할 수 없는 무언가(?)도 올라가는 것처럼 보이더라. 서론에서 언급했던 지표나 뒷받침 할 수 있는 근거는 아직 부족해서, 개인 블로그 회고 형태로 밖에 쓸 수 없지만 언젠간 기술 블로그 형태로 정리해서 공유해보고 싶다.
여튼 AX의 시행착오를 겪으면서 여태 가장 크게 문제점이라고 생각했던 부분은, 초기에 무언가 구축할 수 있는 환경을 전달했을 때,
"이런 것도 AI로 할 수 있구나. 나도 해보고 싶다. "
가 아니라
"오 편하네, 근데 이런건 더 있으면 좋겠네, 이것도 해달라고 해야지"
로 나에게 되돌아오는 것이 가장 큰 문제라고 생각한다. 본인이 직접 개선해보거나 만들어보는 경험을 통해 무한히 확장될 수 있는 사고, 경험들을 의존성 체인에 갇혀 우물 안 개구리가 되는게 아닐까 안타까운 심정이 든다.
조직말고 개인으로 돌아와서, 가장 큰 문제는 내가 LLM을 통해 병렬로 싸지르고 다닌 모든 것들에 대해 나는 완벽하게 알고 있나? 에 대한 의문이다. 분명 제품적으로, 조직 내부적으로 개선이 되었는데 내가 구현에 대해 모든 것들을 이해하고 있지는 않은 상태가 더러 발생했다. 이게 조직에 쌓이면, 나중에 어떤 모양으로 돌아올지 잘 모르겠다만 부정적인건 분명하다. 나부터 이 악습을 끊을 필요가 있다.
공부하자 공부
AX? 숫자로 증명이 가능한가?
이력서를 쓸 때도, 기업 소개를 쓸 때도 PR의 수단은 결국 숫자. 지표라고 생각한다. 그래서 도달하는 결론은 숫자다.
물론 지금 당장 AI를 통해 어떤 지표가 개선이 되었고는 잘 모른다. 미지수이다. 하지만 모든 시도들이 조직적인 자산으로 남기만 해도 언젠가 가속화의 수단이 될 것이라는 생각이 든다. 다만 지금 그게 어떻게 남고 있는지, 남겨야 하는지에 대해 시도해보면서 시행 착오를 겪는 과정에 있는 것 같다. 조직의 컨텍스트를 SSoT로 한 군데에 모은지는 벌써 꽤 되었지만, 이건 단순 시발점에 불과하니까.
결국 AI에 돈을 더 쓰는 만큼 다른 부분에서 향상된 것이 있나? 그걸 숫자로 표현할 수 있나? 에 대해 생각해보게 된다.
이 질문에 답하지 못한다면, 결국 기분의 영역으로 AI로 뭔가 잘 되고 있어!!! 라는 환각 상태에 계속 머무르지 않을까?
마치며 (완전 다른얘기)
Opus 4.7이 나온 시점부터, 뭔가 겹겹이 쌓였던 하네스를 걷어내고 있다.
모델 버전이 올라갈수록 LLM 자체의 내부 하네스가 견고해질 것이라고 생각한다. 그래서 내가 덧대둔 로컬 세팅을 조금씩 덜어내고 있었는데, Opus 4.7, GPT-5.5가 나온 시점부터는 아예 모델의 업데이트마다 로컬 세팅을 모두 제거하고 퓨어한 상태에서 시작해보고 있다. 그래야 그 모델을 있는 그대로 이해하고 그 위에 현재에 필요한 새 하네스를 깔아볼 수 있기 떄문이다.
도구는 점점 알아서 잘해지기 때문에, 그럴수록 나는 내가 뭘 알고 모르는지를 더 또렷이 보는 눈을 길러야겠다. 빨라진 만큼 남는 게 있는지, 그 남는 것들을 내가 이해하고 내 것으로 만들어 성장할 수 있는지.
눈에 보이면 닥치는대로 하면서 최대한 중심을 지키려고 했던 4월이었다. CLI와 API를 합하면 200억 토큰을 조금 넘게 사용해오면서 나에게도 드디어 진하게 우울증이 왔다. LLM을 점점 많이 사용하면서 회의감 같은 것들이 들기 시작했다. (한국에서는 AI로 인한 우울증을 Claude Blue라고 하더라.) 그럼에도 원하는 것들이, 해보고싶은 것들이 있기 때문에 킵 고잉할 수 밖에 없었던 4월이었다.
개인 AI CLI 사용량
Claude Blue
그 전에도 나는 무언가를 시작하면 항상 끝장을 봐야하는, 최상위권은 달성을 해봐야하는 유형의 사람이었다. 개발자로 커리어를 시작하면서도 마찬가지었다. 벽은 너무 높지만 착실히 하나하나 쌓아나가며 의지가 꺾이지 않는다면 언젠가는 도달할 수 있지 않을까? 라고 생각하며 22년부터 25년까지 정말 미친듯이 몰두했던 것 같다.
처음 LLM 코딩 도구들이 등장했을 때, 생산성이 가속화되고 반복 작업들을 대신 해줄 수 있는 것에 매우 만족하며 지냈다.
언제부터였을까, 아마 Opus 4.5가 나오면서부터 조금 많이 변했던 것 같다. 이전까지는 여전히 깊이가 중요하고 내가 처한 상황에서 적절한 선택을 할 수 있는건 엔지니어의 필수 역량이며, LLM도 도구이자 수단으로 오케스트레이션을 할 수 있어야 한다고 생각했다. 하지만 도구들도 서로 경쟁하며 발전하고 Claude Code가 장애가 발생하면 Codex 등의 여러 대체 수단도 경쟁하며 성장하고 있고, 이 성장 속도는 내가 성장하는 것과는 비교할 수 없을 정도로 가파르게 성장하고 있다.
그러다보니, 개발자로 언젠가 닿고자 했던 영역 자체를 부정하게 되었고 그러다보니 내가 사랑하던 개발 자체에 회의감이 들기 시작했다.
꺾이면 어쩔건데.. 존나게 해야지
그래도 다행인건 무언가 만들면서 누군가의 불편함을 해소하고, 더 효율적으로 그들의 리소스를 사용할 수 있게 하는 것에는 여전히 흥미가 있다는 점이다. 조직 내에 조금이라도 불편한 것들을 나서서 해결하고, 지난달 회고에서처럼 정형화된 업무 프로세스를 깨보기도하고 하면서 닥치는대로 이것저것 시도했다.
존나게 하다보니까 느끼는건, 무언가 개발 지식이 깊게 채워지지는 않는다는 것이었다. 물론 내가 가는 방향이 달라서 그런 것 같긴 하지만, 대신 여러 상황을 동시에 생각하는 능력과 여러 병렬 작업 간에 컨텍스트를 관리하는 스킬들이 점점 늘어나는 것 같다.
이런 것들에서 개발자와 유사하지만 또 다른 무언가의 스킬들이 늘면서 어떻게든 성장은 하겠구나 라는 생각이 들었다. 그러니까 또 이상하게 재밌어지더라..
빠르게, 빠르게, 그리고 정교하게
그러던 와중에 조직에서 다면 평가를 했다. 현재 내가 밀고있는 점들을 장점이라고 알아주셔서 너무 감사했다. 반대로 단점은 기존에도 가지고 있었던 것들이라, 더 빠르게 빠르게 병렬적으로 일을 하면서 더 단점이 뾰족하게 드러났다고 생각이 들었다. 조금 더 정교하게 깎을 필요가 있겠다는 생각이 들었다. 직접 코드를 짜든 AI Native하게 일을 하든 말이다.
혼란스러움에 익숙해지기... 익숙해질 수 있을까
정말 혼란스럽다는 말을 입에 달고 사는 것 같다. 스타트업에서의 혼란스러움을 회고에서도 자주 나타냈었는데, 이 위에 작금의 산업 혁명과도 같은 AI들 때문에 더 가중치가 쌓이는 것 같다. 아마 글에서도 정리가 안되는 모습이 많이 보일 것이라고 생각한다.
혼란을 잠재울 수 있다면 너무 좋겠지만, 요즈음 시대에 나처럼 나약한 범부들이 혼란을 어떻게 잠재울 수 있을까.. 그냥 프론티어들의 뒤를 따르면서 LTS를 잘 쓰는 장인으로 익숙해지는 수밖에..
내 오픈소스 이야기
2월달엔가 CLI 토큰 사용량 트래커인 ccusage가 너무 느려졌다. 기존에도 세션 파일은 쌓여있었을텐데 거의 로딩에 1분이 걸릴 정도로 느려져서, Rust로 포팅하다시피한 toktrack이라는 오픈소스를 직접 만들었다.
만들 때 홍보를 한 번 하고, 그 뒤로 건드리지 않았는데 어디선가 입소문이 타는건지 이번달에 갑자기 스타 수가 2배나 늘었다.
그래서 기분이 좋다. 그렇다.
Grafana Meetup을 개최하다
Loki에 기여를 수 차례 했던 것을 계기로, 싱가폴의 Grafana 매니저분과 연결되었다. 한국의, 서울의 그라파나 밋업의 오거나이저가 될 생각이 없냐고... 그 내가 SRE도 아니고 단순 기여 정도에 그치지만, 개발자 커뮤니티를 운영하면서 오는 소프트 스킬이나 기타 다른 부분들의 역량도 향상되지 않을까? 하는 러프한 생각으로 1인으로 개최를 진행했다. 100명 신청을 받았고, 51분 참석하셨다.
운영하는데 별로 힘들거라 생각하지 않았던 것 같다. 실제로 한 번 경험해보니, 혼자서는 도저히 불가능하다는 것을 깨달았다. 준비 과정에서 연락해야할 곳도 많고, 당일에도 운영진으로서 발표는 거의 듣지 못했다. 그래도 다들 오셔서 좋아해주셨기 때문에, 그리고 지속 가능한 밋업이었으면 좋겠다는 니즈들을 많이 들어서 지속 가능하게 발전시켜 보려한다.
마무리
팔란티어의 FDE라는 직군의 등장 이래로 벌써 많은 시간이 흘렀다. LLM의 발전도 벌써 엄청나게 되었고 최근 Codex의 GPT 5.5는 정말 감탄스러운 속도와 품질을 제공하고 있다. 개인적으로는 Opus 4.7은 GPT 5.5에 비하면 음식물 쓰레기라고 생각한다.
하네스, 헤르메스 등의 LLM을 잘 다루기 위한 트렌드는 계속 변한다. 물론 프론티어들이 발굴해내는 영역이겠지만, 그 사이에서 중심을 잃지 않아야 한다는 것이 가장 중요한 것 같다. LLM이던 그 이전의 DDD 등의 아키텍처나 디자인 패턴 또한 상황에 맞게 적절하게 가져다 쓸 수 있어야 했고 지금의 트렌드도 마찬가지다. 단순 PoC를 만드는데 엄청나게 많은 스킬셋이나 훅으로 가드레일이 덕지덕지 붙어 있는 셋업? 아니면 그 유명한 YC의 gstack을 가져다 쓸 필요는 없을 것이다.
내 중심을 지키면서, 환경에 대한 객관화를 항상 해두고, 다음 스텝을 미리 생각해놓는 습관이야말로 기초 소양이자 가장 중요한 덕목 중 하나이지 않을까 라는 생각이 드는 요즘이다.
꽃이 만개하든 말든, 여러 다양한 시도들을 하며 재밌게 바쁘게 알차게 보냈던 3월의 회고.
3월엔 이전의 회고들에서도 주구장창 얘기했던, 깊게 하는 생각을 다 깨고 빠르게 빠르게 바로 시도해 볼 수 있는 기반을 닦았다고 생각이 든다.
기존의 틀을 깨보기
어떻게 보면 AI 과도기인, 산업 혁명(?) 과도 같은 시기에 있는 지금. 아무리 스타트업이라고 하더라도 스프린트에 어느정도 고착화되어 있을 수 있다. 우리 또한 마찬가지라고 생각했다. 기존 업무 방식인 스프린트 단위의 사이클도 물론 빠르지만 PM/디자이너/개발자 등의 업무 체계가 명확하게 나뉘어져있고, 의사 소통에서 오는 병목이 반드시 존재한다.
물론 AI Native하게 바로 가자! 도 아니고, AI를 맹신해서 하는 말도 아니다. 다만 지금 시점에는 스프린트 업무를 가져가면서 병렬로 충분히 가져갈 수 있는 부수 업무들이 많아질 수 있다고 생각한다. 또 다른 얘기로는, 본인의 판단이 맞다고 생각이 든다면 어느정도 PoC를 빠르게 내볼 수 있는 환경 정도는 이미 갖추어져 있다고 생각이 든다.
그래서 시도했던건, 조직 내에 여러 레거시 제품들 중에서, 꾸준히 수익이 나는데 휴먼 루프가 많은 것들을 선정하여 1인 풀스택으로 AI Native하게 개발을 진행했다. 그들의 리소스를 절감하면서, 자연스레 제품의 뎁스도 간소화도 이루어졌고 그 결과 해당 기능들을 이용하는 고객들이 조금 늘어났다.
풀스택 개발을 자랑하려고 하는 것도, 컨텍스트 엔지니어링을 좀 잘하는 것 같다. 라고 으스대려고 하는 말도 아니다. 다만 기존의 업무 방식을 냅다 망치들고 갖다 깨버릴 수 있는 사람이 나타났다!!! 정도로 이해해주길 바란다. 충분히 의도한 바가 맞고, 이런 시도들이 모여 결국 조직의 업무 방식이 긍정적으로 변화할 수 있길 바라는 마음이다.
AX, AX, AX!!!!
AI 관련된 글을 많이 쓰긴 하지만, AI 맹신론자는 아니다. 오히려 Claude Blue라고 불리는, AI 우울증 환자 중 한명이다.
그럼에도 분명한건, 더 많은 기여를 할 수 있는 환경이 깔렸다는 것에는 이견이 없다.
위에 업무 틀을 깨부수는 시도를 했던 것 처럼, 업무 틀을 깨부술 수 있는 가장 좋은 것이 전사적으로 AI를 활용하여 필요한 것들을 만들어 나갈 수 있으면 어떨까? 하는 것이었다. 그게 AX가 되겠지..
그 전까지는, 전사적으로 가져갈만한 조직적인 컨텍스트 관리 도구라던가, 부수적인 봇들을 만드는데 치중했던 것 같다. 뭔가 이런 것들로는 한계가 있을 것이라고 판단해서 - 결국 내가 계속 만들게 되면 업무 틀을 깨는게 아니게 되니 - 3월 초에는, 제품 운영팀 리드분께 Claude Code 사용을 위해 하네스 세팅을 해드리는 것으로 방향을 틀었다.
하루에도 수십 번씩 커밋을 하시면서 나아가시는 모습들을 보면서 괜히 뿌듯하다. 요새는 직접 하네스도 고쳐보시고 스킬도 직접 만들어보시는 등 사내 AX 챔피언으로 자리매김하고 계신다.
의도는 명확했던 것 같다. 최초에는 운영 팀 분들이 쓸 수 있는 전용 대시보드가 필요했고 - 구글 시트, 어드민 페이지 등등 여러 관리 도구들을 통합하기 위함 - 이걸 통합하는 PoC를 빠르게 구축해 드리려했다. 하지만 니즈를 듣고 이해하는데도 병목이 생기니, 직접 해보시라고 깔아드린 것이었다.
이런 사례들도 하나하나 씨앗이 되어 조직적인 AX의 한 걸음을 걸을 수 있지 않을까? 라는 생각이 든다. 더불어 내가 만든 도구들도 잘 말아서, 조직적인 AX에 여기저기 활용할 수 있도록 시도해 볼 생각이다.
묵묵히 나아갈 걸음
프론티어가 아니면 어떤가 라는 생각이 계속해서 든다. 조직적으로도 개인적으로도 말이다.
며칠 전 Claude Code 유출 사건 때 Sionic AI의 진형님이 몇 시간 만에 Rust로 클로드 코드를 포팅한 레포가 화제가 됐다.
오픈클로를 만든 사람이나, OmC, OmX등을 만든 예찬님, 링크드인에 좋은 사례들을 공유해주시는 프론티어처럼 욕심을 내고 잘 안됐을 때 현타가 오는 경험도 겪었던 것 같다.
현재 공유 오피스 건물에 AICX 팀을 별도로 또 운영하고 있는, 미디엄 블로그로 AX 사례를 거의 찍어내다시피 공유하는 마이리얼트립 또한 어떻게 보면 조직적인 AX는 프론티어라는 생각이 들었다.
요즘은 그에 비해 속도가 느리다고 도태되었다거나, 조급해하는 생각을 아예 없애버렸다. 생각해보면 Node도 Nightly 스냅샷을 내놓고 여러 실험들을 하지만 결국 LTS가 선택받는다. LLM 사용 사례를 봐도 여러 키워드들이 나왔다 사라졌지만 최근 트렌드는 결국 Nightly를 시도했던 프론티어 분들이 결론낸 하네스나 컨텍스트 엔지니어링이고, 그걸 잘 말아서 쓰는 사람이 결국 퍼포먼스를 내고 있다고 생각한다.
그래서, 결국 도구에 불과한 이 녀석을 멀리 하지만 않고 변화에 능동적으로 대응할 수 있으면 되지 않을까? 라는 조금 내려놓는 마음가짐을 요새는 가지려고 하는 중인 것 같다.
마치며
다음달 회고에는 단기 목표도 어느정도 이뤘는지 리마인드 하기 위해 이번 달에 세운 목표를 첨부하는 것을 마무리로, 글을 마무리하려고 한다. 어떤 시대건 간에 결국 묵묵히 해내고 해낸 것들을 체화할 수 있는 사람이 결국 성장하고 나아가지 않나. 라는 생각이다
여느 때처럼 평화롭게 개발을 하던 어느날, 프로덕트의 dev 환경에서 502가 계속해서 발생했습니다. 지표도 정상이고 헬스 체크도 정상이고, 도대체 무슨 문제인지 긴가민가 했습니다. 로그를 확인 해 본 결과 처음보는 낯선 에러가 발생하더군요
요약하자면 Python WSGI 애플리케이션 서버인 uWSGI의 buffer-size 설정 값을 초과했기 때문이었습니다. 찾아보니 이 buffer-size는 요청 헤더의 사이즈의 설정 값이라고 하더군요.
Express를 사용했을 때를 떠올려보면 발생하지 않았던 문제였습니다. 요청 헤더가 커서 요청이 Drop된 적이 없었기 때문입니다.
이에 관련된 CS적인 지식은 기초 수준의 간단한 것입니다만, Django 진영의 웹 서버는 왜 이런 방식을 선택했는지 궁금해서, 기록 겸 포스팅을 남깁니다.
원인
위에서 정리했듯이, 원인은 HTTP 요청 헤더의 사이즈 초과입니다.
uWSGI 관련 공식 문서를 보면, 버퍼 사이즈는 매우 작게 설정되어있으며, 로그에서 이상 현상이 감지된다면 더 큰 사이즈로 변경이 필요할 수도 있다고 언급되어 있습니다. 말은 쉽지만 실제 환경에서는 충분히 문제가 발생할 수 있고 저 또한 겪었습니다. 핫픽스로 배포를 나가도 물려있는 CICD 때문에 10여분 넘게 다운타임이 발생할 수 있는 것이죠 (실제로는 서버 다운은 아니고 UX적인 다운타임이겠습니다.)
정확한 원인을 추적하기 위해 좀 더 깊게 파악해보겠습니다.
일반적인 Python 웹 서비스는 요청이 리버스 프록시(nginx)를 통해 uWSGI를 거쳐 Django로 들어오는 흐름입니다. 리버스 프록시는 클라이언트로부터 요청을 받아 uWSGI로 패킷을 전달 할 때 uwsgi 바이너리 프로토콜로 변환해서 보내게 됩니다.
# nginx 설정
location / {
uwsgi_pass unix:///tmp/uwsgi.sock;
include uwsgi_params;
}
uwsgi_pass를 쓰면 nginx가 HTTP 헤더들을 uwsgi 프로토콜의 key-value 쌍으로 변환하고, 이걸 하나의 uwsgi 패킷으로 직렬화해서 전송합니다. uwsgi 프로토콜 스펙 자체가 하나의 패킷을 전달 받아야만 하는 스펙으로 설계가 되어있기 때문입니다. uwsgi 프로토콜은 패킷 헤더에 datasize라는 변수를 16비트 정수로 담습니다.
패킷 헤더의 datasize가 뒤따르는 전체 데이터 크기를 나타내는 구조라서, 요청 메타데이터를 여러 패킷으로 나눠 보내는 개념이 없습니다. 그래서 수신 측인 uWSGI도 이 패킷을 고정 버퍼에 통째로 받는 구조가 됩니다.
즉, 버퍼 사이즈를 초과하는 요청이 들어오면 uWSGI는 Django에 요청을 넘기지 않고 Drop하게 됩니다. 서버가 죽은 것이 아니라 요청이 앱에 도달하기 전에 버려지게 되어 헬스 체크는 200인데 UX는 다운된 것처럼 보였던 것입니다.
Express
이전에 썻던 Node의 Express는 파싱 방식과 기본값에서 차이가 있었습니다.
Node의 HTTP 파서인 llhttp는 헤더를 고정 버퍼에 담지 않습니다. 들어오는 데이터를 바이트 단위로 스트리밍하면서, 누적 크기만 카운터로 추적합니다.
nodejs의 http parser
고정 버퍼에 통째로 담는 uWSGI와 달리 파싱하면서 카운터만 올리는 방식입니다. 그리고 결정적으로 기본 제한값이 16KB로 크기 때문에 대부분의 상황에서 발견하지 못했습니다. 더불어 uwsgi처럼 바이너리 프로토콜 변환 없이 raw HTTP를 직접 파싱하기 때문에 프로토콜 크기의 제약도 없습니다.
즉, Express를 사용했을 때는 같은 요청이여도 서버가 받아들이는 구조가 다르기 때문에 한 번도 겪어본 적이 없었습니다.
해결
원인을 알았으니 해결책은 명확하겠죠..?
근본 원인인 헤더가 커진 이유를 조사하기 시작했습니다. 짐작하시겠지만, 프로덕트가 커져가면서 구워지는(?) 쿠키도 많아졌습니다. 구체적으로는 A/B 테스트나 지표 추적을 위해 이벤트를 심는 양이 늘어남에 따라 쿠키가 같이 늘어났습니다. 디버깅 결과 전체 쿠키의 70% 이상이 해당 지표 추적을 위한 쿠키였고, 계속해서 늘어나고 있었습니다. 브라우저가 요청마다 이 쿠키를 전부 실어 보내는 구조였기 때문에 점점 헤더 사이즈가 커지면서 결국 터져버렸던 것입니다.
저희는 일단 버퍼 사이즈를 늘리는 것으로 선조치를 했습니다. 하지만 쿠키가 계속 늘어난다면 또 터질테니 근본 해결책은 아니겠죠.
CloudFront Origin Request Policy에서 쿠키를 필터링하거나 로컬 스토리지로 쿠키를 마이그레이션, 쿠키 도메인을 분리하는 등의 방안을 고려중에 있습니다. 각각 하나씩 간단히 살펴보면서 마무리하겠습니다.
CloudFront가 오리진(nginx/uWSGI)에 요청을 전달할 때 필요한 쿠키만 화이트리스트로 전달하도록 설정이 가능합니다.
브라우저는 쿠키를 다 보내지만 CF가 걸러줄 수 있도록 설정이 가능하다고 합니다. 프론트 코드 변경 없이 인프라 설정만으로 적용이 가능하지만, CF를 통하지 않고 직접 접근할 때는 효과가 없습니다.
서버에 보낼 필요가 없는 데이터를 쿠키에서 로컬 스토리지로 마이그레이션하는 방법도 있습니다.
이 방법은 서버가 읽어야 하는 쿠키는 이동이 불가능하고, 프론트 코드 변경이 필요할 수도 있습니다.
마지막으로 쿠키 도메인을 분리하는 방법입니다.
.example.com에 쿠키가 설정되면 모든 서브도메인에 전송됩니다. 이걸 api.example.com으로 분리하고 쿠키 도메인을 서브도메인 별로 격리하면, API 요청에는 해당 서브도메인 쿠키만 실리게 되겠죠. 가장 구조적인 해결이 될 것이라 생각하는데, 서드파티 스크립트인 GA나 이벤트 도구 등이 최상위 도메인에 쿠키를 설정해야한다면, 통제가 어려울 수도 있겠습니다. (아직 확인해보진 않았습니다.)
마무리
오랜만에, 정말 오랜만에 AI에서 벗어나 실제 문제를 좀 깊게 파볼 수 있는 시간이었습니다.
스타트업으로 이직 후, 수습 회고 통해 입사 후 혼란스러운 환경을 개선하기 위한 노력들을 소개했었습니다.
요약하자면 빠르게 적응하기 위해 개인적으로 가장 필요했던 것이 무엇인지 생각해봤고, 자주 연달아 이어지는 구두 논의나 회의 때문에 이전 맥락을 잊어버리기 일쑤였습니다. 이를 극복하기 위해 미팅 컨텍스트 허브를 만들어서 개인적으로 사용해왔습니다. 미팅 녹음을 올리면 AI가 자동으로 요약하고, 결정사항을 뽑아주고, 임베딩해서 검색 가능하게 만들어주는 도구였습니다.
개인적으로 만들어 사용했던 미팅 컨텍스트 허브
개인적으로 MCH(Meeting-Context-Hub)라고 부르는 이 도구의 사용성이 꽤나 좋았어서 전사적으로 공유해야겠다고 생각했던 찰나, 사내에서도 티로라고 불리는 비슷한 제품을 구독해서 사용하기 시작했습니다. 미팅록을 한 곳에 모으고 정리하는 역할은 티로가 해결해버렸습니다. 전사적으로 제 도구를 도입하지는 못했지만, 이 과정을 겪으면서 돌아보게 된 것이 있습니다.
구성원들이 더 효율적으로 빠르게 일하기 위해, 현재 워크플로우의 문제들이 무엇일까?
미팅에서 결정된 내용은 티로를 통해 확인할 수 있습니다. 하지만 그 결정이 실행되는 과정은 슬랙 스레드에 흩어집니다.더불어 큰 의사결정 안에서 작은 논의들이 계속 오가고, 자잘하게 변경되는 내용도 많습니다.
또, 저는 이제 막 수습을 지났습니다. 제가 입사하기 이전의 수 많은 컨텍스트는 무시되어도 되는 걸까요? 이전 조직 구성원들이 슬랙에 나눈 대화와 정리된 노션 문서 또한 조직의 자산입니다. 이건 저희 조직 뿐 아니라 어떤 조직이든 공통적인 부분이라고 생각합니다.
MCH를 만들면서 느낀 건, 개인의 기억력 문제가 아니라 조직의 맥락이 검색 가능한 형태로 존재하지 않는다는 것이었습니다. 미팅록은 그 퍼즐의 한 조각일 뿐이고, 여러 컨텍스트들이 전부 한 곳에 모여야 비로소 재활용 가능한 가장 좋은 형태가 될 것이라고 생각했습니다. 원활하게 꺼내볼 수 있어야 이걸 찾아보는 리소스도 줄어들고, 자연스레 다음 스텝이 있을 수 있다고 판단했습니다. 그래서 이 허브를 전사의 컨텍스트 허브로 확장해보기로 했습니다.
사내 컨텍스트 허브인 아라
데이터 수집 파이프라인 설계
사람이 작업을 하든, AI가 하든 맥락이 있어야 합니다. 당연하게도 슬랙과 노션에 흩어진 조직의 지식을 한 곳에 모으는 것이 첫 시작이었습니다. 현재 파이프라인을 시각화하면 아래와 같습니다.
임베딩
임베딩을 통해 텍스트를 고정 길이의 숫자 벡터로 변환합니다. 예를 들어 연차 규정과 휴가 정책의 글자는 다르지만, 임베딩 벡터 간의 거리는 가깝다는 논리입니다. 이를 이용하면 키워드가 정확히 일치하지 않아도 의미적으로 유사한 문서를 찾을 수 있게 됩니다.
임베딩에는 OpenAI의 text-embedding-3-small 모델을 사용했습니다. 우선은 빠르게 PoC를 구축하는 것이 목표였기 때문에 API 호출 한 번으로 바로 쓸 수 있고, 비용이 저렴한 것을 우선에 뒀습니다. 블로그를 작성하는 지금 시점에서는 임베딩 벤치마크 리더보드에서 괜찮은 모델로 마이그레이션 할 계획을 가지고 있습니다.
변환된 벡터를 SQLite에 AI가 추출한 메타데이터와 함께 저장합니다. AI가 뽑아낸 메타데이터(제목, 요약, 태그)는 키워드 검색과 필터링에 활용하고, 임베딩은 시맨틱 검색에 활용됩니다. 이 때 수집된 원본 텍스트는 반드시 그대로 보존합니다.조직의 데이터 규모가 수천 건 수준이기 때문에 파일 하나로 단순하게 관리되고, 백업에도 용이하기 때문입니다. 코사인 유사도(임베딩) 계산은 애플리케이션 레벨에서 수행할 수 있는 수준이기 때문에 SQLite를 사용했습니다.
메타데이터 추출 파이프라인
메타데이터를 수집 시점에 AI가 한 번 추출하지만, 특히 긴 문서의 경우 중요한 결정사항이나 맥락을 놓치는 경우가 있었습니다.
그래서 2-pass 구조로 확장하여, 최초 수집 시점은 빠르게 처리하고, 그 뒤에 정교한 분석을 수행하도록 했습니다.
2nd pass에서는 1st pass의 결과와 원본 텍스트를 함께 LLM에게 전달하여 최종 검토를 하게 됩니다.
데이터 수집
현재 조직의 컨텍스트는 노션과 슬랙으로 관리되고 있습니다. 두 도구의 성격이 당연히 다른데요.
슬랙: 러프한 대화 위주의 내용. 잡담도 포함. 데이터 구조가 단순하며 플랫함(최대 1Depth Thread 구조)
노션: 구조화된 포맷을 기반으로 각 논의된 내용들을 어느정도 정리함. 페이지, DB 등 여러 Depth로 구성되어 복잡함
도구의 성격이 다른 만큼, 수집 방법도 달랐는데요, 슬랙과 노션의 수집 방법은 이렇습니다.
슬랙
평일 1시간 단위의 스케줄러가 슬랙의 채널 대화를 수집합니다. 채널 중에는 정말 잡담을 위한 채널도 존재하고, 레거시 채널도 존재합니다. 수집할 필요가 없는 채널들을 제외하고 화이트리스트 방식으로 수집 대상 채널만 직접 선택하는 구조로 만들었습니다. 이 채널의 대화는 조직의 지식으로 보존할 가치가 있다고 판단되는 채널만 등록했습니다.
굳이 디테일을 언급하자면, 증분 수집을 위해 채널 단위의 독립적인 워터마크를 추가했습니다.
이를 통해 각 채널의 메세지와 쓰레드를 증분 수집했고, 별도 노이즈 필터링을 통해 봇 메세지나 시스템 메세지, 10자 미만 메세지나 특정 노이즈 키워드(ㅋㅋ, 넵 등등)를 위한 딕셔너리를 별도로 두어 관리하고 있습니다.
노션
노션은 특정 결정의 중간/최종 산물의 성격을 띱니다. 그렇기에 평일 12시간 단위로 08:00, 20:00에 하루 일과 시작 전/후를 고려하여 배치 시간을 정했습니다.
위에서 언급했듯이 노션은 복잡한 뎁스의 트리 구조입니다. 페이지 안에 하위 페이지가 있고, 그 안에 또 하위 페이지가 있을 수 있죠. 데이터베이스 안에 row가 있고, 각 row도 하나의 페이지입니다. 그래서 기존 노션 문서들의 포맷을 보면서, 어느 깊이까지 탐색해야 할 지 정했습니다.
더불어 중복과 업데이트 처리도 같이 신경 써야했습니다. 같은 배치 안에서 상위 하위 페이지가 모두 수집 대상이 될 수 있고, 이미 수집한 페이지가 수정되었을 경우도 충분히 발생할 수 있는 경우이기 때문입니다.
// notion-ingester.ts — 중복/업데이트 판별
for (const page of pages) {
// 제외 대상 페이지 또는 그 하위 트리 전체를 건너뜀
if (excludeSet.has(page.id) || (page.parentId && excludeSet.has(page.parentId))) {
result.skippedExcluded++;
continue;
}
// 동일 배치 내 중복 제거
if (seenPageIds.has(page.id)) {
result.skippedDuplicate++;
continue;
}
seenPageIds.add(page.id);
// 마지막 수집 이후 수정되지 않은 페이지는 건너뜀
const existing = existingByPageId.get(page.id);
if (existing && existing.lastEditedTime >= page.lastEditedTime) {
result.skippedUnchanged++;
} else {
toProcess.push(page);
}
}
결론적으로, 제외 목록을 건너뛰고, 배치 내 중복을 건너뛰며, 마지막으로 변경 여부를 확인하여 건너 뛰는 로직을 통과한 페이지만 5개의 동시 워커로 병렬 처리했습니다.
RAG는 실패했다
결론부터 얘기하자면, 저는 RAG 방식으로 대차게 실패했습니다. 첫 시도부터 틀려먹어서 좀 당황했습니다.이 허브를 구축하면서 RAG에 관련된 레퍼런스를 많이 찾아봤는데, 정말 매력적이고 좋은 패턴이라고 생각합니다. 다만 저는 이 패턴에 대한 이해도가 모자랐기 때문에 실패했다고 생각합니다.
제 최초 설계는 5단계 RAG 파이프라인이었습니다.
SEARCH - 임베딩 유사도 + 키워드(SQL LIKE) 하이브리드 검색
FILTER - 유사도 0.3(30%) 미만 결과 제거
ENRICH - 슬랙 메타(채널명, 날짜), 미팅록이 있다면 미팅록의 메타로 검색 결과 보강
ASSEMBLE - Token Budget(21000자, 약 6000토큰) 내에서 컨텍스트 조립
SYNTHESIZE - Claude에게 조립된 컨텍스트와 질문을 전달하여 답변 생성
거창하게 5단계를 나눠서 썻지만, 결론적으로 RAG란 제가 미리 특정 컨텍스트를 단정지어서 LM에게 넘기는 방식입니다.
위 실패 케이스를 자세히 살펴보고 비슷한 실험들을 토대로 결론을 내릴 수 있었습니다. 결국 긴 문서에서 제가 설계한 방법은 틀려먹었다는 겁니다. 저희 노션 페이지에 긴 문서들의 특징은 한 페이지 안에 여러 주제의 내용들이 혼재되어 있는 경우에 해당합니다. 위 연차 규정 같은 경우도 신규 입사자 온보딩 가이드라는 큰 문서의 한 섹션에 있었고, 저는 그 가이드 전체를 통째로 임베딩 했었던 것이었습니다.
문서 전체를 하나의 벡터로 변환했을 때, 그 벡터는 문서의 평균적인 의미를 담습니다. 과연 이 문서가 연차 규정인지 알까요? 당연히 연차 규정이라는 질문과 코사인 유사도가 0.3 미만으로 나왔고, 2단계 필터에서 잘려나갔습니다.
문제점을 파악했다면 당연히 개발자스럽게 해결하면 되겠습니다. 문서를 작은 단위로 쪼개면 해결하기 쉽겠죠? 바로 청킹(Chunking)을 사용하면 됩니다. 청킹이란 방대한 정보나 개별 요소를 의미 있는 작은 덩어리(Chunk)로 묶어 기억하거나 처리하는 인지 심리학 기법이라고 합니다. 여기서의 청킹이란, 긴 문서를 청크 단위로 쪼개서 각각을 별도로 임베딩한다는 의미로 이해하시면 됩니다. 온보딩 가이드를 섹션별로 나누면 연차 섹션의 임베딩이 질문과 높은 유사도를 보일 것입니다.
하지만 청킹은 새로운 튜닝 포인트를 만듭니다. 청크 크기는 얼마로 할 지? 크기에 따라 문맥이 잘릴 수도 있겠죠... 문맥이 잘리면 이전 결정 사항과 분리되어 의미를 잃을 수 있고, 너무 크다면 원래 문제가 재현됩니다. 이를 위해 오버랩을 사용할 수 있습니다. 하지만 오버랩은 노션 페이지가 수정될 때 마다 청크를 다시 나누고 임베딩을 또 해야되는 문제가 있겠죠...
또 다른 방법으로, 청킹 과정에서도 별도 LLM 레이어를 두어 문맥 단위로 청킹 시키도록 할 수 있겠습니다. 하지만 저는 이런 방법들로 또 다른 문제를 야기할 수도 있다는 생각에, 검색 파이프라인을 고도화하는 방법에서 LLM의 자율 주행에 몸을 맡겨보기로 했습니다.
Agentic Search
사실 선택의 이유는 두 가지가 더 있습니다. Claude Code의 모델이 발전함에 따라 알아서 잘 찾겠구나 라는 생각과 거기에 기름을 부어준 클로드 코드 창시자인Boris Cherny의 트윗을 보고 빠르게 전환을 결심했습니다. 트윗의 내용은 Claude Code 자체도 초기에는 RAG + 로컬 벡터 DB 기반이었다가 Agentic Search로 전환했다는 내용입니다.
Agentic Search를 통해 검색 파이프라인의 각 단계를 사람이 하드코딩하지 않고, LLM에게 검색 도구를 쥐어주고 스스로 판단하게 했습니다. 사람이 어떤 순서로 검색할지 정하는 게 아니라 LLM이 질문을 보고 이건 키워드 검색이 낫겠다, 이 문서의 3번째 섹션을 읽어야겠다 를 자율적으로 결정합니다.
Agentic Search도 거창해보이지만 간단합니다. 로컬 CLI로 여태까지 Claude Code를 사용했던 경험을 떠올려 보니 자연스레 MCP를 떠올렸습니다. 저는 8가지 MCP를 통해 LLM의 자율 주행을 보장했습니다. 각 MCP는 Claude Agent SDK의 tool()로 정의하고, query()를 통해 질문과 MCP를 포함한 각종 옵션들을 넘겨 사용합니다.
이렇게 MCP를 분리해서 LLM이 질문의 성격에 따라 전략을 선택할 수 있게 했습니다. 위의 연차 규정처럼 명확한 키워드가 있으면 바로 search_by_keyword를 사용하고, 최근 팀 분위기가 어떠니? 같은 모호한 질문에는 search_by_meaning을 사용합니다. 짧은 스레드는 read_context로 전체를 읽으면 되지만, 긴 맥락은 read_context_section으로 맥락 내 특정 섹션만 읽어야 효율적입니다.
LLM이 완전히 자율적으로 도구를 선택하지만, 효율적인 전략을 가이드했습니다. 예를 들어 검색형 질의에는 search_by_meaning을 우선 사용하고 고유명사가 포함된 질문에는 search_by_keyword을 먼저, 분석형 질의에는 list_channel_contexts로 벌크 조회를 수행하라는 가이드가 있습니다.
연차 규정 알려줘 라는 동일한 질문에 대해 이제 Agent Search는 다음과 같이 동작합니다.
모든 도구 선택을 LLM이 자율적으로 수행합니다. 임베딩 유사도에 의존하지 않기 때문에 긴 문서 안에 묻힌 정보도 정확히 찾아낼 수 있게 되었습니다.
운영하면서 겪은 문제들
2~3주 정도 운영해오면서 아래와 같은 이슈들을 발견했고, 다음과 같이 수정했습니다.
유사도가 낮은 문서까지 여러 개 확인하면서 응답 시간이 수 분까지 늘어났다
비동기 + graceful degradation(실패해도 답변을 돌려줌)로 개선
슬랙 스레드에서 후속 질문을 하면 이전 대화 맥락을 몰라 발생하는 문제
스레드의 이전 메세지를 수집해서 질문에 컨텍스트로 주입
맺으며
개인의 미팅록 관리 도구에서, 필요한 것들을 하나씩 추가하며 점진적으로 구조를 정리하고 전사적인 플랫폼으로 발전시키려고 했습니다. 이 허브가 궁극적으로 지향하는 것은 조직의 기억입니다. 누구든지 직군의 구분 없이 조직의 맥락을 쉽게 파악하고, 과거 논의와 결정을 즉시 찾아주고 담당자가 바뀌어도 히스토리가 유실되지 않게 하는 것. 그것을 사람의 손으로 항상 문서를 정리하고 인수인계를 만들어야 했던 것들을 당장에 해결할 수 있겠죠. 또, 이 허브를 바탕으로 CS 자동화도 쉽게 구축할 수 있습니다. 허브에 과거 CS들을 최초 정리만 하고, 정말 필요 시 CX 담당자가 직접 소통 할 때, CX 담당자의 상담 내용을 재학습하는 하네스를 구축한다면 쉽게 도달 할 수 있는 영역입니다.
(이미 CS 봇은 테스트 중에 있습니다.)
물론 위에서 언급한 UX가 전부라 생각하지는 않습니다. 저는 이 허브를 바탕으로, 다양한 시도들을 해나갈 생각입니다. 이 허브가 정말 두뇌라면, AX화를 하기 위한 멀티 에이전트의 오케스트레이터가 되기에 가장 적합하다고 생각합니다. 이러한 방향으로 발전시켜 볼 생각입니다.