클라우드 Mac에서 iOS 지리적 거리와 지오펜스 경계 검증하기

클라우드 Mac에서 iOS 지리적 거리와 지오펜스 경계 검증하기

가까운 매장이 갑자기 수만 킬로미터 밖에 있는 것으로 표시된다고 해서 반드시 위치 서비스가 고장 난 것은 아닙니다. 위도와 경도의 순서를 바꾸어 입력하거나, 날짜 변경선을 가로지르는 좌표를 단순히 빼거나, 거리를 먼저 반올림한 뒤 범위 안에 있는지 판단해도 비슷한 문제가 발생할 수 있습니다. GNUMac 클라우드 Mac에서는 먼저 고정 좌표로 계산 로직을 검증한 다음 위치 입력과 기기 콜백을 다시 연결할 수 있습니다. 이렇게 하면 실패 원인이 어느 계층에 있는지 명확해져, 지도를 이리저리 움직이며 원인을 추측할 필요가 없습니다.

좌표와 거리의 계약부터 명확히 정하기

이 글에서 검증하는 대상은 ‘두 좌표 사이의 지표면 거리’입니다. 자동차 이동 경로의 길이도 아니고, 사용자가 실제로 걸은 거리도 아닙니다. 입력은 위도, 경도 순서의 십진 도 단위를 사용하며, 내부 거리는 모두 미터로 통일합니다. 단위 변환은 화면에 표시할 때만 수행합니다.

위도는 −90도부터 +90도, 경도는 −180도부터 +180도 범위여야 하며, 유한수가 아닌 값도 거부해야 합니다. 잘못된 입력을 조용히 경계값으로 보정해서는 안 됩니다. 그러면 상위 단계에서 손상된 데이터가 정상적인 레코드처럼 보이게 됩니다.

좌표가 유효하다고 해서 위치 정보를 신뢰할 수 있는 것은 아닙니다. 위도 0도, 경도 0도는 유효한 위치이므로 무조건 ‘아직 위치를 얻지 못함’으로 취급해서는 안 됩니다. 이 값을 허용할지는 데이터 소스의 계약에 따라 결정해야 합니다.

지도 화면에서 보이는 거리 역시 기댓값으로 삼을 수 없습니다. 투영법, 확대·축소 수준, 기기 픽셀에 따라 화면은 달라지지만, 비즈니스 로직의 계산 결과까지 달라져서는 안 됩니다.

클라우드 Mac에서 고정 테스트 데이터 실행하기

개발 도구 체인이 선택된 macOS 환경을 준비하고 xcrun swift --version을 실행해 컴파일러를 사용할 수 있는지 확인합니다. 아래 내용을 distance-check.swift로 저장합니다. 이 스크립트는 현재 위치를 읽지 않으므로 위치 권한을 요청할 필요가 없으며, 네트워크에도 접근하지 않습니다.

import Foundation
import CoreLocation

enum CoordinateError: Error {
    case invalid
}

func point(_ latitude: Double, _ longitude: Double) throws -> CLLocation {
    let coordinate = CLLocationCoordinate2D(
        latitude: latitude,
        longitude: longitude
    )
    guard latitude.isFinite,
          longitude.isFinite,
          CLLocationCoordinate2DIsValid(coordinate) else {
        throw CoordinateError.invalid
    }
    return CLLocation(latitude: latitude, longitude: longitude)
}

func rejects(_ latitude: Double, _ longitude: Double) -> Bool {
    do {
        _ = try point(latitude, longitude)
        return false
    } catch {
        return true
    }
}

let origin = try point(0, 0)
precondition(origin.distance(from: origin) == 0)

let east = try point(0, 179.9)
let west = try point(0, -179.9)
let crossing = east.distance(from: west)
precondition(crossing > 20_000 && crossing < 23_000)
precondition(abs(crossing - west.distance(from: east)) < 1)

let polarA = try point(89.9, 0)
let polarB = try point(89.9, 180)
let polarDistance = polarA.distance(from: polarB)
precondition(polarDistance > 20_000 && polarDistance < 23_000)

precondition(rejects(91, 0))
precondition(rejects(0, 181))
precondition(rejects(.nan, 0))
precondition(rejects(0, .infinity))

다음 명령으로 실행합니다. 모든 단언을 통과하면 아무것도 출력하지 않고, 실패하면 0이 아닌 종료 상태를 반환하므로 파이프라인 단계로 바로 사용할 수 있습니다.

xcrun swift distance-check.swift

위의 범위는 테스트 데이터에 적용한 넉넉한 허용 오차일 뿐, 성능 측정 결과나 서비스의 정확도 보장이 아닙니다. 날짜 변경선 양쪽에 있는 두 점은 서로 가까울 수 있습니다. 극지방에서는 경도 차이가 크더라도 지구 반 바퀴만큼 떨어져 있다는 뜻이 아닙니다. 두 테스트 모두 위도와 경도를 평면 좌표처럼 단순히 빼는 오류를 잡아낼 수 있습니다.

지오펜스 경계 판정을 순수 함수로 분리하기

비즈니스 규칙이 ‘거리가 반경 이하이면 진입으로 판정한다’라고 가정해 보겠습니다. 먼저 직접 지정한 거리로 비교 함수를 테스트한 뒤 좌표 계산 결과를 연결하면, 두 계층의 문제를 동시에 디버깅하지 않아도 됩니다. 같은 파일 끝에 아래 코드를 추가하고 다시 실행합니다.

