About·GitHub·Email

2026-05-19·[기술 문서]

마케팅 글 자동화를 위한 AI 서버 분리 과정

마케팅 글 자동화 POC를 실제 어드민 기능으로 옮기면서 AI 서버를 분리하고, 프롬프트와 프리셋을 운영 가능한 구조로 바꾼 과정

전에 회사에서 마케팅 글 자동화 POC를 만든 이야기를 쓴 적이 있다. ChatGPT 와 NotebookLM 으로 일단 돌아가는 걸 보여줬는데 동료들 반응이 의외로 좋았다. 그때까지는 좋았다.

문제는 그 다음이었다. POC를 본 마케팅 동료들이 이렇게 말했다.

"이거 매번 ChatGPT 켜서 프롬프트 복붙하기 너무 귀찮은데, 그냥 우리 어드민에 버튼 하나 만들어주면 안 돼요?"

처음엔 이 요구가 좀 부담스러웠다. (POC면 됐지 싶었다.) 그래도 가치를 직접 만들어 본 입장에서, 이걸 한 번 더 들고 가지 않으면 다음 단계가 안 보였다.

그리고 솔직히 이번엔 좀 유리한 출발이었다. 프롬프트 자체는 이미 만들어 둔 게 있었으니까. POC 때 NotebookLM 에 팀이 그동안 써 온 마케팅 글 수십 개를 올려놓고, 우리 팀의 톤·구조·자주 쓰는 강조 방식을 역으로 뽑아낸 프롬프트가 따로 정리돼 있었다. 처음부터 "AI 우리 스타일로 써줘" 라고 들이대지 않고, 실제 글에서 역추출한 프롬프트를 쓴 게 POC 가 그나마 봐줄 만했던 결정적 이유였다. 그 자산이 있으니까 서버 쪽으로 들어가는 결정도 덜 부담스러웠다.

그래서 결국 서버를 하나 만들기로 했다

이미 메인 서버는 있다. 거기 라우터 하나 더 박으면 끝나는 일이긴 한데, 그건 빠르게 접었다.

AI 호출은 응답이 길다. 보통 20-30초 걸린다. 그 사이 사용자한테 뭔가 보여줘야 하니까 SSE 가 필요한데, 메인 서버는 일반 REST 트랜잭션 중심이라 SSE 가 어색했다.

그리고 마케팅 글뿐 아니라 답례품 이미지 자동 생성, 어드민 챗봇 응답 같은 AI 기능들이 곧 들어올 거라는 신호가 보였다. 매번 메인 서버에 끼워 넣으면 종류만 다른 AI 코드가 도메인 코드 사이에 점점 박힐 게 뻔했다.

또 — 이건 좀 솔직한 이유인데 — 실험을 마음 편하게 하고 싶었다. AI 서버는 죽어도 메인 서비스가 안 죽는 구조를 원했다. 프롬프트 한 줄 바꿔보다가 운영 트래픽 망치는 건 진짜 끔찍한 그림이라.

처음엔 정말 단순했다

첫 버전은 라우트 하나였다. POST /generate. 프롬프트는 .md 파일에 박아두고, Gemini 호출해서 응답을 그대로 흘려보내면 끝. 하루 만에 동작했다. (이때까진 정말 가벼웠다.)

근데 두 번째 AI 기능을 붙이려고 하자 바로 막혔다. 마케팅 글이랑 답례품 이미지는 입력 모양도, 출력 모양도, 호출하는 모델도 다르다. 라우트 하나에 if 문으로 나누는 그림이 머릿속에 그려졌는데, 그걸 따라가면 한 달 뒤에는 절대로 보고 싶지 않은 파일이 하나 생겨 있을 것 같았다.

여기서 한참 고민하다가 결국 "액션" 이라는 개념을 도입했다.

액션이 곧 AI 기능 하나가 되도록 했다

