어디살지 — 주소·건축물 증강 파이프라인 개선 설계서

2026-05-19 · 진단(실측) + 코드 / Infisical / 테스트 / 운영 4축 종합 개선안
TL;DR — JUSO 키는 등록되어 있고 개발승인키 기간 만료(E0014) 만 발생. 운영키 활용신청 통과 → Infisical 갱신 → 어댑터 폴백 제거 + 총괄표제부·용도구역 파싱 추가 → 24/38 → 33/38 채움. 10 영업일 안에 P0/P1 모두 마감 가능.

1. 진단 (실측 결과 정정)

항목현재실측 근거
JUSO_API_KEY E0014 만료 키 등록 ✓. "개발승인키 기간이 만료되어 서비스를 이용하실 수 없습니다." — 직전 E0001 진단은 shell cut -d=가 base64 패딩을 잘라낸 도구 오류였음.
BLDRGST_API_KEY / DATA_GO_KR_API_KEY 동작 (일 1,000 한도) 5종 endpoint totalCount 메일 기준값과 일치 (9 / 1 / 98 / 1,630 / 1,160)
KAKAO_API_KEY 동작 (일 30만) geocoding · keyword · b_code 모두 응답 정상
BuildingInfo 38 필드 채움률 24 / 38 (63%) 주차 5종·대지면적·건폐율·용적률은 총괄표제부 미연결이라 누락
주소 → b_code 변환 본 코드 경로 dead path (JUSO 의존) 어댑터의 _search_address는 JUSO만 호출. 폴백은 라이브 테스트에만 monkey-patch

2. 개선 작업 마스터 매트릭스

P0 즉시 · 사용자 영향 P1 1주 내 · 채움률 회복 P2 2주 내 · 확장 P3 백로그
ID 우선 작업 영역 예상 완료 조건
N0.1P0 JUSO 운영 활용신청 (도메인·IP 등록) 운영 5m + 1~3d 대기 발급 키로 호출 시 errorCode=0
N0.2P0 data.go.kr 건축물대장 운영(활용) 신청 — 일 1,000 → 무한 운영 5m + 1~3d 일 한도 메시지 사라짐
N1.1P0 Infisical alpha · local · beta 동기화: JUSO_API_KEY 운영키 교체 인프라 10m 3개 env get 결과 동일
N1.2P1 local /ai-real-estate-backend 폴더 채우기 (현 0건 → alpha 기준 70+) 인프라 15m local 환경 단독으로 백엔드 부팅 가능
N1.3P2 beta 폴더 운영 키 일괄 임포트 (현 거의 비어있음) 인프라 30m beta 배포 정상
N2.1P1 어댑터 _search_address: JUSO 1차 + Kakao b_code 2차 폴백 정식화 코드 M (~80 LOC) JUSO 실패해도 BuildingInfo 정상
N2.2P1 어댑터에 getBrRecapTitleInfo 호출 추가 → 표제부 응답 머지 코드 M (~120 LOC) 주차·대지면적·건폐율·용적률 채움 (≥ 4 필드 회복)
N2.3P1 getBrJijiguInfo 응답에서 land_use_district 파싱 추가 코드 S (~25 LOC) jijiguGbCd=2 행 파싱 시 값 채움
N2.4P2 새 어댑터: RTMSRealtyAdapter (실거래 매매/전월세 최근 24개월) 코드 L (~200 LOC) scripts/seed_dummy.py에서 매매 평균가 채움
N2.5P2 새 어댑터: OfficialPriceAdapter (공시지가 별도 OpenAPI) 코드 M (~120 LOC) official_price Empty 케이스 ≥ 50% 회복
N3.1P1 라이브 테스트 폴백 monkey-patch 제거 → 운영키 정상 경로 검증 테스트 S test_building_registry_live.py 6/6 그대로 PASS
N3.2P1 채움률 회귀 테스트 추가 (BuildingInfo 필드 N개 이상) 테스트 S assert len(building) >= 28
N4.1P2 운영 모니터링: JUSO/data.go.kr errorCode Sentry 이벤트화 운영 M E0014·E0001 발생 시 Slack 알림
N4.2P3 일별 호출량 집계 dashboard (Grafana) 운영 M API별 일별 grouping
N5.1P3 도로명주소 전수 ETL → addresses PostgreSQL 테이블 데이터 L 전국 PNU 인덱스 + 월 1회 cron

3. 코드 개선 — building_registry_adapter

(N2.1) JUSO 1차 + Kakao b_code 폴백 정식화

# 현재 (dead path on E0014)
async def _search_address(self, address):
    response = await self._juso_client.get(self.JUSO_API_URL, params={...})
    if common.get("errorCode") != "0": return None
    ...

# 개선 (폴백 내장)
async def _search_address(self, address):
    parsed = await self._try_juso(address)
    if parsed is None:
        parsed = await self._try_kakao_bcode(address)
    return parsed

async def _try_juso(self, address): ...
async def _try_kakao_bcode(self, address):
    resp = await self._kakao_client.get(
        "https://dapi.kakao.com/v2/local/search/address.json",
        params={"query": address},
        headers={"Authorization": f"KakaoAK {self._kakao_api_key}"},
    )
    docs = resp.data.get("documents", [])
    if not docs: return None
    addr = docs[0].get("address") or {}
    b_code = addr.get("b_code", "")
    if len(b_code) < 10: return None
    return {
        "sigunguCd": b_code[:5],
        "bjdongCd": b_code[5:10],
        "bun": (addr.get("main_address_no") or "0").zfill(4),
        "ji":  (addr.get("sub_address_no")  or "0").zfill(4),
    }

