본문 바로가기

전체 글

(123)
네트워크가 끊겼는데 답변은 이미 만들어졌다면? 채팅 재시도(Retry) 판별과 응답 복구 🚨 발단: 재시도 버튼을 눌렀을 때 LLM을 다시 돌려야 할까, 저장된 답을 줘야 할까?모바일 환경에서 유저가 AI 캐릭터에게 메시지를 보냈는데, 지하철에 들어가며 네트워크가 잠깐 끊겼다고 해보자. 화면에는 "전송 실패 - 다시 시도" 버튼이 떴다.하지만 서버 내부에서는 이미 LLM이 답변을 다 만들고 DB에 저장까지 마쳤을 수도 있고, 반대로 답변을 반쯤 만들다가 에러가 났을 수도 있다. 이때 유저가 재시도(Retry)를 누르면 어떤 문제가 생길까?반쪽짜리 답변 복구: AI의 한 턴 답변은 [행동 지문, 첫 번째 대사, 두 번째 대사, 생성된 이미지]처럼 여러 개의 메시지 조각(ChatMessage)으로 나뉘어 저장된다. 만약 DB에 첫 번째 대사 조각 하나만 들어있는 상태에서 "응답이 있네!" 하고..
이벤트 쿠폰을 동시에 연타하면 보상이 두 번 나갈까? 쿠폰 동시성 제어와 버전 캐시 🚨 발단: 같은 그룹의 쿠폰을 두 기기에서 동시에 입력한다면?프로모션 기간에 선착순이나 1인 1회 한정 쿠폰(WELCOME2026), 혹은 "여름 이벤트 쿠폰 A·B·C 중 계정당 1개만 사용 가능"한 그룹 쿠폰(Group)을 배포할 때 가장 흔히 발생하는 사고는 따닥(동시 요청)을 통한 중복 수령이다.만약 서버가 단순하게 1) 수령 이력 조회(FindOne) → 2) 유저 재화 지급 → 3) 수령 이력 저장(InsertOne) 순서로 동작한다면 어떤 일이 벌어질까?그룹 쿠폰 동시 우회: 유저가 폰에서는 SUMMER_A를, 태블릿에서는 SUMMER_B를 0.01초 차이로 동시에 누르면 두 요청 모두 1번 조회(FindOne)를 통과해 1인 1회 제한인 그룹 보상을 두 번 타가는 버그가 발생한다.쿠폰 캐..
결제창을 두 번 열면 구독이 두 개 생길까? 정기구독 이중 결제 방지와 세션 재사용 🚨 발단: 유저가 결제창을 닫고 다시 '구독하기'를 누른다면?인앱 결제와 달리 외부 웹/모바일 결제창(paymentUrl)을 띄워 진행하는 정기구독(NewPlay 등)에서는 '결제창 생성 시점'과 '실제 결제 완료 웹훅(Webhook) 도착 시점' 사이에 긴 시간차가 존재한다.이 공백 시간 동안 아무런 잠금 장치가 없다면 어떤 일이 벌어질까?구독하기 연타 & 멀티 디바이스 동시 진입: 유저가 모바일과 PC에서 동시에 '구독하기'를 누르거나, 결제창이 떴는데 실수로 닫고 다시 버튼을 누르면 결제사 쪽에 두 개의 독립된 정기구독 세션이 만들어진다. 만약 두 창에서 모두 결제가 일어나면 한 명의 유저에게 매달 두 번씩 돈이 빠져나가는 이중 구독 사고가 터진다.무조건 차단할 때의 UX 악화: 그렇다고 "이미 ..
돈은 나갔는데 아이템이 두 번 지급된다면? 결제 소비(Consume)와 재화 원장 Outbox 패턴 🚨 발단: 재화 지급 직후 DB 응답(ACK)이 끊기면 롤백해야 할까, 말아야 할까?인앱 결제(AppStore, GooglePlay, Toss, NewPlay)에서 영수증 검증(VERIFIED)이 끝난 뒤 실제로 유저에게 유료 재화(CHU)나 구독권을 지급하는 소비(ConsumePurchase) 단계는 가장 치명적인 돈 사고가 발생할 수 있는 구간이다.중복 지급을 막기 위해 먼저 영수증 상태를 VERIFIED → CONSUMED로 선점(Claim)한 뒤 유저의 재화를 올려주는데, 만약 재화 지급 DB 쿼리(UpdateOne)를 날린 순간 네트워크 순단으로 응답(ACK)만 유실되었다면 어떤 일이 벌어질까?무조건 롤백(CONSUMED → VERIFIED)하는 경우: 실제로는 DB에 재화가 반영되었는데 서버..
채널에 넣었다고 전송된 게 아니다: WebSocket 소켓 쓰기 보장과 무중단 Drain 🚨 발단: send 가 성공했는데 왜 클라이언트는 못 받았을까?Go에서 WebSocket 서버(gorilla/websocket)를 구현할 때는 소켓 동시 쓰기(Concurrent Write) 패닉을 막기 위해 흔히 세션마다 전송 채널(send chan []byte)과 전용 쓰기 고루틴(writePump)을 둔다.그런데 기본 오픈소스 구현(SendMessage)은 메시지를 채널 버퍼에 넣기만 하면 무조건 성공으로 간주(Fire-and-Forget)한다. 이 구조로 실시간 AI 스트리밍을 운영하면 두 가지 치명적인 문제가 터진다:가짜 전송 성공과 고루틴 누수: 모바일 유저의 네트워크가 끊겨 실제 소켓 쓰기(conn.WriteMessage)는 실패했는데도, 상위 워커는 채널(send)에 넣었다는 이유만으로 ..
DB가 느린 게 아니었다": 배치 조회(Batch Read)와 Context 취소 전파로 유령 에러 잡기 🚨 발단: 시작 33ms 만에 터지는 DB 에러 로그, 범인은 누구일까?프로덕션 환경의 커뮤니티 추천 목록 조회(GetCommunityRecommend) API에서 간헐적으로 MongoDB 조회 실패(ERROR) 로그가 찍히기 시작했다. 처음에는 "추천 섹션이 많아지면서 DB(Atlas) 쿼리가 느려져 타임아웃이 나는 건가?"라고 의심했다.하지만 로그의 타임스탬프를 뜯어보고 두 가지 놀라운 사실을 발견했다:클라이언트는 시작 33ms 만에 이미 떠났다: DB가 느려서 타임아웃(DeadlineExceeded)이 난 게 아니라, 유저가 앱에서 화면을 빠르게 전환하면서 요청 시작 33ms 만에 클라이언트가 연결을 취소(context.Canceled)한 것이었다.떠난 유저를 위해 서버는 끝까지 루프를 돌며 에러..
Go 채팅 서버와 Python AI 에이전트의 공존: 비동기 과금 정산기 (Accountant) 🚨 발단: 한 번의 대화에 여러 서비스의 비용이 제각각 도착한다면?고성능 실시간 입출력을 위해 게이트웨이와 WebSocket 채팅 서버(chat)는 Go(Kratos)로 만들고, 복잡한 프롬프트와 그래프 오케스트레이션이 필요한 AI 엔진(model)은 Python(LangGraph)으로 분리했다.그런데 유저가 음성으로 말을 걸고 AI가 음성으로 대답하는 한 턴의 대화가 일어날 때, 과금(정산) 관점에서는 복잡한 문제가 발생한다. 한 번의 대화에 들어가는 비용 보고서가 여러 곳에서, 그것도 서로 다른 시점에 날아오기 때문이다:요청 측 비용: 유저의 음성을 텍스트로 변환한 STT 비용 (Go 상행 워커에서 발생)응답 측 비용 1: 대화 생성, 기억 분류, 도구 호출에 쓰인 LLM 토큰 비용 (Python ..
LLM 스트리밍부터 감정 TTS까지: 지연 시간을 줄이는 실시간 대화 조립기 (Assembler) 🚨 발단: LLM 답변이 끝날 때까지 기다렸다가 음성을 만들면 어떻게 될까?실시간 AI 음성 대화에서 유저가 가장 민감하게 느끼는 지표는 첫 번째 목소리가 들리기까지 걸리는 시간(TTFA, Time-To-First-Audio)이다. 만약 LLM이 34문장짜리 답변을 모두 생성할 때까지 기다렸다가 TTS(음성 합성) API를 호출하면, 유저는 최소 46초 동안 어색한 침묵을 견뎌야 한다.그렇다고 LLM에서 스트리밍으로 떨어지는 토큰(글자 조각)을 그대로 TTS에 넘기면 또 다른 참사가 벌어진다:부자연스러운 끊어 읽기: 문장이 완성되기 전에 조각 단위로 음성을 합성하면 억양이 뚝뚝 끊기거나 괄호((웃음)) 안의 지문까지 어색하게 읽어 버린다.종료 마커(reply_end)의 추월 현상: 텍스트 스트리밍은 메..

LIST