본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 20. 21:00

케냐를 위한 AI 토지 사기 탐지기를 구축했습니다 — 전체 엔지니어링 스토리

요약

케냐의 심각한 토지 사기 문제를 해결하기 위해 Google Gemini 3 Pro를 활용한 자율 조사 에이전트 'TitleTrust'를 구축한 엔지니어링 사례를 소개합니다. 이 시스템은 멀티모달리티와 긴 컨텍스트 윈도우를 활용하여 소유권 증서, 등기 이력 등 복잡한 문서 간의 논리적 모순을 포렌식 수준으로 탐지합니다.

핵심 포인트

  • Gemini 3 Pro의 네이티브 멀티모달리티와 사고 모드(Thinking Mode)를 활용한 포렌식 감사 기능 구현
  • 단순 OCR을 넘어 문서 간의 시간적, 법적 모순을 추론하는 Chain-of-Thought 방식의 엔진 설계
  • 권리증, 그린 카드, 매매 계약서 등 다양한 부동산 관련 서류의 데이터 무결성 검증
  • 현장 조사관이 모바일로 즉시 사용할 수 있는 자율 조사 에이전트 형태의 서비스 구조

"우리는 하나님을 믿습니다. 토지는 검증합니다." 문제는 개인적입니다. 케냐에서 토지는 단순한 자산이 아닙니다. 그것은 정체성입니다. 가족들은 작은 샴바(shamba, 농지)를 사기 위해 수십 년 동안 저축합니다. 그것은 자녀들에게 보여줄 수 있는 것이며, 당신이 성공했다는 것을 증명하는 것입니다. 그리고 그것은 위조된 서류와 공모한 관리들을 통해, 저항할 방법이 없는 평범한 사람들로부터 체계적이고 대규모로 도난당하고 있습니다. 가장 잔인한 변종은 "에어 서플라이(air supply)"라고 불립니다. 사기꾼이 법적으로 존재하지 않는 토지를 매물로 내놓는 것입니다. 그들은 세련된 브로슈어, 실제처럼 보이는 장소로의 현장 방문, 그리고 설득력 있는 매매 계약서를 가지고 있습니다. 구매자는 대금을 지불합니다. 하지만 소유권 증서(title deed)는 영원히 오지 않습니다. 사기임을 깨달았을 때는 — 때로는 몇 년이 지난 후에야 — 돈은 이미 사라졌고 법적 시스템은 거의 구제책을 제공하지 못합니다. Lesedi Developers 스캔들은 최근 가장 악명 높은 사례입니다. 수천 명의 예비 주택 소유자들이 나이로비(Nairobi) 위성 도시의 유령 단지에 10억 실링(Sh1 billion) 이상의 돈을 잃었습니다. Juja, Ruiru, Thika. 실제 장소들이지만, 토지는 가짜였습니다. 이것이 제가 이 코드를 사용하여 해결하고자 했던 문제입니다.

저를 구축하게 만든 해커톤
저는 TitleTrust를 2026 Google Gemini 3 해커톤에 제출했습니다. 시기가 적절했습니다. Gemini 3 Pro의 네이티브 멀티모달리티(native multimodality), 200만 토큰 컨텍스트 윈도우(two-million-token context window), 그리고 "사고 모드(Thinking Mode)"는 이 모델을 단순한 사실 검색을 넘어, 포렌식 감사관(forensic auditor)처럼 소유권 체인(chain of title)을 추론할 수 있는 최초의 강력한 모델로 만들었습니다. 즉, 무엇이 누락되었는지, 무엇이 시간적으로 불가능한지, 그리고 무엇이 법적으로 무효인지를 탐지할 수 있게 된 것입니다. 이것은 단순한 챗봇 래퍼(chatbot wrapper)가 아닙니다. 이것은 자율 조사 에이전트(autonomous investigation agent)입니다.

TitleTrust가 실제로 하는 일
사용자가 제품을 경험하는 방식으로 설명한 다음, 각 단계의 밑바탕에 깔린 엔지니어링을 설명하겠습니다.

  1. 조사관이 세션을 엽니다
    현장 조사관 — 지역 사회 옹호자, 지역 관리, 구매 대리인 — 이 휴대폰에서 TitleTrust를 열고 새로운 조사를 시작합니다.

