2026-04-14 (화) 개발일지
오늘의 TIL: 사주 소개팅 앱 고도화와 권한 시스템 개선의 하루
오늘은 정말 여러 프로젝트를 넘나드는 하루였습니다. 사주 소개팅 앱의 고도화 작업을 마무리하고, 소명처리 시스템의 권한 문제를 해결했으며, Pulse 엔지니어링의 설계를 개선했습니다. 각각의 문제와 해결 과정을 회고해보겠습니다.
사주 앱: 매칭 시스템과 사진 검열 구현
문제 → 시도 → 해결 → 배운 점
1. 매칭 카드에 JSON이 그대로 표시되는 치명적 버그
아침부터 사용자로부터 “매칭 카드에 raw JSON이 표시된다”는 보고를 받았습니다. 확인해보니 match.dart의 MatchCandidate.fromJson에서 summaryRaw['summary']를 찾는데, 백엔드 응답 구조가 바뀌어 해당 키가 없었습니다. 결과적으로 .toString()이 호출되어 전체 JSON 문자열이 카드에 출력되는 참사였죠.
시도: 먼저 백엔드의 matching.py 라우터를 확인했습니다. compat 딕셔너리 전체를 내려보내고 있어서, Flutter에서 원하는 summary 키를 찾지 못하는 것이었습니다.
해결: 백엔드에서 compatibility_comment 필드를 명시적으로 추가하고, Flutter 측에서는 compatibilitySummary를 우선 사용하도록 수정했습니다. matching_screen.dart에서도 maxLines 제한을 추가해 텍스트가 넘치지 않게 했습니다.
배운 점: 백엔드-프론트엔드 간 데이터 계약(contract)이 변경될 때는 양쪽을 모두 확인해야 합니다. 특히 fromJson 메서드는 null-safe하게 작성하는 습관이 중요하네요.
2. 사진 자동 검열 시스템 구현
사용자가 요구한 “앨범형 프로필 사진 + 자동 검열” 기능을 구현해야 했습니다. 소개팅 앱에서 필수적인 기능이지만, 처음 구현해보는 기술 스택이었습니다.
시도: 오픈소스 검열 알고리즘을 리서치했고, NudeNet(NSFW 탐지)과 OpenCV(얼굴 인식) 조합이 적합하다고 판단했습니다. pip3 install nudenet opencv-python-headless Pillow로 설치를 시작했는데, NudeNet이 첫 실행 시 모델을 다운로드하느라 시간이 좀 걸렸습니다.
해결: app/services/moderation.py를 새로 생성했습니다. 주요 로직은:
_try_nudenet(): NSFW 점수 계산 (0.45 이상이면 거부)_try_face_detection(): Haar Cascade로 정면 얼굴 감지analyze_photo(): 두 검사를 실행하고 결과를 JSON으로 반환
Photo 모델에 moderation_result JSONB 컬럼을 추가하고, 사진 업로드 시 자동으로 검열이 실행되도록 photos.py 라우터를 수정했습니다.
배운 점: opencv-python-headless를 사용하면 GUI 의존성 없이 순수 이미지 처리만 가능합니다. 실제 서비스에서는 비동기 처리나 큐 시스템을 도입해야 할 것 같습니다.
3. SMS/이메일 인증 시스템
휴대폰 인증 기능을 구현하면서 NCloud SMS API와 FastAPI-Mail을 사용했습니다.
문제: User 모델에 is_phone_verified 컬럼이 없어서 마이그레이션이 필요했고, 인증 코드 관리용 별도 테이블이 필요했습니다.
해결: VerificationCode 모델을 생성하고, 6자리 랜덤 코드를 생성해 Redis나 DB에 저장하도록 했습니다. NCloud는 월 100건 무료라 개발 단계에 적합했고, 실제 운영에서는 Twilio 등으로 교체할 계획입니다.
흥미로운 점: 사용자가 “회사메일 인증시스템도 오픈소스에서 사용할수있으면”이라고 요청했는데, 실제로 기업용 소개팅 앱에서는 회사 이메일 도메인으로 사용자 신원을 확인하는 경우가 많더군요.
소명처리 시스템: BRANCH_ADMIN 권한 문제 해결
문제 → 시도 → 해결 → 배운 점
1. BRANCH_ADMIN의 SIEM 설정 접근 문제
“BRANCH_ADMIN 미들웨어 경로 제한이 무슨 말이냐”는 질문을 받고 코드를 살펴보니, 조합회 관리자(BRANCH_ADMIN)가 접근하면 안 되는 API를 미들웨어에서 차단하는 로직이었습니다.
문제 인식: 기존에 “BRANCH_ADMIN은 SIEM 설정 접근 자체가 불필요하다”고 결정했는데, 코드에는 읽기 전용 접근이 추가되어 있었습니다. 중앙회에서 설정하면 site_guid로 자동 상속되는 구조였기 때문에 조합회 관리자의 설정 접근은 불필요했습니다.
해결: tenant_scope.py 미들웨어에서 BRANCH_ADMIN의 /api/v1/tenants와 /api/v1/users 접근을 GET-only로 제한하고, SIEM 설정 관련 경로는 완전히 제거했습니다. 프론트엔드의 SiemConnectionTab.tsx에도 “중앙회에서 상속받은 설정입니다”라는 배너와 읽기 전용 UI를 추가했습니다.
배운 점: 권한 정책을 코드로 구현할 때는, 비즈니스 결정사항과의 일관성을 꼭 확인해야 합니다. 특히 금융권 보안 시스템에서는 “최소 권한의 원칙”이 매우 중요하네요.
2. 역할명 변경: ANALYST → OPERATOR, REVIEWER → APPROVER
사용자 경험을 위해 역할명을 더 직관적으로 변경하는 작업을 했습니다.
시도: backend/app/models/user.py에서 UserRole enum을 수정하고, 모든 관련 파일에서 일괄 변경을 실행했습니다. grep -rl로 모든 발생 위치를 찾아내는 게 핵심이었습니다.
주의사항: CaseStatus.NEEDS_ANALYST_REVIEW는 역할명이 아닌 상태값이므로 변경하지 않았습니다. 이런 세부사항을 놓치면 시스템이 망가질 수 있죠.
배운 점: 대규모 리팩토링 시에는 마이그레이션 스크립트를 반드시 작성해야 합니다. 043_rename_analyst_reviewer_roles.py를 만들어 DB의 기존 데이터도 함께 업데이트했습니다.
Pulse 엔지니어링: 설계 개선과 병렬 처리 충돌 해결
문제 → 시도 → 해결 → 배운 점
1. SKILL.md의 비대화 문제
pulse/SKILL.md가 475줄에 달해 매번 컨텍스트를 낭비하고 있었습니다. UI 스택 라이브러리 테이블(50줄), BUILD 3단계 상세 설명 등이 모두 포함되어 있었죠.
해결: 참조 파일로 분리했습니다. references/ui-stack-guide.md, references/build-protocol.md를 생성하고 SKILL.md는 130줄로 슬림화했습니다. 이제 핵심 워크플로우만 담겨 있어 가독성이 크게 향상되었습니다.
부가 효과: Tier 번호 불일치(pulse/SKILL.md와 pulse-qa), 계약서 경로 문제도 함께 해결했습니다.
2. 병렬 에이전트 결과물 충돌 방지
가장 어려웠던 문제였습니다. 여러 에이전트가 병렬로 작업할 때 API 명세서, 기획, 구현 방식, 결과물이 충돌하는 문제를 해결해야 했습니다.
해결: architecture.template.md를 생성해 “아키텍처 계약서” 패턴을 도입했습니다. 모든 병렬 작업 시작 전에:
- API 엔드포인트 사양
- 데이터 모델 정의
- 파일 구조 합의 를 명시적으로 계약으로 작성하도록 했습니다.
되감기 프로토콜: references/rewind-protocol.md에 “충돌 감지 → 원인 분석 → 되감기 → 재시도” 절차를 문서화했습니다. 이제 에이전트들이 서로의 작업을 덮어쓰는 일이 크게 줄었습니다.
배운 점: AI 에이전트 간 협업에서는 인간 팀의 협업보다 더 명시적인 커뮤니케이션이 필요합니다. 암묵적인 이해를 기대하면 항상 충돌이 발생하더군요.
오늘의 통합적 교훈
세 프로젝트를 오가며 느낀 공통점은 “컨텍스트 관리의 중요성” 이었습니다. 사주 앱에서는 백엔드-프론트엔드 간 데이터 컨텍스트, 소명처리 시스템에서는 역할별 권한 컨텍스트, Pulse에서는 에이전트 간 작업 컨텍스트를 명확히 하는 것이 핵심이었습니다.
특히 Pulse의 아키텍처 계약서 패턴은 다른 프로젝트에도 적용할 수 있을 것 같습니다. 다음에 API 명세를 작성할 때는, 협업하는 동료뿐 아니라 미래의 자신(또는 AI 어시스턴트)를 위한 명확한 컨텍스트 제공을 고민해봐야겠습니다.
삽질한 점이라면… 사주 앱의 궁합 점수 계산 로직에서 element_counts와 ohaeng_analysis.counts 필드명 불일치를 발견하는 데 30분을 썼습니다. 결국 compatibility.py와 saju_engine.py를 대조해보니 간단한 문제였는데, 이걸 왜 진작에 코드 리뷰나 테스트로 발견하지 못했을까 하는 아쉬움이 남습니다.
오늘의 한줄
“컨텍스트를 명확히 하는 것이 모든 협업(인간-인간, 인간-AI, AI-AI)의 첫걸음이다.”