서머타임 전환 시간대 테스트로 일일 보상과 이벤트 타이머 자동 검증하기
서머타임 전환일에 게임의 일일 보상 리셋과 이벤트 카운트다운이 어긋나는 원인을 분석하고 IANA 시간대와 UTC 기준으로 스프링 포워드·폴백을 자동 검증하는 테스트 설계와 CI 운영 방법을 정리합니다.

핵심 요약
서머타임 문제는 서버와 클라이언트가 서로 다른 시간대를 사용하거나 현지 시각을 24시간 경과 시간처럼 계산할 때 발생합니다. 일일 보상 리셋은 플레이어의 IANA 시간대 기준 날짜로 판정하고 이벤트 타이머는 UTC 절대 시각의 차이로 계산한 뒤 서머타임 전환 경계에서 자동 테스트해야 합니다.
핵심 검증 대상은 다음 세 가지입니다.
- 스프링 포워드에서 존재하지 않는 현지 시각을 안전하게 처리하는가
- 폴백에서 같은 시각이 두 번 나타나도 보상 중복 지급이나 타이머 역행이 없는가
- 서버 재시작과 클라이언트 재접속 이후에도 동일한 UTC 기준 결과를 재현하는가
서머타임 전환 때 게임 시간 버그가 발생하는 이유는 무엇인가?
서머타임은 현지 시계를 한 시간 앞당기거나 되돌리는 제도입니다. 따라서 현지 시각과 실제 경과 시간은 항상 같은 방식으로 움직이지 않습니다.
| 전환 유형 | 현지 시각 변화 | 주요 위험 |
|---|---|---|
| 스프링 포워드 | 01:59:59 다음에 03:00:00이 됨 | 존재하지 않는 02시대에 리셋을 예약함 |
| 폴백 | 01:59:59 다음에 다시 01:00:00이 됨 | 같은 현지 시각을 두 번 처리하거나 보상을 중복 지급함 |
| 시간대 미설정 | 서버 기본 시간대 또는 UTC를 사용함 | 플레이어별 일일 리셋 시점이 달라짐 |
| 클라이언트 계산 | 기기 로컬 시각으로 남은 시간을 계산함 | 기기 시간 조작과 시간대 변경으로 타이머가 왜곡됨 |
예를 들어 미국 뉴욕 시간대에서 스프링 포워드가 발생하면 02:30이라는 현지 시각은 존재하지 않습니다. 반대로 폴백에서는 01:30이 서로 다른 UTC 시각으로 두 번 나타날 수 있습니다.
따라서 테스트에서는 LocalDateTime만 비교해서는 안 됩니다. 다음 세 값을 분리해 기록해야 합니다.
- 절대 시각: UTC 또는
Instant - 표시 시각: 플레이어 시간대의 현지 시각
- 시간대 식별자:
America/New_York,Europe/Berlin같은 IANA ID
일일 보상과 이벤트 타이머의 시간 기준은 어떻게 분리해야 하는가?
두 기능은 시간의 의미가 다르므로 같은 계산식을 사용하면 안 됩니다.
| 기능 | 기준 | 권장 표현 | 판정 방식 |
|---|---|---|---|
| 일일 로그인 보상 | 플레이어 현지 날짜 | Instant를 플레이어 시간대로 변환한 LocalDate | 날짜별 클레임 키로 멱등 처리 |
| 시즌 이벤트 종료 | 전 세계 공통 시점 | UTC Instant | 종료 시각과 현재 시각의 차이 계산 |
| 서버 점검 예약 | 운영 지역의 현지 시각 | IANA 시간대 + 현지 시각 | 정책에 따라 모호한 시각 해소 |
| 클라이언트 카운트다운 | 서버가 전달한 절대 시각 | eventEndUtc - serverNowUtc | 클라이언트 로컬 시계 사용 금지 |
일일 보상은 다음처럼 계산해야 합니다.
claimKey = playerId + ":" + localDate(playerNowUtc, playerTimeZone)
이 구조에서는 플레이어가 뉴욕에 있든 베를린에 있든 자신의 현지 날짜를 기준으로 하루 한 번만 보상을 받을 수 있습니다. 단순히 UTC 날짜를 사용하면 자정 부근의 플레이어에게 의도하지 않은 지급 시점이 생깁니다.
이벤트 타이머는 UTC 절대 시각을 기준으로 계산합니다.
현지 시계가 한 시간 뒤로 돌아가는 폴백에서도 두 UTC 시각의 차이는 계속 증가해야 합니다. 화면에 표시할 때만 플레이어 시간대로 변환하면 됩니다.
자동 시간대 테스트는 어떤 구조로 설계해야 하는가?
권장 구성은 시간대 데이터 공급자, 시간 계산 서비스, 데이터 기반 테스트, CI 검증의 네 계층입니다.
1. IANA 시간대와 tzdb 버전을 고정한다
서버 테스트 기준으로 .NET 8, C# 12, NUnit 4와 Noda Time 3.x를 사용할 수 있습니다. Noda Time의 DateTimeZoneProviders.Tzdb는 IANA tzdb를 사용하므로 Windows와 Linux의 시간대 ID 차이를 줄이는 데 유리합니다.
운영 환경에서 다음 정보를 함께 기록하는 것이 좋습니다.
- 시간대 ID:
America/New_York - tzdb 버전
- 서버 현재 시각의 UTC 값
- 원본 이벤트 종료 시각
- 변환 후 현지 시각
- UTC 오프셋
시간대 규칙은 법률과 운영 정책에 따라 바뀔 수 있습니다. 그러므로 테스트 결과만 저장하지 말고 실행 당시 사용한 tzdb 버전도 빌드 로그에 남겨야 합니다.
2. 전환 경계에서 테스트 시각을 자동 생성한다
특정 연도의 날짜를 하드코딩하는 방식은 다음 해에 약해집니다. 시간대 데이터에서 오프셋이 바뀌는 전환 순간을 찾고 전환 전후에 테스트 시각을 생성하는 방식이 더 안전합니다.
테스트 케이스는 최소한 다음 범위를 포함해야 합니다.
- 전환 1초 전
- 전환 순간
- 전환 1초 후
- 전환 30분 전후
- 일일 리셋 00:00 또는 운영 정책으로 정한 리셋 시각
- 이벤트 종료 시각이 전환 경계와 겹치는 경우
3. 현지 시각의 유효성과 모호성을 검사한다
스프링 포워드에서는 특정 현지 시각이 사라집니다. 폴백에서는 하나의 현지 시각에 두 개의 오프셋이 대응합니다.
제품 정책을 먼저 정하고 테스트에 반영해야 합니다.
| 상황 | 예시 | 가능한 정책 |
|---|---|---|
| 건너뛴 시각 | 02:30이 존재하지 않음 | 다음 유효 시각으로 이동하거나 리셋 시각을 금지 |
| 반복된 시각 | 01:30이 두 번 존재함 | 이전 오프셋 또는 이후 오프셋을 명시적으로 선택 |
| 전환 중 서버 재시작 | 전환 직전과 직후 재기동 | UTC 기반으로 동일한 이벤트 상태 복원 |
정책을 정의하지 않은 상태에서 라이브 서버의 기본 변환 함수에 의존하면 OS와 런타임에 따라 결과가 달라질 수 있습니다.
.NET 8과 Noda Time으로 서머타임 전환을 검증하는 예제
아래 예제는 전환 순간 전후의 절대 시간이 1초 차이를 유지하는지 확인하고 현지 표시 시각의 오프셋 변화도 함께 검증하는 형태입니다.
using NodaTime;
using NUnit.Framework;
public class DaylightSavingTransitionTests
{
[TestCase("America/New_York")]
[TestCase("Europe/Berlin")]
public void Transition_keeps_elapsed_time_monotonic(string zoneId)
{
var zone = DateTimeZoneProviders.Tzdb[zoneId];
var from = Instant.FromUtc(2026, 1, 1, 0, 0);
var to = Instant.FromUtc(2027, 1, 1, 0, 0);
foreach (var transition in FindTransitions(zone, from, to))
{
var before = transition - Duration.FromSeconds(1);
var after = transition;
var beforeLocal = before.InZone(zone);
var afterLocal = after.InZone(zone);
Assert.That((after - before).TotalSeconds, Is.EqualTo(1));
Assert.That(after, Is.GreaterThan(before));
Assert.That(beforeLocal.Offset, Is.Not.EqualTo(afterLocal.Offset));
}
}
private static IEnumerable<Instant> FindTransitions(
DateTimeZone zone,
Instant from,
Instant to)
{
var interval = zone.GetZoneInterval(from);
while (interval.HasEnd && interval.End < to)
{
yield return interval.End;
interval = zone.GetZoneInterval(
interval.End + Duration.FromSeconds(1));
}
}
}
이 테스트의 핵심은 현지 시각의 숫자 순서를 검증하지 않는 것입니다. 폴백에서는 현지 시각이 뒤로 이동할 수 있지만 Instant의 순서는 절대 뒤집히면 안 됩니다.
일일 보상 리셋은 어떻게 자동 검증해야 하는가?
일일 보상은 날짜 기반 멱등성이 가장 중요합니다. 보상 지급 API가 같은 플레이어와 같은 현지 날짜에 대해 여러 번 호출되어도 실제 지급은 한 번만 일어나야 합니다.
권장 불변 조건은 다음과 같습니다.
- 같은
playerId, 같은 시간대, 같은 현지 날짜에서는 클레임 결과가 한 번만 성공한다. - UTC 자정과 플레이어 현지 자정이 달라도 클레임 날짜가 섞이지 않는다.
- 폴백으로 같은 현지 시각이 반복되어도 클레임 키가 중복 생성되지 않는다.
- 서버 재시작 전후에 동일한 입력을 사용하면 동일한 클레임 상태를 반환한다.
public sealed record RewardClaimResult(
bool Granted,
string ClaimKey,
LocalDate LocalDate);
public RewardClaimResult ClaimDailyReward(
string playerId,
Instant serverNowUtc,
DateTimeZone playerZone,
IRewardClaimStore store)
{
var localDate = serverNowUtc.InZone(playerZone).Date;
var claimKey = $"{playerId}:{playerZone.Id}:{localDate} ";
var granted = store.TryCreateOnce(claimKey);
return new RewardClaimResult(granted, claimKey, localDate);
}
실제 코드에서는 문자열 끝의 공백이 생기지 않도록 다음처럼 작성해야 합니다.
var claimKey = $"{playerId}:{playerZone.Id}:{localDate}";
이 테스트는 다음 시나리오를 데이터 기반으로 실행할 수 있습니다.
| 시나리오 | 입력 | 기대 결과 |
|---|---|---|
| 스프링 포워드 직전 | 01:59:59 현지 시각 | 현재 날짜의 보상만 지급 |
| 스프링 포워드 직후 | 03:00:00 현지 시각 | 날짜가 바뀌지 않았다면 중복 지급 없음 |
| 폴백 첫 번째 01:30 | 이전 UTC 오프셋 | 해당 현지 날짜의 첫 클레임 |
| 폴백 두 번째 01:30 | 이후 UTC 오프셋 | 같은 클레임 키로 거부 |
| UTC 자정 직후 | 플레이어 현지 날짜는 전날 | 현지 날짜 기준으로 판정 |
이벤트 타이머가 서머타임에서 역행하지 않는지 확인하는 방법
이벤트 서버는 eventEndUtc를 저장하고 클라이언트에는 서버 기준 현재 시각과 종료 시각을 전달해야 합니다. 클라이언트가 자신의 운영체제 시계로 남은 시간을 계산하면 사용자가 기기 시계를 변경했을 때 타이머가 늘어나거나 음수가 될 수 있습니다.
public static long GetRemainingSeconds(
Instant serverNowUtc,
Instant eventEndUtc)
{
var remaining = eventEndUtc - serverNowUtc;
return Math.Max(0, (long)Math.Floor(remaining.TotalSeconds));
}
검증할 불변 조건은 다음과 같습니다.
- 시간이 앞으로 흐르면 남은 시간이 증가하지 않는다.
- 이벤트 종료 이후 남은 시간은 음수가 아니다.
- 현지 오프셋이 바뀌어도 같은
eventEndUtc에 대한 남은 시간은 연속적이다. - 서버와 클라이언트의 표시 시간대가 달라도 남은 초 수는 같다.
- 재접속 시 클라이언트가 마지막으로 보관한 카운트다운을 신뢰하지 않고 서버 값을 다시 사용한다.
특히 폴백 구간에서는 화면에 표시되는 현지 시각이 01:59에서 01:00으로 돌아갈 수 있습니다. 이것은 표시 시각의 변화이지 이벤트 타이머가 역행했다는 뜻이 아닙니다. QA 로그에는 현지 시각과 함께 UTC 시각과 오프셋을 표시해야 오탐을 줄일 수 있습니다.