func isInside(distance: Double, radius: Double) -> Bool {
    guard distance.isFinite, radius.isFinite,
          distance >= 0, radius >= 0 else {
        return false
    }
    return distance <= radius
}

precondition(isInside(distance: 499.9, radius: 500))
precondition(isInside(distance: 500, radius: 500))
precondition(!isInside(distance: 500.1, radius: 500))
precondition(!isInside(distance: .nan, radius: 500))
precondition(!isInside(distance: 0, radius: -1))

let meters = 1_250.0
precondition(meters / 1_000 == 1.25)

경계 테스트에는 명백히 안쪽이거나 바깥쪽인 경우뿐 아니라 거리가 반경과 정확히 같은 경우도 포함해야 합니다. 화면에 ‘0.5킬로미터’로 표시된 문자열을 근거로 판정 결과를 역산해서는 안 됩니다. 원래 거리가 달라도 반올림하면 같은 문자열로 표시될 수 있습니다.

제품에서 경계 부근의 잦은 상태 전환을 줄여야 한다면, 진입과 이탈에 서로 다른 임계값을 쓰는 히스테리시스 정책을 정의할 수 있습니다. 이는 상태 머신에 해당하므로 연속된 입력 시퀀스에 대한 테스트를 추가해야 합니다. 비교 함수에 ‘대충 이 정도’라는 상수를 임의로 더해서 해결할 문제가 아닙니다.

위치 정확도와 계산 허용 오차 구분하기

실제 위치 샘플에는 타임스탬프와 수평 정확도도 포함됩니다. 입력 연동 계층에서는 오래된 샘플을 명시적으로 거부하고 수평 정확도가 음수인 경우도 처리해야 합니다. 좌표가 유효하더라도 정확도가 낮다면 곧바로 ‘진입 확정’ 이벤트를 생성해서는 안 됩니다.

판정 근거를 검토할 수 있는 보수적 규칙은 다음과 같습니다. 거리를 d, 수평 정확도를 a, 반경을 r로 둡니다. d+a≤r일 때만 확실히 내부에 있다고 판정하고, d−a>r일 때만 확실히 외부에 있다고 판정하며, 나머지는 불확실 상태로 표시합니다. 이는 비즈니스 차원의 보수적 판단 규칙이지, 정확도 값이 절대적인 오차 한계를 보장한다는 뜻은 아닙니다.

테스트에서는 정확한 샘플, 정확도가 낮아 모호한 샘플, 오래된 샘플, 무효한 샘플을 각각 주입합니다. 불확실 상태가 유지되는지, 화면에서 확정된 결과로 잘못 표시하지는 않는지 확인해야 합니다. 계산 단언의 허용 범위를 넓혀 위치 측정의 노이즈를 감추어서는 안 됩니다.

검증 기록을 남기고 iOS 프로젝트에 연결하기

위 스크립트는 프레임워크의 동작을 빠르게 재확인하는 데 적합합니다. 실제 프로젝트에 통합할 때는 좌표 유효성 검사와 비교 함수를 테스트 가능한 모듈에 넣고, 테스트 타깃에서 프로덕션 구현을 직접 호출하도록 합니다. 알고리즘은 한 벌만 유지해야 스크립트는 올바른데 앱은 여전히 예전 로직을 사용하는 상황을 막을 수 있습니다.

실행할 때마다 커밋 식별자, 도구 체인 버전, 테스트 데이터의 좌표, 원시 미터 값, 실패한 단언을 기록해 둡니다. 시스템 업그레이드 후에는 실패 원인이 입력 유효성 검사인지, 거리 범위인지, 상태 정책인지 먼저 확인해야 합니다. 모든 기댓값을 곧바로 갱신해서는 안 됩니다.

마지막으로 iOS 실제 기기에서의 인수 검증을 한 차례 남겨 두어야 합니다. 영역 모니터링 이벤트, 백그라운드 실행, 실제 이동은 별도의 처리 경로에 속합니다. 순수 계산 테스트를 통과했다는 것은 고정 입력에서 계약에 맞는 결과를 얻었다는 뜻일 뿐입니다. 이 검증 범위의 한계를 인수 검증 기록에 명시하는 것이 무작위 좌표를 더 추가하는 것보다 가치 있습니다.

자주 묻는 질문

Mac 스크립트가 통과하면 iOS 지오펜싱 검증이 끝나나요?

아닙니다. 스크립트는 좌표 처리, 거리 계산과 경계 정책을 검증합니다. 영역 이벤트, 백그라운드 동작과 실제 위치 정확도는 iOS 기기에서 별도로 확인해야 합니다.

화면에 표시된 킬로미터 값으로 영역 진입을 판단해도 되나요?

안 됩니다. 업무 로직은 반올림하지 않은 미터 값을 사용해야 합니다. 단위 변환과 반올림은 표시 단계에서 수행하고 위치 정확도 처리 정책은 별도로 정의하세요.

워크플로에 맞는 전용 Mac mini를 구성하세요

프로젝트에 맞춰 모델, 노드, 대여 기간을 선택하세요. 주문 전에 메모리, 저장 공간, 연결 요구 사항을 확인하세요. 실제 이용 가능 여부는 콘솔에 실시간으로 표시됩니다.

먼저 워크플로를 정하고, 바로 시작하세요.

전용 물리 Mac mini를 이용하세요. 모델, 노드, 대여 기간을 선택한 다음 프로젝트에 필요한 저장 공간을 구성하면 됩니다.