2026-08-20 (목) 개발일지
오늘은 여러 프로젝트를 오가며 체계적으로 문제를 파악하고 정리하는 날이었다. 사주 앱의 매칭·채팅 플로우 구현부터 시작해, 결제 게이팅 설계 검증, 그리고 제안서 자동화 도구의 DB 제약 버그까지 — 각각 다른 성질의 트러블슈팅이었지만, 모두 “가정 → 검증 → 발견” 사이클을 돌았다.
사주 앱 매칭·채팅 플로우 — 실제 API 시뮬로 E2E 검증
처음엔 단순히 백엔드 엔드포인트들이 정상 작동하는지만 확인하려고 했다. 그런데 실제 앱 사용 흐름을 따라가려면 가입 → 매칭 → 좋아요 → 채팅 전체를 시뮬해야 했고, 로컬 개발 환경에서 두 사용자를 동시에 띄울 수는 없었다. 모바일 에뮬레이터 두 개를 켜면 내 컴퓨터가 감당하지 못하니까.
그래서 다른 방법을 택했다. 백엔드 채팅 API를 직접 호출하는 에이전트 두 개를 만들어, 실제 사용자 입장에서 대화를 나누게 한 것이다. 서연(여, 28세, uid 14)과 지훈(남, 31세, uid 15)이라는 페르소나를 만들고, 둘이 궁합 81.9점으로 매칭된 후 room 3에서 실제 채팅 엔드포인트로 5턴을 주고받았다.
두 에이전트가 각각 실제 채팅 API로 대화를 나눴고, 서로 턴을 주고받으며 자연스럽게 이어갔다.
서연이 먼저 인사를 건넸고(“지훈님 안녕하세요! 저 서연이에요 ㅎㅎ 매칭돼서 반가워요~”), 지훈이 답장했다. 그 다음 서연이 일상 얘기를 꺼내며 질문을 던졌고, 지훈이 커피 얘기로 응했다. 마지막엔 지훈이 “좋아하는 카페 데려가달라”는 데이트 제안까지 꺼냈다. 중간에 궁합 점수(81.9점)는 자연스럽게 한 번만 언급해서, 앱의 핵심 가치를 부자연스럽지 않게 녹여냈다. 메시지 5개가 모두 실제 DB에 저장되고, API 응답도 정상이었다. 이제 매칭부터 채팅까지의 플로우는 검증됐다.
결제 게이팅 설계 — 코드 감사로 명시된 위반 찾기
다음으로 결제 로직이 설계 명세와 맞는지 검증하는 작업을 시작했다. 오연 앱의 핵심 제약이 네 가지였다:
- 프리미엄 궁합 콘텐츠는 결제한 쪽만 볼 수 있다 (상대는 못 봄)
- 매칭 상대 목록 노출은 결제 여부와 무관하게 항상 공개
- 결제 후 생성되는 “선호도 예상 질문 힌트” 콘텐츠가 있어야 함
- 매칭 쌍이 한 번 결제하면 둘 다 채팅을 열 수 있다 (한 사람의 결제로 양쪽 unlock)
코드를 직접 파서 각 항목을 검증했다. match.py의 Entitlement 모델을 읽어보니, 결제 상태가 paid_by_a_at과 paid_by_b_at 두 필드로 개별 저장되고 있었다. 즉, A가 결제해도 B의 필드엔 기록이 안 남는 구조였다. 그러면 사항 4(match-shared unlock)가 위반된다.
채팅 라우터(chat.py)를 grep 해봤더니, 결제 여부를 전혀 확인하지 않고 있었다. 누구든 매칭되면 바로 채팅을 열 수 있었다. 이건 설계와 달랐다. 결제 게이팅이 아예 없었던 것이다.
프리미엄 콘텐츠 생성(matching.py의 generate_personal_analysis)을 살펴봤더니, LLM 출력이 정확히 8개 키를 가지고 있었다: outer_compat, mental_compat, life_style_compat, luck_cycle, advice, warning, date_idea, gift_idea. 하지만 “선호도 예상 질문 힌트”는 없었다. 사항 3(c)이 구현되지 않은 상태였다.
이 검증 과정에서 중요한 건, 코드를 단순히 “읽는” 게 아니라 실제 데이터 흐름을 따라가며 근거를 수집하는 것이었다. 각 파일에서 해당 로직을 찾아 라인 번호와 함께 기록했고, 최종 보고서에는 “어디서 뭘 봤는지”가 명확히 남았다.
제안서 자동화 도구 — DB CHECK 제약 위반으로 인한 연결 끊김
마지막으로 제안서 자동화 도구에서 “Failed to fetch” 에러가 났다. 처음엔 단순한 네트워크 오류라고 생각했다. 하지만 VM 로그를 보니 SQLite3 CHECK 제약 위반이 터지고 있었다.
sqlite3.IntegrityError: CHECK constraint failed: via IN ('표','LLM','붙여넣기')
req_runs 테이블의 via 컬럼에 CHECK 제약이 있었는데, 최근 병합(8a9a6ce)에서 표와 LLM을 하나로 합치며 새로운 via 값을 만들어냈다. 그런데 DB 스키마는 아직 옛 3개 값만 허용하고 있었다. 그래서 INSERT가 실패했고, 서버가 크래시했고, 클라이언트는 “연결이 끊겼다”고 본 것이다.
근본 원인은 따로 있었다. 요구사항 병합 로직에서 seq 필드(위치 식별자)를 계산할 때, it["no"] 데이터 필드를 쓰고 있었다. 이건 문서마다 다르고, 특히 여러 소스를 합칠 때 중복이 생길 수 있다. 실제로 골프존 제안서를 테스트해보니, 98건 중에 no 중복이 없어 보였지만, 다른 문서에선 문제가 터질 수 있었다.
올바른 방식은 enumerate 위치값을 쓰는 것이었다. 그러면 어떤 문서든 seq가 항상 1..N으로 유일해진다. 이 수정으로 DB 제약 위반을 원천 차단할 수 있다.
또한 요구사항 추출 후 “신규 여부” 판정이 잘못되고 있었다. 내 mark_origin 함수는 요구사항 라이브러리와만 비교했는데, 사용자 정의상 “신규”는 “기존 장표에 관련 콘텐츠가 없다”는 뜻이었다. 실제로 BM25로 장표 코퍼스를 대조해보니, 신규 라벨이 붙은 14건 전부 강한 장표 매칭(중앙값 51.7, 기존형 중앙값 53.7)을 가지고 있었다. 예를 들어 “이상행위 탐지 시스템”(신규 라벨)은 HL-0088(BM25=90)과 명백히 관련이 있었다.
신규 라벨이 “관련 기존 장표 없음”을 뜻하지 않는다는 걸 발견했다. 실제 BM25 스코어로 대조하니 신규 14건이 전부 강한 매칭을 가지고 있었다.
이 발견으로 내 라벨링 로직 자체가 설계와 어긋난다는 걸 알 수 있었다. 요구사항 라이브러리 비교만으로는 부족했고, 실제 장표 코퍼스 검색 결과를 봐야 했다.
오늘의 한줄
설계를 검증할 땐 문서를 읽는 것만으로 충분하지 않다 — 실제 데이터 흐름을 따라가고, 경계 케이스를 손으로 재현해봐야 진짜 문제가 드러난다.