클라우드 Mac에서 iOS 반복 일정의 서머타임 경계 검증하기

클라우드 Mac에서 iOS 반복 일정의 서머타임 경계 검증하기

클라우드 Mac에서 iOS 반복 일정의 서머타임 경계 검증하기

“매일 현지 시각 02:30에 한 번 동기화”하는 작업을 백그라운드에 맡기면 평소에는 잘 동작해도 서머타임 전환일에는 실행이 누락되거나 중복될 수 있습니다. 먼저 클라우드 Mac에서 다음 실행 시각 계산을 순수 함수로 분리하고, 고정된 날짜로 검증한 뒤 알림이나 백그라운드 스케줄링에 연결하세요. 여기서 검증하는 것은 달력 계산의 계약입니다. 운영체제가 실제로 앱을 깨운 시각을 알고리즘의 결과로 취급하지 않습니다.

현지 시각과 경과 시간을 먼저 구분하기

“스물네 시간마다”와 “매일 02:30에”는 서로 다른 규칙입니다. 전자는 경과한 초 수를 기준으로 다음 시각을 계산하고, 후자는 특정 시간대의 달력을 기준으로 계산합니다. 하루가 항상 스물네 시간인 것은 아니므로, 두 규칙을 같은 초 단위 덧셈 로직으로 구현할 수는 없습니다.

매일 실행하는 규칙에는 시, 분, 시간대 식별자, 정책 버전을 저장하세요. 다음 실행 시각을 절대 시각으로 저장하면 조회는 편리하지만, 원래 규칙을 대신할 수는 없습니다. 사용자가 시간대나 정책을 변경하면 다시 계산해야 합니다.

Date는 절대 시각을 나타내며 시간대 정보를 자체적으로 갖지 않습니다. “매일 몇 시”는 달력 규칙이므로, 명시적인 Calendar 및 TimeZone과 함께 해석해야 합니다.

일정이 어느 시간대를 따를지도 결정해야 합니다. 생성 당시의 시간대로 고정할지, 사용자의 현재 위치를 따를지 정하세요. 클라우드 머신의 현재 시간대가 이 제품 정책을 대신 결정하게 해서는 안 됩니다. 고정 오프셋만 저장해서도 안 됩니다. 오프셋만으로는 향후 서머타임 변화를 표현할 수 없습니다.

존재하지 않거나 두 번 나타나는 시각의 처리 정책 정하기

봄에 시계를 앞으로 돌리면 02:30이 존재하지 않을 수 있습니다. 그날 실행을 건너뛸 수도 있고, 그날 이후 처음으로 유효한 시각까지 미룰 수도 있으며, 분 값을 유지한 채 뒤로 옮길 수도 있습니다. 세 가지 선택의 결과는 서로 다릅니다.

Calendar의 .strict는 정확히 일치하는 시각을 요구합니다. 매일 실행하는 규칙에서 해당 시각이 존재하지 않으면 이후 날짜를 계속 검색합니다. .nextTime은 이후의 유효한 시각을 선택하지만, 원래 분 값까지 유지한다고 가정해서는 안 됩니다. 분 값을 유지해야 한다면 .nextTimePreservingSmallerComponents를 별도로 검증하세요.

가을에 시계를 뒤로 돌리면 02:30이 두 번 나타날 수 있습니다. .first 또는 .last로 어느 시각을 사용할지 명시하세요. 제품 요구사항상 두 번 모두 실행해야 한다면 두 후보 시각과 각각의 이벤트 식별자를 설계해야 합니다. 매칭 호출 한 번으로 전체 구현이 끝났다고 볼 수는 없습니다.

고정 날짜로 Swift 인수 테스트 스크립트 실행하기

아래 내용을 recurrence.swift로 저장하세요. 예제는 그레고리력과 명시적으로 지정한 시간대를 사용합니다. 테스트 날짜는 경계 조건 검증용 픽스처이지, 측정을 수행하는 날짜가 아닙니다. 머신의 시각을 바꾸거나 시뮬레이터를 실행할 필요가 없습니다.

import Foundation

var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "Europe/Berlin")!

let formatter = ISO8601DateFormatter()

func instant(_ value: String) -> Date {
    guard let date = formatter.date(from: value) else {
        fatalError("Invalid fixture date")
    }
    return date
}

func next(
    after cursor: Date,
    policy: Calendar.MatchingPolicy,
    repeated: Calendar.RepeatedTimePolicy
) -> Date {
    guard let date = calendar.nextDate(
        after: cursor,
        matching: DateComponents(hour: 2, minute: 30, second: 0),
        matchingPolicy: policy,
        repeatedTimePolicy: repeated,
        direction: .forward
    ) else {
        fatalError("No matching occurrence")
    }
    precondition(date > cursor)
    return date
}

let spring = instant("2027-03-28T00:00:00+01:00")

let skipped = next(
    after: spring,
    policy: .strict,
    repeated: .first
)
precondition(skipped == instant("2027-03-29T02:30:00+01:00"))

let shifted = next(
    after: spring,
    policy: .nextTime,
    repeated: .first
)
precondition(shifted == instant("2027-03-28T03:00:00+02:00"))

let autumn = instant("2027-10-31T00:00:00+02:00")

