어디살지 — 주소·건축물 증강 파이프라인 개선 설계서
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.1 | P0 | JUSO 운영 활용신청 (도메인·IP 등록) | 운영 | 5m + 1~3d 대기 | 발급 키로 호출 시 errorCode=0 |
N0.2 | P0 | data.go.kr 건축물대장 운영(활용) 신청 — 일 1,000 → 무한 | 운영 | 5m + 1~3d | 일 한도 메시지 사라짐 |
N1.1 | P0 | Infisical alpha · local · beta 동기화: JUSO_API_KEY 운영키 교체 |
인프라 | 10m | 3개 env get 결과 동일 |
N1.2 | P1 | local /ai-real-estate-backend 폴더 채우기 (현 0건 → alpha 기준 70+) |
인프라 | 15m | local 환경 단독으로 백엔드 부팅 가능 |
N1.3 | P2 | beta 폴더 운영 키 일괄 임포트 (현 거의 비어있음) | 인프라 | 30m | beta 배포 정상 |
N2.1 | P1 | 어댑터 _search_address: JUSO 1차 + Kakao b_code 2차 폴백 정식화 |
코드 | M (~80 LOC) | JUSO 실패해도 BuildingInfo 정상 |
N2.2 | P1 | 어댑터에 getBrRecapTitleInfo 호출 추가 → 표제부 응답 머지 |
코드 | M (~120 LOC) | 주차·대지면적·건폐율·용적률 채움 (≥ 4 필드 회복) |
N2.3 | P1 | getBrJijiguInfo 응답에서 land_use_district 파싱 추가 |
코드 | S (~25 LOC) | jijiguGbCd=2 행 파싱 시 값 채움 |
N2.4 | P2 | 새 어댑터: RTMSRealtyAdapter (실거래 매매/전월세 최근 24개월) |
코드 | L (~200 LOC) | scripts/seed_dummy.py에서 매매 평균가 채움 |
N2.5 | P2 | 새 어댑터: OfficialPriceAdapter (공시지가 별도 OpenAPI) |
코드 | M (~120 LOC) | official_price Empty 케이스 ≥ 50% 회복 |
N3.1 | P1 | 라이브 테스트 폴백 monkey-patch 제거 → 운영키 정상 경로 검증 | 테스트 | S | test_building_registry_live.py 6/6 그대로 PASS |
N3.2 | P1 | 채움률 회귀 테스트 추가 (BuildingInfo 필드 N개 이상) | 테스트 | S | assert len(building) >= 28 |
N4.1 | P2 | 운영 모니터링: JUSO/data.go.kr errorCode Sentry 이벤트화 | 운영 | M | E0014·E0001 발생 시 Slack 알림 |
N4.2 | P3 | 일별 호출량 집계 dashboard (Grafana) | 운영 | M | API별 일별 grouping |
N5.1 | P3 | 도로명주소 전수 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_KEY | data.go.kr 15057511 | 실거래 매매/전월세 (BLDRGST와 동일 인증서비스 가능) |
VWORLD_API_KEY | vworld.kr | PNU·지오메트리 (선택) |
NEIS_API_KEY | open.neis.go.kr | 학군·통학구역 (선택) |
5. 테스트 정비
- N3.1
test_building_registry_live.py:_resolve_via_kakao폴백 함수와_kakao_resolve_to_bcodemonkey-patch 제거 → JUSO 정상 경로만 검증. 운영키 후 JUSO 그대로 동작하면 폴백 코드는 어댑터에 있어도 호출 안 됨. - N3.2 새 회귀 테스트
test_building_info_field_coverage.py— 가정동 277 로 호출 후build_year·total_area·main_purpose·floors·structure·building_area·land_area(N2.2)·coverage_ratio(N2.2)·floor_area_ratio(N2.2)·parking.total_count(N2.2)·land_use_zone·land_use_district(N2.3) 12 핵심 필드 모두 not-None 확인. - N3.3 CodeRabbit 리뷰 통과 후 main 머지 (이번부터
--admin우회 금지)
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 채움 필드 | 24 | 26 | 30 | 33+ |
| JUSO errorCode==0 율 | 0% | 100% | 100% | 100% |
| 주소 조회 평균 지연 | 실패 | ~120ms | ~90ms (캐시) | ~30ms (PNU DB) |
| 실거래 매매가 보강 | 0 | 0 | 0 | ≥ 24 개월 |
| 운영 알림 (errorCode≠0) | 없음 | 없음 | Sentry+Slack | +Grafana |
참고
- 실측 검증 결과: field-validation-live.pages.dev
- 필드 카탈로그 38종: property-enrichment-fields.pages.dev
- 건축물대장 totalCount 검증: building-registry-report.pages.dev
- 증강 전략 분석: address-augmentation-strategy.pages.dev
- JUSO 활용신청: business.juso.go.kr
- data.go.kr 1613000: openapi.do
- data.go.kr 15057511 (RTMS): openapi.do