CI에서 시간대 테스트를 안정적으로 실행하는 3단계
1단계: 대표 시간대를 선정한다
최소 구성으로 다음 시간대를 권장합니다.
America/New_York: 스프링 포워드와 폴백을 모두 확인Europe/Berlin: 유럽 규칙과 미국 규칙의 차이 확인Asia/Seoul: 서머타임이 없는 지역에서 일반 일일 리셋 확인UTC: 기준 시간대 자체의 계산 오류 확인
서비스가 실제로 지원하는 지역이 있다면 대표 지역을 추가해야 합니다. 시간대 수가 많을 경우 모든 지역을 매 빌드에서 실행하기보다 tzdb 전체 스모크 테스트와 서비스별 대표 지역 테스트를 분리할 수 있습니다.
2단계: 시스템 시계를 바꾸지 말고 시간을 주입한다
테스트 실행 중 OS 시간을 변경하면 병렬 테스트와 다른 프로세스에 영향을 줍니다. IClock 또는 자체 IGameClock 인터페이스를 만들고 Instant를 주입하는 방식이 안전합니다.
public interface IGameClock
{
Instant UtcNow { get; }
}
public sealed class FixedGameClock : IGameClock
{
public FixedGameClock(Instant utcNow) => UtcNow = utcNow;
public Instant UtcNow { get; private set; }
public void Set(Instant utcNow) => UtcNow = utcNow;
}
3단계: 실패 시 재현 정보를 남긴다
실패한 테스트에는 다음 값을 모두 출력해야 합니다.
zoneId=America/New_York
tzdb=2026a
serverNowUtc=2026-11-01T05:59:59Z
serverNowLocal=2026-11-01T01:59:59-04:00
eventEndUtc=2026-11-01T07:00:00Z
remainingSeconds=3601
claimKey=player-42:America/New_York:2026-11-01
로그를 남기면 폴백으로 인해 현지 시각이 반복된 것인지 실제 타이머 역행인지 빠르게 구분할 수 있습니다. 빌드 파이프라인에서는 시간대 데이터가 업데이트됐을 때 전체 시간대 회귀 테스트를 다시 실행하도록 구성하는 것도 좋습니다.
패치 전후에 확인할 운영 체크리스트
패치 전
- 지원 시간대 목록과 IANA ID를 검토한다.
- 일일 리셋 시각이 존재하지 않는 현지 시각인지 확인한다.
- 이벤트 종료 시각이 UTC로 저장되는지 확인한다.
- 전환 전후 30분 구간의 자동 테스트를 실행한다.
- 보상 클레임 API의 멱등 키가 시간대와 현지 날짜를 올바르게 포함하는지 확인한다.
패치 직후
- 전환 구간의 보상 지급 성공률과 중복 지급 로그를 모니터링한다.
- 이벤트 타이머가 0보다 큰 값에서 갑자기 증가하지 않는지 확인한다.
- 서버 재시작과 클라이언트 재접속 시 종료 시각이 유지되는지 확인한다.
- 현지 시각만 기록된 기존 로그가 있다면 UTC와 오프셋을 추가한다.
장애가 발생했을 때
- 플레이어 시간대 ID와 요청 시각의 UTC 값을 수집한다.
- 보상 클레임 키와 이벤트 종료 시각의 저장 형식을 확인한다.
- 동일한 입력을 고정 시계 테스트로 재현한다.
- 중복 지급이면 데이터베이스의 유일성 제약과 멱등 처리부터 확인한다.
- 타이머 오류면 클라이언트 로컬 시계가 계산에 사용됐는지 확인한다.
자주 묻는 질문 (FAQ)
서머타임이 없는 한국 시간대도 테스트해야 하나요?
예. Asia/Seoul은 전환이 없지만 UTC 자정과 현지 자정의 날짜 변환, 일반 일일 리셋, 서버 재시작 복원을 검증하는 기준 시간대가 됩니다.
일일 보상 리셋을 무조건 UTC 자정으로 통일하면 안 되나요?
가능하지만 게임 기획이 전 세계 공통 리셋을 의도한 경우에만 적합합니다. 플레이어 현지 자정 보상을 제공하려면 현지 날짜를 사용하고 전역 이벤트 종료는 UTC 절대 시각을 사용하는 식으로 정책을 분리해야 합니다.
폴백에서 같은 01:30을 두 번 지급하면 어떻게 막나요?
현지 시각 문자열만 클레임 키로 사용하지 말고 playerId, 시간대 ID, 현지 날짜를 포함한 멱등 키를 데이터베이스에서 유일하게 관리해야 합니다. 두 호출이 서로 다른 UTC 오프셋을 가지더라도 같은 현지 날짜의 클레임은 한 번만 성공해야 합니다.
정리
서머타임 전환 테스트의 핵심은 현지 시각과 절대 시각을 분리하는 것입니다. 일일 보상은 플레이어 시간대의 LocalDate와 멱등 키로 검증하고 이벤트 타이머는 UTC Instant의 차이로 계산해야 합니다.
.NET 8과 C# 12 환경에서는 시간 주입, IANA tzdb 기반 전환 탐색, 전환 전후 데이터 기반 테스트를 CI에 포함하면 특정 날짜를 하드코딩하지 않고도 스프링 포워드와 폴백 회귀를 반복 검증할 수 있습니다.



