위치 권한 없이 광고를 지역화하기: GeoIP와 Provider Chain 설계
지역마다 사용할 수 있는 광고 공급자가 다르면 앱은 사용자의 지역을 어느 정도 구분해야 한다. 그렇다고 광고를 고르기 위해 OS 위치 권한부터 요청하는 것은 기능과 권한의 무게가 맞지 않는다. 광고 라우팅에 필요한 것은 사용자의 정확한 좌표가 아니라, 어느 공급자를 먼저 시도할지 정하는 대략적인 힌트이기 때문이다.
그 힌트로 GeoIP를 쓸 수 있다. 다만 GeoIP는 현재 위치의 정답도, 개인정보 문제를 없애 주는 장치도 아니다. VPN이나 중계 서비스, 이동통신사 게이트웨이 때문에 실제 지역과 다를 수 있고, 조회 서버는 요청 과정에서 IP 주소를 보게 된다. 따라서 설계의 목표는 위치를 알아맞히는 것이 아니라 불완전한 신호로도 안전하게 실패하는 광고 계획을 만드는 것이어야 한다.
광고 지역화에 정밀 위치는 필요하지 않다
앱에서 접할 수 있는 지역 신호는 서로 다른 질문에 답한다.
| 신호 | 실제로 알려 주는 것 | 광고 라우팅에 쓸 때의 한계 |
|---|---|---|
| 앱 언어 | 사용자가 읽고 싶은 언어 | 거주지나 현재 위치가 아니다 |
| 시스템 Locale의 국가 | 사용자가 선택한 지역 설정 | 여행 중이거나 설정을 바꾸지 않았을 수 있다 |
| OS 위치 서비스 | 기기가 측정한 위치 | 권한 요청과 민감한 데이터 처리가 필요하다 |
| GeoIP | 네트워크가 외부에 보이는 IP의 대략적 지역 | VPN·중계·통신망에 따라 틀리거나 비어 있을 수 있다 |
언어가 한국어라는 이유로 국내 광고를 선택해서는 안 되고, Locale의 국가가 현재 위치라고 가정해서도 안 된다. 반대로 광고 공급자 선택에 국가나 넓은 지역 정도면 충분한데 정밀 위치 권한을 요구할 이유도 없다. Apple과 Android의 권한 지침처럼, 위치 기능이 실제로 필요할 때 사용자 맥락 안에서 필요한 수준만 요청하는 편이 맞다.
광고 라우팅은 가장 낮은 정밀도의 신호부터 시작할 수 있다. Locale은 앱의 언어·정책 기본값을 고르는 데만 쓰고, 지리적 공급자 가용성을 확정하지 않는다. 실제 지역에 따른 분기가 필요할 때 GeoIP를 별도로 조회하되, 어느 단계에서도 확신할 수 없다면 unknown을 반환한다. 추측으로 빈칸을 채우지 않는 것이 첫 번째 폴백이다.
동의가 끝난 뒤 한 번만 조회한다
지역 판정보다 먼저 확인할 것은 광고 요청 가능 상태다. Google UMP의 canRequestAds 같은 게이트가 열리기 전에 GeoIP를 미리 조회하면, 아직 광고를 요청할 수 없는 세션에서도 불필요한 네트워크 호출이 발생한다. 동의 결과가 광고 요청 허용을 뜻할 뿐 지역이 정확하다는 뜻은 아니라는 점도 분리해야 한다.
시작 순서는 다음 정도면 충분하다.
- 저장된 동의 정보를 갱신하고 필요한 동의 화면을 처리한다.
- 광고 요청 가능 상태가 되면 지역 해석을 시작한다.
- 한 프로세스에서 지역을 한 번만 해석하고 결과 버킷을 메모리에 보관한다.
- 버킷으로 Provider Chain을 만들고 첫 번째 공급자를 시도한다.
- 실패할 때마다 다음 공급자로 이동하고 인하우스 콘텐츠에서 끝낸다.
동의 갱신 직후와 화면 처리 완료 콜백 양쪽에서 광고 요청 가능 상태가 참이 될 수 있다. 두 경로가 같은 초기화를 시작하지 않도록, 이미 생성한 지역 조회 작업이나 광고 계획을 재사용해야 한다. 이 단일 실행 보장은 요청 횟수와 경쟁 상태를 함께 줄인다.
동의가 확인되지 않았거나 외부 광고 요청을 허용하지 않는 상태라면 지역 조회를 생략한다. 이때 무엇을 보여 줄지는 제품과 적용 정책에 따라 별도로 정하되, 외부 광고를 우회 호출하는 폴백을 만들어서는 안 된다.
GeoIP는 정답이 아니라 라우팅 힌트다
CDN이 제공하는 뷰어 위치 헤더는 서버가 본 IP 주소를 바탕으로 만들어진다. 도시 같은 세부 값은 모든 IP에 존재하지 않으며, 비 ASCII 문자는 인코딩될 수도 있다. 앱이 원시 도시 문자열을 직접 해석하고 광고 정책까지 결정하게 만들면 플랫폼마다 파싱과 예외 처리가 반복된다.
더 작은 경계는 서버에서 원시 값을 넓은 버킷으로 접는 것이다.
regionalMarket: 지역 공급자를 먼저 시도할 수 있음otherMarket: 전역 공급자만 시도함unknown: 판정할 수 없음
응답에는 좌표나 원시 IP, 도시 이름 대신 이 버킷만 담는다. 도시 단위 라우팅이 꼭 필요하더라도 앱에는 사업 규칙에 맞춘 식별자만 반환하는 편이 낫다. 헤더가 없거나 디코딩·검증에 실패하면 unknown으로 끝낸다.
CDN 응답이 지역에 따라 달라진다면 캐시 정책도 같은 경계를 알아야 한다. 필요한 헤더를 캐시 키에 포함하지 않으면 한 지역의 결과가 다른 지역에 재사용될 수 있고, 지나치게 세분화하면 캐시 효율과 데이터 노출 면적이 나빠진다. 작은 GeoIP 응답을 캐시하지 않거나, 서버에서 만든 거친 버킷만 캐시 기준으로 쓰는 식으로 정책을 명시해야 한다.
지역 판정과 Provider 선택을 분리한다
지역 해석기는 광고 SDK를 알 필요가 없고, 광고 로더는 IP나 Locale을 알 필요가 없다. 둘 사이에는 작은 열거형만 두면 된다.
enum ConsentState {
case pending
case adsAllowed
case externalAdsBlocked
}
enum AdRegion {
case regionalMarket
case otherMarket
case unknown
}
enum AdProvider {
case regional
case global
case inHouse
}
func makeAdPlan(
consent: ConsentState,
resolveRegion: () async -> AdRegion
) async -> [AdProvider] {
switch consent {
case .pending, .externalAdsBlocked:
return [.inHouse]
case .adsAllowed:
break
}
switch await resolveRegion() {
case .regionalMarket:
return [.regional, .global, .inHouse]
case .otherMarket, .unknown:
return [.global, .inHouse]
}
}
이 함수는 광고를 불러오지 않고 순서만 만든다. 단일 실행과 캐시는 이 함수 바깥의 호출자나 지역 해석기가 이미 만든 작업을 재사용해 보장한다. 실제 로더는 배열을 앞에서부터 순회해 첫 성공에서 멈춘다. 실패한 공급자를 즉시 다시 시도하거나 체인을 처음부터 반복하지 않는다. 마지막의 inHouse는 네트워크 광고가 모두 실패해도 화면이 빈 채로 끝나지 않게 하는 종료점이다.
iOS, Android, Web은 같은 버킷과 우선순위를 공유할 수 있지만 구현까지 한 모듈로 묶을 필요는 없다. 각 플랫폼은 자신의 동의 SDK와 광고 SDK를 사용하고, ConsentState → AdRegion → [AdProvider] 계약만 동일하게 유지하면 된다.
짧은 timeout과 프로세스 캐시로 실패를 흡수한다
광고 슬롯은 GeoIP 응답을 오래 기다릴 이유가 없다. 지역 해석기의 네트워크 경계에는 다음 원칙이면 충분하다.
- 예상한 HTTPS endpoint만 호출하고 뜻밖의 redirect는 따르지 않는다.
- timeout은 수 초 이내로 짧게 두고 렌더링 경로에서 재시도하지 않는다.
- 응답 크기와 허용 버킷을 제한하고 나머지는
unknown으로 처리한다. - 한 프로세스에서 결과와 진행 중인 요청을 재사용한다.
- 원시 IP·도시 응답을 디스크나 분석 로그에 남기지 않는다.
- HTTP 오류, timeout, 오프라인, 파싱 실패를 모두 정상적인 실패 결과로 접는다.
프로세스 캐시는 네트워크가 바뀐 직후 잠시 오래된 버킷을 쓸 수 있다. 광고 공급자 우선순위를 정하는 거친 힌트라면 보통 감수할 수 있는 범위이고, 다음 실행에서 다시 평가하면 된다. 더 긴 캐시가 실제로 필요하다는 측정 결과가 생기기 전에는 영구 저장과 만료 정책을 추가하지 않는 편이 단순하다.
unknown에서도 전역 공급자를 시도할지는 그 공급자의 지원 범위와 동의 조건에 달려 있다. 핵심은 이 결정을 지역 해석기 내부에 숨기지 않고 Provider Chain 정책에 명시하는 것이다.
위치 권한이 없다고 개인정보 처리가 사라지지는 않는다
OS 위치 API를 호출하지 않으면 위치 권한 프롬프트는 피할 수 있다. 그러나 GeoIP 요청을 받은 서버나 CDN은 네트워크 주소를 처리한다. 따라서 “위치 권한 없음”을 “위치 관련 데이터 처리 없음”이나 “익명”으로 표현해서는 안 된다.
수집 목적, 보관 여부, 제3자 전송, 동의와의 관계는 실제 데이터 흐름을 기준으로 검토해야 한다. 앱에는 거친 버킷만 전달하고, 서버 로그에서도 원시 위치 헤더와 IP를 불필요하게 보존하지 않으며, 광고 라우팅 결과를 장기 사용자 프로필로 재사용하지 않는 것이 안전한 기본값이다. 이 설계는 데이터 최소화에 도움을 줄 뿐, 법률이나 스토어 정책 준수를 자동으로 보장하지 않는다.
네트워크가 아니라 경계를 주입해 검증한다
단위 테스트에서 실제 GeoIP 서비스나 광고 SDK를 호출하면 VPN, 네트워크 상태, 광고 재고에 따라 결과가 흔들린다. 대신 지역 해석 함수와 Provider 로더를 주입해 다음 경계를 검증한다.
| 조건 | 기대 결과 |
|---|---|
동의 상태가 pending |
지역 조회 0회, 인하우스만 선택 |
| 광고 요청 가능, 지역 버킷 반환 | 지역 공급자를 먼저 시도 |
| 지역 공급자 실패 | 전역 공급자로 한 번 이동 |
| GeoIP timeout 또는 잘못된 응답 | unknown 경로로 전역 공급자 시도 |
| 모든 외부 공급자 실패 | 인하우스에서 종료 |
| 동의 완료 콜백이 두 번 도착 | 지역 조회와 광고 계획 생성은 1회 |
통합 환경에서는 오프라인, 느린 응답, 빈 헤더, 인코딩된 도시 값, VPN이나 Private Relay처럼 관측 IP가 달라지는 조건을 확인한다. 여기서 통과 기준은 실제 위치를 맞히는 것이 아니다. 어떤 입력에서도 허용된 체인만 선택하고, 제한 시간 안에 종료하며, 민감한 원시 값을 로그에 남기지 않는지가 기준이다.
정리
위치 권한 없이 광고를 지역화하는 핵심은 GeoIP의 정확도를 믿는 데 있지 않다. 동의가 끝난 뒤 한 번만 대략적 지역 버킷을 구하고, 지역 판정과 광고 공급자 우선순위를 분리하며, 모든 실패를 더 일반적인 공급자와 인하우스 콘텐츠로 접는 데 있다.
이 구조에서는 GeoIP가 틀려도 앱이 멈추지 않는다. 새로운 광고 공급자가 생겨도 위치 해석기를 고치지 않고 체인만 조정할 수 있다. 무엇보다 정밀 위치 권한을 광고 선택의 지름길로 쓰지 않으면서도, 네트워크 기반 지역 신호가 가진 개인정보 경계를 숨기지 않게 된다.
출처
- Apple Developer Documentation — Requesting authorization to use location services
- Apple Human Interface Guidelines — Privacy
- Android Developers — Minimize your permission requests
- Android Developers — Request location access at runtime
- AWS — Add CloudFront request headers
- Google for Developers — Set up UMP SDK for Android
- Google for Developers — Set up UMP SDK for iOS
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.