액션은 그냥 한 개의 AI 기능이다. 마케팅 글 생성도 액션, 답례품 이미지 생성도 액션, 어드민 챗봇 답변도 액션. 각 액션은 자기가 어떤 프롬프트를 쓰고, 어떤 프리셋 타입을 받고, 입력/출력 schema 가 뭐고, 응답을 SSE 로 줄지 JSON 으로 줄지를 자기 파일 안에서 다 선언한다.

그리고 액션 정의를 모아두는 레지스트리가 있어서, 새 액션을 추가하려면 정의 파일 하나 만들고 레지스트리 배열에 push 하면 끝이다. 라우터도 자동으로 노출되고, 어드민 UI 도 /registry 응답을 보고 자동 렌더링한다. (어드민 코드를 안 건드려도 새 기능이 떠 있는 게 좀 기분 좋았다.)

프롬프트는 파일이 아니라 DB 로 옮겼다

이게 좀 큰 결정이었다. 처음엔 프롬프트도 .md 파일이었는데, 운영해 보니까 마케팅 동료들이 프롬프트 한 줄 바꾸려고 매번 개발자한테 PR 요청을 보내야 했다. 마케팅 입장에서 이건 답답한 일이었다. 글의 톤을 살짝 다듬고 싶을 뿐인데 코드 PR 까지 가야 한다는 게 부자연스러웠다.

그래서 프롬프트를 DB 로 옮겼다. 어드민에서 새 버전을 만들고 활성화하면, 다음 요청부터는 새 프롬프트가 적용된다. 활성 버전은 정확히 1개만 존재해야 해서 활성화 로직은 트랜잭션 안에서 처리했다 — 기존 활성 버전을 deactivate 하고 새 버전을 activate 하는 게 원자적으로 일어나도록.

여기서 한 번 운영 사고가 있었다. RDS 점검 시간에 마케팅 자동화가 통째로 멈춰서, 동료가 "지금 글 안 만들어져요!" 라고 슬랙에 다급히 적었다. (이때 좀 식은땀이 났다.)

그 다음에 추가한 게 파일 fallback 이다. DB 가 죽어 있으면, 마지막 활성 프롬프트의 사본이 들어 있는 .md 파일을 읽어서 그걸로 동작한다. 어드민에서 활성화할 때마다 파일 사본도 같이 떨어트리는 구조. 완벽하진 않은데 운영 중에 AI 가 통째로 멈추는 일은 막을 수 있었다.

프리셋으로 톤을 갈아 끼우게 했다

같은 마케팅 글이어도 채널마다 톤이 다르다. 이메일 서문은 차분하고, SNS 는 가볍고, 매거진 포맷은 길고 감성적이다. 매번 프롬프트를 새로 쓰자니 관리 부담이 너무 늘어났다. 그래서 프롬프트 템플릿은 고정하고, 톤만 프리셋 JSON 으로 갈아 끼우는 방식으로 갔다.

어드민 UI 에는 이런 식으로 떠 있다.

어드민에 등록된 프리셋 목록

기본형, 브리핑형, 매거진형, Q&A 해설형, 후기형 같이 11개 프리셋이 운영 중이다. 새 채널이 추가될 때마다 개발자 손이 가는 게 아니라, 마케팅 쪽에서 프리셋만 새로 등록하면 된다. (이게 진짜 사람 살린다.)

프리셋과 액션이 잘못 짝지어지는 일을 막기 위해 액션마다 받는 프리셋 타입을 선언하고 런타임에서 검증한다. writer 타입 액션에 chat 타입 프리셋이 들어오면 호출 자체를 막는다. 안 그러면 잘못된 컨텍스트로 Gemini 한테 토큰을 태우게 되는데, 이게 누적되면 비용이 꽤 아깝다.

진행률을 보여주니까 체감 시간이 줄었다

생성 한 번에 20-30초가 걸린다. 처음엔 단순히 스피너만 돌렸는데, 동료들이 "이거 죽은 거 아니에요?" 라고 자꾸 물어봤다. 알고 보니 20초가 길었던 게 아니라, 안에서 뭐가 일어나는지 안 보이는 게 답답한 거였다.

