1왜 이렇게 만들었는가
금융분야에서 AI를 쓸 때 가장 중요한 과제는 「AI를 활용하면서 어떻게 신뢰도를 확보할 수 있는가」라고 생각합니다. 저는 그 질문을 기존의 안정적인 개발기술과 융합하여 답하고자 했습니다.
AI의 장점을 극대화하면서 동시에 단점을 최소화 할 수 있게 서비스의 범위를 조절하고, 기존의 신뢰 가능한 기술을 활용하여 리스크를 최소화했습니다.
AI시대에 걸맞는 스마트 금융 서비스를 개발하려면 결국 AI의 편의성을 필두로 하는 생산성 향상 효과는 차용하되, 리스크를 다룰 줄 아는 개발자가 될 필요성을 느꼈습니다.
| 대상 | 개선에만 의존하지 않은 것 | 범위 조절 | 그 자리를 맡은 기술 |
|---|---|---|---|
| 판정 | LLM 정확도 튜닝 | LLM은 고객대응만 | screen_loan 코어 함수 |
| 파싱 | LLM 파싱 신뢰도 | LLM 파싱을 후보군으로 한정 | 사람이 확정하는 폼 (+ 두 파서 후보를 대조하는 API) |
| 인젝션 | 입력 필터 강화 | 판정 로직을 LLM과 분리 | 공격 표면 자체를 제거 |
| 수치 | 환각 억제 프롬프트 | 리스크가 없는 단순 설명문으로 한정, 수치는 배제 | 확정값 주입 + Eval 수치근거 지표 |
| 품질 | 프롬프트 튜닝 반복 | 정량평가 진행 | Eval 하네스 + 결함 주입 역검증 |
이 서비스를 설계하면서 가장 중요하게 생각했던 부분은 LLM이 가지고 있는 장단점을 명확하게 구분하여 적재적소에 도입하는 것이었습니다. LLM의 범위 조절 및 리스크관리가 중요한 부분에서는 함수 로직을 사용하도록 설계하여 문제를 풀어냈습니다.
2LLM 기능은 어디서 작동하는가
- 사람자연어 입력
-
LLM[API] parsing-preview규칙 파서 · LLM 파서를 나란히 실행 → 두 후보와 불일치 필드를 그대로 응답 (이 단계에서는 어느 쪽도 채택하지 않음)사람[데모 화면]규칙 파서가 폼을 채움 → 사람이 확인·수정
- 사람확정된 폼 값심사에 들어가는 유일한 값
- 코어 함수
screen_loan()LLM 미개입 · 판정 · DSR · 추천 - LLMLLM 안내문 생성확정값을 데이터로 주입
- 코드 검증Eval 6지표 통과분만 공개
LLM이 닿는 곳은 입구(파싱 후보)와 출구(안내문) 두 군데로, 정확한 판단이 필요한 핵심 로직에서는 일부러 배제했습니다.
3이 설계가 실제로 작동하는가
개발 과정에서 실제로 두 개의 파서 대조를 통해서 오류를 탐지할 수 있었습니다.
사례 1 — LLM 파서가 「부채 200만원」을 20만원으로 전달
기능 테스트를 위해 API키로 파이프라인을 돌리다가 발견한 상황입니다.
| 부채 | 희망금액 | |
|---|---|---|
| 입력값 | 200만원 | 500만원 |
| LLM 파서 | 200,000 | 500,000 |
| 규칙 파서 | 2,000,000 | 5,000,000 |
| 확정 | 규칙 파서 값 | 규칙 파서 값 |
단순 LLM 파서만 사용할 경우 10배나 차이나는 금액을 기준으로 심사가 진행될 수도 있다는 것을 확인할 수 있었습니다.
사례 2 — 규칙 파서가 「3,000,000」을 300억으로 잘못 읽음
배포 후 추가 테스트 과정에서 발견한 사항인데, 일반적으로 유저들이 단위를 생략하는 경우가 많다보니(「월급 700」 = 700만원) 단위 미기재 시 만원으로 보도록 설계했었습니다.
그러다 보니 금액 전부를 작성하는 유저들에 대해서 오류가 발생하게 됐고, 이를 고치기 위해서 로직을 변경하여 일반적으로 발생할 수 있는 금액의 범위를 조정했습니다.
예를 들어 콤마의 개수를 확인, 하나면 만원 표기에서 흔한 자리수이므로 만원 관행을 따르고, 둘 이상이면 만원으로 읽을 경우 백억을 넘어가기 때문에 일반적이지 않다고 판단, 단위를 원으로 받도록 변경했습니다. 추가로 열여섯 가지 표기를 회귀 테스트로 고정했습니다.
그래도 예외적으로 발생할 수 있는 부분들이 있다고 판단하여 최소한의 안전장치를 추가했습니다. 「500,000」은 콤마가 하나라 만원 관행이 적용되어 50억으로 파싱되는데, 실제로는 50만원을 적은 사용자일 수도 있습니다. 어떤 표기 규칙도 이 구간을 양쪽 모두 맞출 수는 없다고 판단, 규칙을 더 얹어 덮는 대신 실행 전에 금액을 확인했다는 체크를 사람이 직접 하게 하고 그 한계를 회귀 테스트로 적어 두었습니다.
4코드 품질을 어떻게 담보했는가
코드 결과를 단순히 사람이나 AI가 확인하는 것을 넘어서, 매번 같은 기준의 로직을 통해 품질 일관성을 확보할 수 있도록 설계했습니다.
| 무엇이 | 어디까지 | 무엇을 막는가 |
|---|---|---|
| 테스트 | 249개 (DB 통합 포함) | 고쳤던 결함이 다시 등장하는 것 |
| Eval | 90/90 — 15케이스 × 6지표 | 안내문이 확정값과 어긋나는 것 |
| Eval 역검증 | 결함 주입 7종 | 채점기가 검증 없이 통과만 시키는 것 |
| 기계 게이트 | 제출 전 자동 차단 — 현재 BLOCK 1 · WARN 9(커밋 모드 10) | 명세·코드 괴리, 비밀키 노출, 미사용 표면 존재 여부 등 |
| 부하 시험 | 535 req/s · 동시 요청 100건 · 실패율 0% · p95 268ms | 부하 상황에서 장애가 발생하는 것 |
| ADR | 37건 | 기각했던 대안에 대한 아이디어 기록이 소실되는 것 |
각 장치가 실제로 하는 일
Eval 6지표 — 파싱정확도 · 판정정합성 · 추천정합성 · 수치근거 · 조건부표현 · 디스클레이머 6가지 지표로 구성되어 있습니다. LLM의 응답이 고객에게 도달하기 전에 이 지표를 바탕으로 검증, 책임지지 못할 문구가 그대로 전달되지 않도록 개발했습니다. 「수치근거」는 안내문에 나온 모든 숫자가 상품 CSV나 확정 판정값에 실제로 있는지 대조하고, 「조건부표현」은 “승인합니다” 같은 단정적인 표현을 막아냅니다.
결함 주입 7종 — 90/90은 채점기가 정상적으로 작동하지 않아서 결함을 잡아내지 못하는 경우에도 나올 수 있는 검증점수입니다. 그래서 지표마다 일부러 틀린 안내문을 넣고 해당 지표를 올바르게 잡아내는지 확인하도록 했습니다. 금리를 CSV에 없는 2.0%로 바꾸면 수치근거 하나만 점수가 떨어져야 하고, 다른 부분도 함께 감점이 발생하면 채점기가 오작동한다는 것을 잡아낼 수 있었습니다.
기계 게이트 — 제출 전에 gate.py가 명세와 코드의 차이, 소스에 박힌 비밀키, 정의만 되고 호출되지 않는 함수, 커밋 준비 상태를 봅니다. 통과 도장이 아니라 차단이 목적입니다.
부하 시험 — 동시 요청 100건을 60초 동안 쉬지 않고 밀어 실패율과 p95를 같이 봅니다. 평균만 보면 꼬리에서 무너지는 것을 놓칩니다.
코드만 읽어서는 나오지 않는 결함
위 장치는 전부 코드를 대상으로 검토하도록 만들었습니다. 따라서 코드 자체에는 문제가 없더라도 발생하는 결함들이 따로 있었습니다.
- alpine의 postgres는 uid 70인데 데비안 값 999를 적어 둔 것 — 컨테이너가 볼륨을 소유하지 못해 initdb가 멈춥니다.
docker compose up을 처음 돌렸을 때 발견, 수정 반영했습니다. - 이미지에 마이그레이션 파일이 들어가지 않아 스키마를 만들 수단 자체가 없던 것
- 저장소 안의 Caddy 설정·마운트·compose가 전부 옳았는데 도메인이 DNS에 없어 인증서가 발급되지 않던 것.
- CSS 문법이 옳고 테스트도 전부 통과하는데, 알림 컨테이너를 통째로 칠하는 바람에 안내(파랑)까지 경고(노랑)로 보이던 것.
5구현하지 않은 것과 지금 형태로는 안 되는 것
공개 데모에서 동작하지 않는 것
- 안내문 생성의 대기열 처리. 안내문은 심사와 분리해 대기열에 쌓고 별도 워커가 가져가 처리하도록 만들었습니다. LLM이 느리거나 실패해도 판정 응답이 거기 붙잡히지 않게 하려는 구조입니다. 공개 서버에는 그 워커를 띄우지 않아서, 방문자가 자기 키를 넣으면 요청 안에서 바로 생성됩니다. 대기열 경로는 로컬 실행에서 볼 수 있습니다
- 두 파서 교차검증 화면. API 엔드포인트로는 존재하지만 화면에 따로 노출시키지는 않았습니다. 화면은 규칙 파서만 쓰도록 설계했습니다.
- 적격 상품 전체 목록. 현재 코드상에서 실제 심사 경로는 상위 3건만 저장하므로 화면도 3건만 보여주도록 만들었습니다.
- 금액 자리수의 자동 판별. 「500,000」처럼 표기만으로 만원인지 원인지 명확하게 판단되지 않는 값이 있습니다. 기재 단위를 화면에 고지하고, 실행 전 금액 확인 체크를 사람에게 받는 것으로 보완했습니다.
구조상 지금 형태로는 안 되는 것
- 방문자별 요청 상한. 명세는 클라이언트 주소 기준이라고 적었는데, 브라우저 → 프록시 → 화면 컨테이너 → API 구조에서 API가 보는 주소는 언제나 화면 컨테이너 하나입니다. 방문자를 구분하지 못하고 사실상 인스턴스 전체 상한으로 동작합니다. 넉넉히 잡아 부작용을 줄였고, 한계를 기록해 두었습니다
- 인증·권한. 데모프로젝트이다 보니 사칭문제에서 비교적 자유로워서 아직 구현하지는 않았고, AI서비스를 기존 금융 서비스에 결합하는 방식에 더 집중했습니다. 실제로 상용되는 서비스로 발전된다면 가장 중요한 요소 중 한가지 이므로 개발이 필요합니다
검토는 했지만 미구현한 것
- 내부 구간 mTLS — 지금은 화면·API·DB가 한 호스트 안의 컨테이너라 내부 통신이 도커 네트워크 밖으로 나가지 않기 때문에 단일 노드 데모에 과도하다고 판단했습니다.
app을 외부에 노출하거나 화면을 다중화하거나 DB를 별도 인스턴스로 옮기면, 내부 통신이 호스트 경계를 넘게 되며 필수로 바뀌므로 그 때 도입해야 한다고 메모해두었습니다.
6금융권에서 중요한 '규제'를 어떻게 다뤘는가
데모 프로젝트이다 보니 규제 대상이 되는 기능을 의도적으로 구현 범위에서 제외하는 방향으로 설계했습니다.
데모 프로젝트이다 보니 실제 규제사항을 지켜서 개발할 수 있는 환경이 아니었기 때문에, 애초에 그 조건에 들어가지 않도록 기능의 범위와 책임 경계를 설계했습니다. 대신 규제별로 조건들을 검토하여 추후 만약 실서비스를 다루게 된다면 어떤 사항들을 확인하고 적용해야 하는지 별도로 학습했습니다.
| 조건 | 실무에서 | 이 데모 |
|---|---|---|
| 구속력 있는 승인·거절 | 개인정보보호법 제37조의2 (자동화된 결정) | 자동으로 승인·거절을 확정하지 않습니다. 사람이 결정하도록 만들었습니다 |
| 상품 추천·중개 | 금융소비자보호법 제17·19조 | 실제 금융상품이 아닌 합성 상품표를 사용했습니다 |
| 실제 개인신용정보 | 개인정보보호법 제15·16·21·30조, 신용정보법 | 화면에 입력 금지를 고지하고 별도로 받지 않도록 만들었습니다 |
| 전자금융업 | 전자금융거래법 제2조 제1호, 제21조 | 운영 주체가 금융회사가 아니고, 자금 이동·결제·계약 체결이 없으며, 상담원이 최종 결정하므로 「종사자와 의사소통 없이 자동화된 방식」에 해당하지 않습니다 |
금융 IT에서 통용된다고 알려진 표준들도 따로 확인해본 결과, 제가 파악한 적용 체계는 전자금융감독규정·ISMS-P·ISO/IEC 27001이었습니다. IEEE 830·1471·829는 이미 ISO/IEC/IEEE 표준으로 대체·폐지된 것으로 파악되고, 이 프로젝트에 실질적으로 걸리는 것은 금액 계산의 IEEE 754였습니다. 인증이나 심사를 받은 것은 아니므로 준수했다고 말하지는 않습니다. 이 체계들의 취지를 기준 삼아 판단했고, 무엇을 기준으로 무엇을 판단했는지를 문서에 남겼습니다.
실서비스로 전환한다면 검토할 것: 사용자 인증과 자원 소유권, 마이그레이션·런타임 권한 분리, 개인정보 영향평가, 모델 리스크 관리, 감사 로그 보존기간.
7만든 것
- 백엔드 FastAPI 모듈러 모놀리스 — 독립 배포와 컴포넌트별 확장이 필요한 규모가 아니라 프로세스는 하나로 두고, 화면·API·DB는 컨테이너와 네트워크로 분리했습니다 · PostgreSQL · Alembic 수동 마이그레이션
- 정합성 단일 트랜잭션 저장 · Idempotency-Key + 요청해시 + DB UNIQUE 제약 ·
FOR UPDATE SKIP LOCKED - 재현성 규칙·상품·모델·프롬프트 4종 버전을 판정 결과에 함께 저장
- 배포 AWS EC2 · Docker Compose 3-tier · Caddy 리버스 프록시가 유일한 외부 진입점 · Let's Encrypt 자동 갱신
- 이 페이지도 같은 인스턴스에서 Caddy가 직접 서빙합니다. 앱이 죽어도 이 링크는 살아 있도록 컨테이너와 분리했고, 재부팅 후 복구를 실측했습니다. 다만 인스턴스가 한 대라 가용영역 자체가 죽는 장애는 이 구성으로 막지 못합니다