그들은 "거래 패키지(deal pack)": 권리증(Title Deed), 그린 카드(Green Card, 공식 등기 이력), 변동 양식(Mutation Form, 분할 기록), 그리고 매매 계약서(Sale Agreement)를 업로드합니다. 세션이 시작됩니다. 화면에 실시간 타임라인이 나타납니다. 요원들이 업무를 시작합니다.

  1. 포렌식 요원(Forensic Agent)의 문서 판독
    포렌식 엔진(ForensicEngine, backend/forensic_engine.py)은 업로드된 이미지와 PDF를 가져와, 사고의 사슬(Chain-of-Thought) 추론을 강제하는 구조화된 프롬프트(Structured Prompts)와 함께 Gemini 3 Pro를 통해 실행합니다. 이 엔진은 단순히 문서의 OCR(광학 문자 인식)을 수행하는 것이 아닙니다. 문서를 바탕으로 추론합니다. 모든 날짜, 모든 거래, 모든 당사자 이름을 추출한 다음 다음 사항들을 확인합니다: 법적 소유권을 갖기 전에 토지를 양도할 수 있는가? (아니오.) 근저당 해지(Discharge of Charge)가 근저당 설정(Charge)보다 앞설 수 있는가? (아니오 — 하지만 위조된 그린 카드는 이를 시도합니다.) 변동 양식(Mutation Form)에 기재된 측량사(Surveyor)가 면허를 보유하고 있는가? (관보(Gazette) 기록과 교차 참조.) 분할된 필지들의 면적 합계가 모권리증(Mother Title)의 면적을 초과하는가? (만약 그렇다면: 초과 분양 사기(Oversubscription fraud).)

여기서 Gemini의 사고 모드(Thinking Mode)가 중요한 이유: 표준 LLM(대규모 언어 모델)은 법률이 충돌할 때 타협안을 만들어내는 환각(Hallucination) 현상을 일으킵니다. 카운티(County) 용도 지역 지도에는 "주거 지역"이라고 되어 있지만, 국가 토지법(National Land Act)에는 "하천 보호 구역(Riparian Reserve)"이라고 되어 있을 수 있습니다. Gemini 3의 include_thoughts=True 설정은 모델이 계층 구조(국가법이 카운티법보다 우선함)를 통해 추론하도록 강제하며, 가시적인 추론 흔적(Reasoning Trace)과 함께 방어 가능한 판결을 내놓게 합니다. 그 흔적은 단순히 있으면 좋은 기능이 아닙니다. 이 맥락에서 추론 흔적 자체가 곧 제품입니다. 조사관, 변호사 또는 판사는 AI가 단순히 무언가를 플래그(Flag)했다는 사실뿐만 아니라, 왜 그렇게 했는지에 대한 이유를 확인해야 합니다.

  1. 위치 요원(Location Agent)의 물리적 실체 확인
    지리 공간 엔진(GeospatialEngine, backend/geospatial_engine.py)은 다른 질문에 답합니다: 해당 토지가 그들이 말하는 위치에 실제로 존재하는가? 가장 교묘한 사기 유형은 미끼 상품 바꿔치기(Bait-and-switch)입니다. 구매자는 아름답고 평탄하며 접근성이 좋은 필지로 안내됩니다. 하지만 그들이 받는 권리증은 5km 떨어진 늪지대의 것입니다. 조사관은 해당 토지에 서서 GPS와 사진을 캡처합니다.