그래서 SSE 로 단계별 progress 이벤트를 보내기로 했다. 프리셋 로드, 프롬프트 로드, 생성 중 — 이렇게 세 단계로 끊어서 쏜다. UI 에 "프롬프트 불러오는 중", "AI 응답 받는 중" 같이 떠 있으면 같은 20초여도 사람이 덜 답답해한다. 이게 의외로 컸다.

추가로, 사용자가 탭을 닫으면 서버가 그걸 감지해서 이후 이벤트를 안 쏘게 했다. 안 그러면 이미 끊긴 연결에 쓰기 시도하는 에러가 로그에 미친 듯이 찍힌다. (한 번 로그 보고 좀 충격이었다.)

그래도 솔직히 — 이거 너무 섞였다

여기서 솔직한 얘기를 좀 해야겠다.

이 서버는 처음에 "범용 AI 런타임" 같은 그럴듯한 이름으로 시작했는데, 운영하다 보니 마케팅 도메인 로직이 자꾸 안에 끼어들었다. 마케팅 글의 출력 schema 검증, 도메인별 필수 필드, 프리셋 타입과 액션의 매칭 룰 — 이런 게 다 AI 서버 안에 들어와 있다. 다른 팀에서 같이 쓰자고 했을 때, 그 팀의 도메인 검증을 여기에 또 끼워 넣어야 하는 모양새가 된다. (이게 별로 마음에 안 든다.)

그리고 또 하나, AI 가 내놓는 출력의 다양성에 대한 대응이 부족하다. Gemini 가 가끔 schema 가 어긋난 답을 내놓을 때가 있는데, 지금은 그걸 그냥 400 으로 던지고 클라이언트가 재호출하게 둔다. 잘 만든 서버였으면 repair 시도나 fallback 모델 호출이 들어갔어야 하는데, 그게 안 들어갔다.

그럼에도 일단 돌아가는 걸 먼저 만들었다

이렇게 부족한 점이 보이는데도 이 시점에 글을 쓰는 이유는, 동료들 손에 "마케팅 글 자동화" 라는 가치를 일단 쥐여주는 게 더 우선이라고 판단했기 때문이다. 어드민에서 버튼 누르면 몇 초 안에 글 초안이 나오는 경험 — 이게 운영 채널 하나당 작업 시간을 눈에 띄게 줄였다.

완벽한 추상화를 먼저 만들었으면 동료들은 한참 뒤에야 자동화를 손에 쥘 수 있었을 거다. 그건 너무 늦었다고 본다. 깔끔한 경계를 먼저 그으려고 했다면 아마 지금쯤 코드만 예쁘고 아무도 안 쓰는 서버가 됐을 것 같다.

다음 시즌의 숙제

요즘 다시 보면서, 이 서버가 한 번 더 쪼개져야 할 시점인 것 같다. AI 서버는 AI 응답을 안정적으로 받아오고, 출력 schema 를 검증하고, 토큰/비용/출처 같은 메타데이터를 정직하게 돌려주는 데만 집중하는 게 맞는 것 같다. 마케팅 글이라는 도메인이 알아야 할 검증 룰은 마케팅 도메인 서비스로 옮기고, AI 서버는 "프롬프트 + 모델 + 출력 schema 받으면 결과 서빙해주는 신뢰성 있는 박스" 로 좁히는 게 깔끔하다.

처음 설계할 때부터 그렇게 그어야 했나 싶기도 한데, 또 그때 경계를 너무 일찍 그었으면 동료들이 마케팅 자동화를 못 써봤을 것 같기도 하다. 그 사이 어딘가에 답이 있는 것 같다.

다음에 또 다른 AI 기능을 만들 때는, 이 서버에 그냥 박을지 아니면 도메인 서버로 옮길지부터 한 번 더 멈춰서 고민해보려고 한다.