ZIP:ON에서는 실거래가, 건축물대장, V-World, R-ONE 같은 여러 외부 API 데이터를 조합해서 부동산 계약 전 위험진단을 한다.

그런데 외부 API 데이터를 연결하다 보면 한 가지 문제가 생긴다.

V-World 공시가격 데이터를 조회하려면 PNU가 필요한데, 모든 데이터가 PNU를 가지고 있지는 않다.

PNU는 필지고유번호다. 쉽게 말하면 “이 부동산이 정확히 어느 필지 위에 있는지”를 나타내는 19자리 식별자다.

PNU = 법정동코드 10자리 + 산 여부 1자리 + 본번 4자리 + 부번 4자리

예를 들어 특정 지번 주소가 있다면 법정동코드, 산 여부, 본번, 부번을 조합해 PNU를 만들 수 있다.

문제는 실거래가나 주소검색 결과에는 PNU가 직접 들어있지 않은 경우가 많다는 것이다. 이때 단순히 “PNU가 없으니 매칭 실패”로 처리하면 fact 데이터가 너무 작아지고, 진단 결과도 계속 “자료 부족”으로 흐르게 된다.

잘못된 접근

처음에는 이렇게 생각하기 쉽다.

주소 입력
→ PNU 생성
→ V-World API 호출
→ 공시가격 저장
→ 위험진단

하지만 이 구조는 PNU가 없으면 전체 흐름이 막힌다.

PNU 없음
→ V-World 조회 불가
→ 공시가격 없음
→ 자료 부족
→ 추가 확인 필요

이렇게 되면 실제로는 실거래가, 건축물대장, 지역 통계 같은 다른 정보가 있는데도 사용자는 계속 애매한 결과만 보게 된다.

핵심은 원천 저장과 매칭을 분리하는 것

해결 방향은 단순하다.

외부 API fact 데이터는 먼저 원천 그대로 저장하고, PNU 매칭은 별도 단계에서 처리해야 한다.

즉, 이런 구조가 되어야 한다.

외부 API 원천 fact 적재
→ 주소/지번 정규화
→ PNU 후보 생성
→ PNU 확정 또는 보류
→ V-World 공시가격 조회
→ 위험진단에 반영

PNU가 없어도 실거래가 row, 건축물대장 row, R-ONE 통계 row는 저장해야 한다.
PNU는 이 데이터들을 나중에 연결하기 위한 식별자이지, 모든 fact 저장의 선행조건이 아니다.

PNU 생성 경로는 여러 개가 필요하다

PNU는 한 곳에서만 만들려고 하면 실패율이 높다.
그래서 여러 경로를 둬야 한다.

첫 번째는 건축물대장 기반이다.

건축물대장에는 보통 다음 값들이 있다.

sigunguCd
bjdongCd
platGbCd
bun
ji

이 값들을 조합하면 PNU를 만들 수 있다.
건축물대장 기반 PNU는 가장 신뢰도가 높다.

두 번째는 JUSO 주소검색 기반이다.

도로명주소만 들어온 경우에는 바로 PNU를 만들기 어렵다.
이때 JUSO 결과에서 지번 관련 값을 가져와야 한다.

admCd     법정동코드
mtYn      산 여부
lnbrMnnm  본번
lnbrSlno  부번

세 번째는 실거래가 지번 기반이다.

실거래가에는 시군구코드, 법정동명, 지번, 단지명, 전용면적 같은 정보가 있다.
여기서 법정동코드 테이블과 지번 파싱을 이용하면 PNU 후보를 만들 수 있다.

다만 이 경우에는 후보가 여러 개 나올 수 있으므로 바로 확정하면 안 된다.

매칭 상태를 나눠서 관리해야 한다

PNU 매칭은 성공/실패로만 보면 안 된다.

예를 들어 다음과 같은 상태가 필요하다.

public enum PropertyIdentityMatchStatus {
    EXACT_PNU,
    DERIVED_FROM_BUILDING_LEDGER,
    DERIVED_FROM_JUSO,
    DERIVED_FROM_TRANSACTION,
    MULTIPLE_CANDIDATES,
    REGION_ONLY,
    UNRESOLVED
}

이렇게 나누면 백엔드와 프론트 모두 더 정확하게 판단할 수 있다.

예를 들어 프론트에서는 이렇게 보여줄 수 있다.

공시가격 확인됨
공시가격 후보 확인 필요
지역 통계만 반영됨
정확 필지 확인 불가

중요한 것은 PNU 미확정이 곧 위험하다는 뜻은 아니라는 점이다.
PNU 미확정은 데이터 연결 상태의 문제이고, 위험도 판단과는 분리해야 한다.

V-World fact 적재는 PNU 후보 기반으로

V-World 공시가격 데이터를 적재할 때 전국 모든 PNU를 무작정 호출하는 것은 현실적이지 않다.

하지만 반대로 사용자가 조회한 주소만 적재하면 fact table이 너무 작아진다.

그래서 중간 전략이 필요하다.

전국 무차별 호출은 하지 않는다.
하지만 사용자 조회 주소만 저장하지도 않는다.
ZIP:ON이 가진 원천 데이터에서 만들 수 있는 PNU 후보를 넓게 모은다.

PNU 후보는 다음 source에서 만들 수 있다.

사용자 진단 주소
관심 부동산
건축물대장 조회 결과
JUSO 주소검색 결과
실거래가 row
관리자 seed 대상

특히 실거래가 row는 대량으로 적재할 수 있으므로, 여기서 PNU 후보를 만드는 것이 중요하다.
이렇게 하면 V-World fact table이 단순 사용자 요청 캐시 수준으로 작아지는 문제를 줄일 수 있다.

최종 정리

이번 고민의 결론은 이렇다.

PNU는 V-World 조회에 중요하다.
하지만 모든 외부 API fact 저장이 PNU에 종속되면 안 된다.

따라서 ZIP:ON의 데이터 적재 구조는 다음 원칙을 가져야 한다.

1. 외부 API fact는 원천 키 기준으로 먼저 저장한다.
2. PNU는 별도 매칭 단계에서 생성하거나 연결한다.
3. PNU 생성 경로는 건축물대장, JUSO, 실거래가 기반으로 나눈다.
4. 매칭 상태는 enum으로 관리한다.
5. V-World는 PNU 후보 universe 기반으로 적재한다.
6. PNU 미확정은 위험도 하락이 아니라 데이터 신뢰도 이슈로 표현한다.

한 문장으로 정리하면 다음과 같다.

PNU 후보 기반 적재는 사용자 요청 캐시가 아니라, ZIP:ON이 가진 원천 fact들로부터 생성 가능한 PNU universe를 만들고 그 범위 안에서 V-World fact를 적재하는 전략이다.

이 구조를 사용하면 V-World 공시가격 데이터가 너무 작아지는 문제와, 전국 모든 필지를 무작정 호출하는 문제를 동시에 피할 수 있다.