GeospatialEngine:

  • 권리증의 필지 기하 구조 (parcel geometry)와 GPS 궤적을 대조하여 검증합니다.
  • 타당성 검사 (plausibility checks)를 실행합니다: 경계선까지의 거리, 위치 신뢰도 휴리스틱 (location confidence heuristics).
  • 해당 필지가 보호 구역인 하천 유보지 (river reserve)에 있음을 시사하는 수변 식생 패턴을 탐지합니다.
  • geospatial_verification 이벤트를 발행합니다: "위치 확인됨 (Location verified)" 또는 "위치 불일치 — 비콘을 다시 스캔하십시오 (Location mismatch — please re-scan beacons)".
  • Solar API 데이터가 주장된 필지가 홍수 구역에 있음을 보여주거나, 30미터 수변 완충 구역 (riparian buffer) 오버레이가 필지의 60%가 법적으로 건축 불가능함을 보여주는 경우를 처리합니다.
    조사관은 비용을 지불하기 전에 이 사실을 알게 됩니다.
  1. Orchestrator: 조사를 지속시키는 역할
    MarathonLoop (backend/agent/marathon_loop.py)는 모든 것을 조정하는 작업 상태 머신 (job state machine)입니다. 이 루프는 다음과 같은 역할을 수행합니다:
  • 세션이 생성될 때 시작됩니다.
  • 조사 단계에 따라 진행됩니다.
  • 조사관이 고민할 필요 없이 다음에 무엇을 확인할지 결정합니다.
  • 지수 백오프 (exponential backoff)를 사용하여 실패한 API 호출을 재시도합니다.
  • 인간의 입력이 진정으로 필요할 때 사용자에게 에스컬레이션 (escalate)합니다: "북동쪽 모서리에 있는 비콘의 더 선명한 사진을 제공해 주세요."
    이 시스템은 어시스턴트가 주도하는 조사처럼 동작하도록 설계되었습니다. 현장 조사관은 부동산 양도법 (conveyancing law)을 이해할 필요가 없습니다. 그들은 프롬프트 (prompts)를 따르고, 시스템이 추론합니다.
  1. Mobile Client: 모든 것을 실시간으로 보여줌
    Flutter 클라이언트는 모든 에이전트 활동에 대해 실시간이며, 중복이 제거되고, 순서를 인식하는 타임라인을 유지합니다. 모든 발견 사항, 모든 증거 등록, 모든 검증 단계가 포함됩니다. 이는 단순한 UX 개선이 아닙니다. 조사 도구로서 실시간 타임라인은 조사관이 당국에 제시하는 감사 추적 (audit trail)입니다. 이는 시골 현장 점검 도중 휴대폰 신호가 10분 동안 끊기더라도 완전하고, 순서가 맞으며, 재현 가능해야 합니다.

엔지니어링 아키텍처 — 모든 결정은 제품과 연결되어 있습니다
대부분의 사례 연구가 실패하는 지점이 바로 여기입니다. 아키텍처를 설명한 뒤 제품을 별도로 설명하곤 합니다. TitleTrust에서는 모든 엔지니어링 결정이 제품의 제약 사항 (product constraint) 때문에 내려졌습니다.

그 결정 사항들을 하나씩 살펴보겠습니다.

왜 WebSockets 대신 SSE를 사용했는가
현장 조사관은 Juja에서 저렴한 모바일 데이터를 사용하며, 신호가 간헐적으로 끊기는 환경에 서 있습니다. WebSockets는 지속적인 양방향 연결 (bidirectional connection)을 필요로 합니다. 연결이 끊기면 상태 (state)를 처음부터 다시 구축해야 합니다. 반면, Last-Event-ID를 사용하는 SSE (Server-Sent Events)는 더 나은 대안을 제공합니다. 브라우저(및 Flutter 클라이언트)가 자동으로 재연결을 시도하며 마지막으로 수신한 이벤트 ID를 전송합니다. 그러면 서버는 정확히 그 지점부터 다시 재생 (replay)합니다. 조사관은 인지하지 못하는 사이에 타임라인이 스스로 복구됩니다. 서버에서 클라이언트로의 통신 — 즉, 저희에게 필요한 유일한 용도 — 인 진행 상황 업데이트 (progress-update) 케이스의 경우, SSE가 더 단순하고 모바일 네트워크에서 더 탄력적(resilient)이며, 재생 의미론 (replay semantics)을 네이티브로 지원합니다.

왜 단순한 메시지 큐가 아닌 Redis Streams인가
표준 메시지 큐 (message queue)는 메시지를 전달하고 나면 이를 잊어버립니다. 이는 백그라운드 작업 (background jobs)에는 괜찮지만, 조사 감사 추적 (investigation audit trail)에는 적합하지 않습니다. Redis Streams는 추가 전용 (append-only)의 순서가 보장된 로그 (ordered log)입니다. 모든 이벤트는 안정적인 오프셋 (offset)과 함께 저장됩니다. 모바일 클라이언트가 신호 단절 후 재연결되면, 서버는 마지막으로 확인된 지점부터 에이전트 작업의 정확한 시퀀스를 다시 재생할 수 있습니다. 더 중요한 점은, 조사가 종료된 후 변호사가 에이전트가 무엇을, 어떤 순서로, 어떤 증거와 함께 발견했는지를 정확히 재구성해야 한다는 것입니다. Redis Streams는 바로 그 기록입니다. 이것은 단순한 인프라가 아니라, 증거 관리 연속성 (chain of custody) 그 자체입니다.

왜 두 개의 커서 (event_id + stream_offset)를 사용하는가
모든 이벤트는 두 가지 식별자를 가집니다:
event_id: 클라이언트가 재연결 시에도 추적하는 안정적인 애플리케이션 레벨의 UUID
stream_offset: 효율적인 서버 측 탐색 (seek)을 위한 Redis Streams의 위치