(N2.2) 총괄표제부 머지 — 주차·대지·건폐·용적 회복

async def get_building_info(self, address):
    ...
    title = await self._get_building_data(...)         # getBrTitleInfo
    recap = await self._get_recap_data(sigungu, bjdong, bun, ji)
    merged = {**(recap or {}), **title}                 # 표제부 우선, 총괄로 보강
    return Ok(self._parse_building_info(merged, units, ...))

async def _get_recap_data(self, sigungu, bjdong, bun, ji):
    # getBrRecapTitleInfo — platArea·bcRat·vlRat·indrMechUtcnt 등 채움
    response = await self._building_client.get(
        "https://apis.data.go.kr/1613000/BldRgstHubService/getBrRecapTitleInfo",
        params={"serviceKey": self._data_go_kr_api_key, ...},
    )
    items = response.data.get("response", {}).get("body", {}).get("items", {})
    item = items.get("item", [])
    if isinstance(item, dict): item = [item]
    return item[0] if item else None

(N2.3) 용도구역 파싱

async def _get_land_use_zone(...):
    ...
    land_use_district = None
    land_use_district = None
    for item in item_list:
        if item.get("jijiguGbCd") == "2" or item.get("jijiguGbCdNm") == "용도구역코드":
            v = (item.get("jijiguCdNm") or "").strip()
            if v: land_use_district = v; break
    return zone, land_use_district  # 반환 시그니처 변경

4. Infisical 작업 (N1.1 ~ N1.3)

N1.1 — JUSO 운영키 갱신 (3개 env)

# 활용신청 후 발급된 새 키를 모든 환경에 동기화
NEW_KEY="prodUxxx..."  # 운영키 (prod 접두)
PRJ=c08a58ef-000b-409d-a6f1-5bde4e3e84eb
PATH=/ai-real-estate-backend

for env in local alpha beta; do
  infisical secrets set "JUSO_API_KEY=$NEW_KEY" \
    --env=$env --path=$PATH --projectId=$PRJ
done

N1.2 — local 폴더 채우기

현재 /ai-real-estate-backend local 폴더는 0건. 운영자가 alpha 값 export → local import 한 번만 수행하면 됨.

infisical export --env=alpha --path=/ai-real-estate-backend \
  --projectId=$PRJ > /tmp/alpha.env
infisical import /tmp/alpha.env --env=local --path=/ai-real-estate-backend \
  --projectId=$PRJ
rm /tmp/alpha.env  # 평문 시크릿 즉시 삭제

신규 등록 권장 키 (N2.4 / N2.5 의존)

출처역할
RTMS_API_KEYdata.go.kr 15057511실거래 매매/전월세 (BLDRGST와 동일 인증서비스 가능)
VWORLD_API_KEYvworld.krPNU·지오메트리 (선택)
NEIS_API_KEYopen.neis.go.kr학군·통학구역 (선택)

5. 테스트 정비

6. 운영 모니터링 (N4.1)

# ResilientHttpClient 에 hook 추가
async def _on_response(self, response):
    body = response.json() if "application/json" in response.headers.get("content-type", "") else {}
    err = body.get("results", {}).get("common", {}).get("errorCode")
    if err and err != "0":
        sentry_sdk.capture_message(
            f"[{self._service_name}] errorCode={err}",
            level="warning",
            extras={"endpoint": str(response.url), "body": body},
        )
        await self._slack_alert(f":warning: {self._service_name} {err}")

같은 hook 으로 일별 호출량 카운터를 Redis INCR 로 집계 → Grafana datasource (N4.2).

7. 실행 타임라인

Day -1 Day 0 Day 1~3 Day 4~7 Day 8~10 ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │N0.1·N0.2│ │N1.1·N1.2│ │N2.1·N2.3│ │N2.2 │ │N2.4·N2.5│ │활용신청 │──대기→│Infisical│───►│폴백 PR │───►│RecapPR │───►│실거래· │ │ JUSO·GO │ │운영키 │ │N3.1 갱신│ │N3.2 회귀│ │공시 PR │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ ▼ ▼ ▼ │ 24/38 → 26/38 26/38 → 30/38 30/38 → 33+/38 │ └── 대기 동안 비차단 작업: N2.1 PR 작성 (현재 dev 키로도 폴백 동작 검증 가능)

Critical Path = N0.1(JUSO 신청 대기 1-3d) — 그 외 코드 작업은 모두 병렬화 가능. 운영키 받기 전에도 어댑터 폴백 PR(N2.1)은 작성·머지 가능 → 발급 즉시 무중단 전환.

8. 위험 / 완화

위험영향완화
JUSO 운영 활용신청 반려 1주 추가 지연 현재 Kakao b_code 폴백 PR(N2.1) 머지로 무중단 운영 — JUSO 의존 자체 제거
총괄표제부 응답 스키마가 단지 형태별로 상이 일부 건물 채움률 변동 표제부 우선 + 총괄로 보강하는 머지 순서로 회귀 최소화
data.go.kr 운영키 신청 후 IP 화이트리스트 미동기화 배포 환경에서만 401 alpha·beta 출구 IP 사전 등록 (현재 EC2 elastic IP 사용)
Sentry 알림 과다 온콜 노이즈 errorCode==0 외만 캡처 + 1분 dedup

9. 완료 지표

지표현재P0 후P1 후P2 후
BuildingInfo 채움 필드24263033+
JUSO errorCode==0 율0%100%100%100%
주소 조회 평균 지연실패~120ms~90ms (캐시)~30ms (PNU DB)
실거래 매매가 보강000≥ 24 개월
운영 알림 (errorCode≠0)없음없음Sentry+Slack+Grafana

참고