let first = next(
    after: autumn,
    policy: .strict,
    repeated: .first
)
let last = next(
    after: autumn,
    policy: .strict,
    repeated: .last
)
precondition(first == instant("2027-10-31T02:30:00+02:00"))
precondition(last == instant("2027-10-31T02:30:00+01:00"))
precondition(last.timeIntervalSince(first) == 3600)

선택한 개발 도구 체인으로 실행하세요.

xcrun swift recurrence.swift

성공하면 스크립트는 아무 출력 없이 종료되고, 단언문 검증에 실패하면 실패 상태로 종료됩니다. 로그가 생성되었는지만 확인하지 말고, 실패가 파이프라인에 전달되도록 연결하세요. 도구 체인이나 운영체제를 업그레이드한 뒤에는 픽스처를 다시 실행하고, 운영체제 버전, 도구 체인 버전, 시간대 식별자를 함께 기록하세요.

이 스크립트는 02:30으로 고정된 규칙만 검증합니다. 프로젝트에 통합할 때는 시와 분을 매개변수로 만들고, 일반 날짜, 월말, 연말, 윤일 테스트 사례를 추가하세요. 각 사례에는 예상 절대 시각을 명시해, 서식이 적용된 문자열만 비교하는 데 그치지 않도록 하세요.

커서 이동과 누락분 재실행의 중복 방지 검증하기

여러 후보 시각을 계산할 때는 직전 반환값을 다음 검색의 커서로 사용하고, 결과가 매번 이전 값보다 큰지 확인하세요. 1초를 뺀 뒤 다시 조회해서는 안 됩니다. 경계에 정확히 일치하면 같은 이벤트가 다시 반환될 수 있습니다. 일괄 검색에는 반복 횟수 상한도 두어, 비정상적인 규칙이 실행기를 오랫동안 점유하지 않도록 하세요.

누락분을 재실행할 때는 고정된 구간을 사용하세요. 시작점은 제외하고 끝점은 포함합니다. 먼저 끝점을 고정한 다음 후보를 열거하고, 반복문 안에서 현재 시각을 계속 읽지 마세요. 그렇지 않으면 오래 실행되는 작업의 재실행 범위가 계속 늘어나고, 인수 테스트 결과도 재현할 수 없게 됩니다.

하루에 한 번만 실행한다면 “일정 식별자+규칙 버전+규칙 시간대의 현지 날짜”로 중복 방지 키를 만드세요. 완료 기록은 영구 저장해야 합니다. 비즈니스 작업이 외부 부수 효과를 일으킨다면 이 키를 비즈니스 계층에도 전달해야 하며, 메모리에만 성공으로 표시해서는 안 됩니다.

처리 도중 중단되는 상황도 테스트하세요. 비즈니스 작업은 완료했지만 완료 기록은 아직 영구 저장소에 기록되지 않은 상태에서 중단을 재현합니다. 재시도 시에는 비즈니스 계층의 멱등성 규칙이 동일 작업의 재수행을 막아야 합니다. 스케줄러의 중복 방지를 트랜잭션 보장으로 간주해서는 안 됩니다.

달력 계산의 정확성과 시스템 스케줄링을 분리해 검증하기

순수 함수가 테스트를 통과한 뒤에는 시뮬레이터에서 규칙 편집, 표시, 영구 저장한 데이터의 재조회를 테스트하세요. 백그라운드 실행 기회는 시스템이 결정하므로 초 단위로 정확한 실행을 보장할 수 없습니다. “예정 실행 시각”과 “실제 실행 시각”을 별도 필드로 표시해야 문제를 분석할 때 지연을 달력 계산 오류로 오인하지 않습니다.

마지막으로 네 가지를 확인하세요. 테스트가 기본 시간대를 읽지 않는지, 매일 실행하는 규칙이 고정된 초 수만큼 이동하지 않는지, 중복 시각 처리 정책을 명시적으로 전달하는지, 누락분 재실행에 고정된 구간과 영구 저장된 식별자를 사용하는지 확인합니다. 서머타임 전환을 가로지르는 회귀 테스트에서는 계산 결과와 실행 기록을 비교해야 합니다. 시스템 시계를 바꾸며 우연히 문제가 드러나기를 기대해서는 안 됩니다.

자주 묻는 질문

매일 실행하려면 이전 실행 시각에 이십사 시간을 더하면 되나요?

고정된 경과 시간이 요구될 때만 적합합니다. 현지 시각을 매일 유지하려면 명시적인 시간대의 Calendar에서 시·분·초를 매칭해야 서머타임 전환 뒤 시각이 밀리지 않습니다.

서머타임 종료 때 반복되는 시각에는 두 번 실행해야 하나요?

첫 번째, 두 번째 또는 두 번 모두 실행할지 제품 규칙으로 정해야 합니다. 하루 한 번이라면 UTC 타임스탬프만 쓰지 말고 일정 식별자, 규칙 버전, 현지 날짜로 중복을 막습니다.

전용 물리 Mac에서 프로젝트 실행

클라우드 Mac으로 빌드와 테스트를 진행하고 원격 데스크톱을 이용해 보세요. 프로젝트에 맞는 사양과 요금 주기를 선택할 수 있으며, 실제 이용 가능 여부는 콘솔에 실시간으로 표시됩니다.

다음 단계: 실행 환경 확인

먼저 사양을 비교한 뒤 접속 방법을 선택하세요. SSH 명령줄은 빌드에, VNC 데스크톱은 직접 조작하며 디버깅할 때 유용합니다.