클라이언트는 event_id를 알고, 서버는 stream_offset을 압니다. 재개 (resume) 로직은 이 둘 사이를 매핑합니다. 왜 둘 다 필요할까요? event_id는 Redis의 재시작이나 재수집 (re-ingestion) 상황에서도 살아남는 안정적인 애플리케이션 식별자이기 때문입니다. stream_offset은 서버가 영구 로그 (durable log) 내에서 효율적으로 탐색할 수 있게 해줍니다. 이 두 가지가 모두 없다면, 너무 많은 데이터를 다시 재생하여 낭비하거나, 잘못된 지점부터 재생하여 오류가 발생할 위험이 있습니다.

모든 이벤트가 하나의 증거가 되는 조사 과정에서, 잘못된 재생 (replay)은 단순한 성능 버그가 아니라 정확성 (correctness) 버그입니다.

왜 인프로세스 브로드캐스터 (In-Process Broadcaster) + Redis Streams (하이브리드 방식)인가

브로드캐스터 (backend/realtime/broadcaster.py)는 메모리 내 로컬 팬아웃 (fanout) 큐와 내구성이 있는 Redis Streams 추가 (append)를 모두 유지합니다. 메모리 내 큐는 Redis에 일시적인 장애가 발생하더라도 밀리초 단위로 SSE 클라이언트에 이벤트를 전달합니다. Redis Streams 추가는 재생 (replay) 및 감사를 위해 이벤트를 영구적으로 저장합니다. 성능 저하 모드 (Redis 사용 불가 시)에서 시스템은 플래그를 전환하여 로컬 전달을 계속하며, 연결이 복구되면 클라이언트는 Firestore로부터 권위 있는 상태 (authoritative state)를 복구할 수 있습니다. 조사관의 타임라인은 계속 업데이트됩니다. 그들은 로딩 스피너를 보며 기다릴 필요가 없습니다.

이 설계를 이끈 제품 제약 조건은 다음과 같습니다: 연결 상태가 좋지 않은 지역에서 현장 방문을 수행하는 조사관이 Redis 인스턴스에 일시적으로 접속할 수 없다는 이유로 세션이 중단되어서는 안 된다는 점입니다. 사용자에 대한 가용성 (Availability)은 타협할 수 없는 요소입니다. 하지만 감사 추적 (audit trail) 또한 마찬가지입니다. 하이브리드 방식은 이 두 가지를 모두 제공합니다.

왜 증거에 SHA256 체크섬 (checksum)과 트레이스 ID (trace ID)가 부여되는가

ForensicEngine이 등록하는 모든 증거 — 모든 사진, 모든 문서 분석 결과 —에는 다음이 부여됩니다:

  • 콘텐츠의 SHA256 체크섬 (checksum)
  • 서비스 전반에 걸쳐 이를 연결하는 trace_id
  • 세션 내 순서 정렬을 위한 sequence_id

이것은 과도한 엔지니어링 (over-engineering)이 아닙니다. 이것은 증거 관리 연속성 (chain of custody)입니다. 만약 조사관이 토지 관리 위원회 (Land Control Board)나 법원에 TitleTrust의 조사 결과를 제출한다면, 해당 증거는 수정되지 않았으며 정확하게 귀속되었음을 검증할 수 있어야 합니다. 체크섬은 콘텐츠의 무결성 (integrity)을 증명합니다. 트레이스 ID는 출처 (provenance)를 증명합니다. 시퀀스 ID는 순서 (ordering)를 증명합니다.

왜 결정론적 카오스 테스트 (Deterministic Chaos Tests)가 필요한가

사람들이 자신의 전 재산을 보호하기 위해 의존하게 될 시스템을 공개적으로 배포하기 전에, 저는 시스템이 단순히 잘 작동하기를 바라는 것이 아니라, 장애 상황에서도 올바르게 작동한다는 것을 증명할 수 있어야 했습니다.

테스트 하네스 (test/test_realtime_chaos.py, tests/support/fake_redis.py)에는 다음이 포함됩니다:

  • 프로그래밍 방식으로 실패, 절단(truncated) 또는 쓰기 거부를 유도할 수 있는 가짜 Redis (Fake Redis)
  • xadd 실패, 발행(publish) 지연 및 부분적 영속성(partial persistence)을 시뮬레이션하는 장애 주입기 (Failure Injector)

