동기화가 판매점 8,802곳을 지운 날: 공공데이터 필드 이름 하나와 100건짜리 목록
로또 판매점 지도 앱에서 동기화 뒤 판매점 이름이 전부 「알수없음」으로 바뀌고, 앱에 넣어 둔 판매점 8,802곳이 사라졌습니다. 공공데이터 응답의 필드 이름 변경, 첫 100건만 받은 목록으로 삭제하던 동기화, 지오코더의 없는 키까지 원인 세 가지와 고친 방법을 정리합니다.
지도가 「알수없음」으로 찼다
로또 판매점 지도를 보여 주는 앱을 운영하고 있습니다. 앱 안에 판매점 8,802곳을 담은 SQLite 파일(store.db)을 넣어 두고, 공공데이터포털의 로또 판매점 API 와 비교해서 바뀐 것만 반영하는 구조입니다.
9월 18일, 판매점 탭을 열면 목록 이름이 전부 「알수없음」으로 나왔습니다. 실기기(갤럭시 노트10+)에서 앱 DB 를 꺼내 열어 보니 행이 295개뿐이었고, 이름은 하나도 빠짐없이 「알수없음」이었습니다. 8,802곳이 있어야 할 자리였습니다.
원인은 세 겹이었습니다.
원인 1: 응답 필드 이름이 바뀌었다
앱은 판매점 이름을 이렇게 읽고 있었습니다.
String merchant = item.has("상호") ? item.get("상호").getAsString() : "알수없음";
API 를 직접 불러 응답을 보니 상호 라는 칸이 없었습니다. 이름은 판매점명 에 들어 있었고, 지번주소 칸도 없어졌습니다. 오늘(10월 7일) 다시 불러 본 응답도 칸은 네 개입니다.
도로명주소, 순서, 판매점명, 판매점종류 (totalCount 23,503)
has("상호") 가 늘 false 이니, 들어오는 판매점은 전부 「알수없음」이 됐습니다. 여기까지는 이름이 틀리는 문제입니다. 행이 사라진 건 두 번째 원인 때문입니다.
원인 2: 100건짜리 목록으로 지웠다
동기화 흐름은 이랬습니다.
- API 에서 전체 개수(
totalCount)만 먼저 받는다. - 기기 DB 의 행 수와 같으면 DB 를 그대로 쓴다.
- 다르면 목록을 받아서 비교한다. 새 판매점은 넣고, 목록에 없는 DB 행은 지운다.
문제는 3번에서 받는 목록이 page=1&perPage=100, 즉 첫 100건뿐이었다는 점입니다. 전체 개수가 기기 DB 와 같은 동안에는 3번을 탈 일이 없어서 드러나지 않았습니다. API 의 전체 개수가 23,503 이 되어 8,802 와 달라지자, 앱은 100건을 받아 놓고 「이 100건에 없는 판매점은 문을 닫았다」 고 보고 나머지를 지웠습니다.
게다가 그 100건은 원인 1 때문에 이름이 전부 「알수없음」이었습니다. 비교 키가 「이름|도로명주소」 라서 내장 8,802건과는 하나도 맞지 않았고, 그래서 남김없이 지워졌습니다.
원인 3: 지오코더도 없는 키를 읽었다
동기화로 새로 들어온 판매점은 네이버 지오코딩으로 좌표를 붙입니다. 이 코드는 응답의 address1·address2 를 읽고 있었는데, 실제 응답 키는 roadAddress·jibunAddress 였습니다. 그래서 동기화로 들어온 행은 주소가 늘 빈 문자열이었습니다.
세 가지 모두 「응답에 이 키가 있을 것이다」 라는 가정이 틀린 경우입니다. 컴파일 오류도 크래시도 없었습니다. 값만 조용히 비었습니다.
고친 것 (1.0.58)
1. 이름은 여러 키로 읽고, 그래도 없으면 비슷한 칸을 찾는다
private static String storeName(JsonObject item) {
for (String k : new String[]{"판매점명", "상호", "상호명", "판매점"}) {
String v = str(item, k);
if (!v.isEmpty()) return v;
}
for (String k : item.keySet()) {
if ((k.contains("상호") || k.contains("점명")) && item.get(k).isJsonPrimitive()) {
String v = item.get(k).getAsString().trim();
if (!v.isEmpty()) return v;
}
}
return "알수없음";
}
이름이 「알수없음」으로 나온 판매점은 아예 DB 에 넣지 않습니다.
2. 받은 목록이 전체일 때만 지운다
boolean complete = stores.size() >= nCount;
syncStoreDataDelta(db, stores, complete);
// syncStoreDataDelta 안, 새 행을 넣고 바뀐 행을 고친 뒤
if (!complete) return; // 일부만 받았으면 지우지 않는다
100건만 받았으면 넣거나 고치기만 하고 지우지는 않습니다.
3. 이미 망가진 기기는 스스로 복구한다
업데이트를 받는 사용자 기기에는 이미 망가진 DB 가 있습니다. Room 의 createFromAsset 은 DB 를 처음 만들 때만 자산 파일을 읽기 때문에, 앱을 업데이트해도 지워진 8,802건은 돌아오지 않습니다. 그래서 동기화를 시작할 때 두 가지를 먼저 합니다.
- 이름이 「알수없음」이거나 주소가 빈 행을 지운다.
- 기기 DB 행 수가 자산 DB 의 90% 아래면, 자산
store.db를 캐시 폴더로 복사해 읽기 전용으로 열고, 기기 DB 에 없는 행(키 = 이름 + 도로명주소)을 다시 넣는다.
4. 지오코더 키를 바로잡는다
roadAddress·jibunAddress 를 읽고, 그래도 비어 있으면 요청한 주소를 그대로 씁니다.
같은 노트10+ 에서 1.0.58 을 실행한 결과는 8,901건 · 「알수없음」 0건 · 빈 주소 0건이었고, 지도에 판매점 실명이 다시 떴습니다.
정리
- 외부 API 의 필드 이름은 문서나 예전 코드를 보고 가정하지 않습니다. 응답을 직접 불러서 키를 확인합니다.
- 삭제가 들어간 동기화는 받은 목록이 전체라는 게 확인됐을 때만 지웁니다. 일부만 받은 목록에 없다는 건 「없어졌다」 가 아니라 「아직 안 받았다」 입니다.
- 「개수가 같으면 건너뛴다」 같은 지름길은 그 아래 경로를 오래 숨깁니다. 이번 삭제 코드도 개수가 달라지기 전까지는 실행되지 않았습니다.
- 코드를 고치면 버그는 멈추지만, 이미 지워진 데이터는 돌아오지 않습니다. 사용자 기기에 남은 잘못된 데이터는 수정판이 스스로 고치게 만듭니다.
이 글에 나온 앱은 로또 명당(Google Play) 입니다.