다음 사항을 검증하는 테스트:

  • 시퀀스 단조성 (sequence monotonicity), Redis 재시작 후 올바른 재생 (replay), 로컬 버퍼로의 폴백 (fallback), 간격 탐지 후 권위적 상태 (authoritative state)로의 클라이언트 수렴

이 하네스를 통해 정상 경로 (happy-path) 테스트에서는 절대 나타나지 않았을 실제 버그들이 드러났습니다:

  • 테스트 인스턴스가 메트릭 이름을 재사용할 때 발생하는 Prometheus 수집기 충돌 (collector collisions)
  • 클라이언트가 Redis ID가 아닌 값을 재개 토큰 (resume token)으로 전달할 때 발생하는 재생 불일치 (replay mismatches)
  • 비영속적 (non-durable) 항목을 권위적 재생 소스로 실수로 사용한 경우

이에 대한 제품 측면의 논거는 다음과 같습니다: 확률적 통합 테스트 (stochastic integration tests)는 때때로 버그를 찾아내지만, 결정론적 장애 주입 (deterministic failure injection)은 버그를 재현 가능하게 찾아냅니다. 정확성이 매우 중요한 시스템에서 '재현 가능성'은 유일하게 수용 가능한 표준입니다.

아키텍처 다이어그램
┌─────────────────────────────────────────────────────────┐
│ Flutter 모바일 클라이언트 │
│ RealtimeController: 중복 제거 (dedupe), 시퀀스, 간격 탐지 │
│ RecoveryCoordinator: 권위적 Firestore 복구 │
└────────────────────┬────────────────────────────────────┘
│ SSE (Last-Event-ID)

┌─────────────────────────────────────────────────────────┐
│ FastAPI 백엔드 │
│ /realtime/sse · /realtime/last-state/{session_id} │
└────────────────────┬────────────────────────────────────┘
│ ▼
┌─────────────────────────────────────────────────────────┐
│ Broadcaster │
│ 프로세스 내 제한된 큐 (저지연 로컬 팬아웃) │
│ Redis Pub/Sub (인스턴스 간 팬아웃) │
│ Redis Streams (영속적 순서 로그 + 재생) │
└──────┬──────────────────────┬───────────────────────────┘
│ │
│ ▼ ▼
┌─────────────┐ ┌──────────────────────────────────┐
│ Redis │ │ Agent Workers │
│ Streams │ │ MarathonLoop (오케스트레이터) │
│ (영속적 │ │ ForensicEngine (비전/문서) │
│ 순서 로그) │ │ GeospatialEngine (GPS/지도) │
└─────────────┘

└──────────────┬───────────────────┘ │ ▼ ┌──────────────────┐ │ Firestore │ │ (canonical │ │ session state) │ └──────────────────┘ 일상 속의 한 장면: 하나의 조사 과정

이해를 돕기 위해, 단일 현장 점검에 대한 전체 흐름을 다음과 같이 설명합니다:

  1. 조사관이 Flutter 앱을 통해 거래 패키지(권리증 (Title Deed), 그린 카드 (Green Card), 변동 양식 (Mutation Form))를 업로드합니다.
  2. Firestore에 세션이 생성됩니다.
  3. MarathonLoop가 시작됩니다.
  4. session_started 이벤트가 발생하여 로컬에 브로드캐스트되고, Redis Streams에 추가됩니다.
  5. ForensicEngine이 실행됩니다:
    • Gemini 3 Pro가 그린 카드 (Green Card)를 읽습니다.
    • Entry #4: Equity Bank에 대한 담보 설정 (Charge), 2018년 12월 1일과 Entry #6: 담보 해지 (Discharge of Charge), 2018년 1월 10일을 찾아냅니다.
    • 담보 해지 (Discharge)가 담보 설정 (Charge)보다 앞선 날짜임을 확인합니다.
    • 시간적 이상 (Temporal anomaly)이 플래그(flagged) 처리됩니다.
    • 증거가 SHA256 체크섬 (checksum) 및 추적 ID (trace ID)와 함께 등록됩니다.
    • evidence_registered 이벤트가 발생합니다.
  6. 조사관의 휴대폰에 있는 타임라인이 SSE (Server-Sent Events)를 통해 즉시 업데이트됩니다.
    • 조사관은 다음 내용을 확인합니다: "⚠️ 시간적 이상 감지 — 담보 해지 (Discharge of Charge)가...

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
1

댓글

0