DateTime과 DateTimeOffset은 비슷해 보이지만 용도가 다릅니다. DateTime은 달력 시각을 표현하며 오프셋 정보가 없거나 Kind로만 힌트를 줍니다. DateTimeOffset은 "시각 + 오프셋"을 함께 담아 전 세계 어디서나 동일한 순간(instant)을 안전하게 비교하고 전달할 수 있습니다.
1. 한눈에 개념 정리
DateTime: 달력 시각입니다. Kind가 Utc, Local, Unspecified로 구분되며 오프셋 값은 없습니다. 즉, 어느 시간대 기준인지가 불명확해질 수 있습니다.
DateTimeOffset: 시각과 오프셋(예: +09:00)을 함께 저장합니다. 서로 다른 오프셋이어도 같은 순간인지 안전하게 비교할 수 있습니다.
var local = DateTime.Now; // Kind = Local
var utc = DateTime.UtcNow; // Kind = Utc
var uns = new DateTime(2024, 5, 1, 9, 0, 0); // Kind = Unspecified
Console.WriteLine($"Kinds: {local.Kind}, {utc.Kind}, {uns.Kind}");
Console.WriteLine($"Ticks equal? {utc.Ticks == local.ToUniversalTime().Ticks}");
var dtoLocal = DateTimeOffset.Now;
var dtoUtc = DateTimeOffset.UtcNow;
var dtoWithOffset = new DateTimeOffset(2024, 5, 1, 9, 0, 0, TimeSpan.FromHours(9));
Console.WriteLine(dtoWithOffset.Offset); // +09:00
2. DateTimeKind가 만드는 함정
Unspecified는 해석 기준이 없어서 변환/직렬화 시 버그를 만들기 쉽습니다. Local은 실행 환경에 따라 달라지고, Utc만이 전송/보관 시 안전합니다. 외부 경계를 넘는 값은 반드시 Utc 또는 DateTimeOffset으로 통일하는 것이 좋습니다.
// Unspecified는 피합니다. 정책적으로 거부하거나 Utc로 지정합니다.
public static DateTime EnsureUtc(DateTime dt)
{
return dt.Kind switch
{
DateTimeKind.Utc => dt,
DateTimeKind.Local => dt.ToUniversalTime(),
_ => DateTime.SpecifyKind(dt, DateTimeKind.Utc) // 팀 정책에 따라 명확히
};
}
3. 같은 순간 비교: DateTimeOffset의 강점
서로 다른 오프셋을 가진 DateTimeOffset도 같은 순간이면 동등합니다. 반면 DateTime은 오프셋 정보가 없어 직접 Utc 변환을 맞춰야 안전합니다.
var a = new DateTimeOffset(2024, 5, 1, 9, 0, 0, TimeSpan.FromHours(9)); // 00:00Z
var b = new DateTimeOffset(2024, 5, 1, 0, 0, 0, TimeSpan.Zero); // 00:00Z
Console.WriteLine(a == b); // True (같은 instant)
Console.WriteLine(a.UtcDateTime == b.UtcDateTime); // True
4. 시간대 변환은 TimeZoneInfo로
특정 시간대 시각으로 보여줄 때는 TimeZoneInfo를 사용합니다. DateTimeOffset은 ConvertTime 오버로드가 있어 간단합니다.
var seoul = new DateTimeOffset(2024, 5, 1, 9, 0, 0, TimeSpan.FromHours(9));
var ny = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
var nyClock = TimeZoneInfo.ConvertTime(seoul, ny);
Console.WriteLine(nyClock); // 뉴욕 현지 시각과 오프셋
5. DST(서머타임) 주의
DST 전환 구간에서는 시계 시간이 건너뛰거나 겹칩니다. DateTimeOffset에 24시간을 더하면 같은 instant 기준으로는 정확하지만, 특정 시간대의 시계 시간은 1시간 차이가 날 수 있습니다.
var nyZone = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
var preDst = new DateTimeOffset(2024, 3, 9, 12, 0, 0, TimeSpan.FromHours(-5)); // EST
var plus24 = preDst.AddHours(24); // instant 기준 +24h
Console.WriteLine(TimeZoneInfo.ConvertTime(preDst, nyZone)); // 2024-03-09 12:00 -05:00
Console.WriteLine(TimeZoneInfo.ConvertTime(plus24, nyZone)); // 2024-03-10 13:00 -04:00 (시계 +1h)
특정 시간대의 "달력 기준 동일 시각"을 원하면 단순 덧셈이 아닌 해당 시간대 로컬 시각을 구성한 뒤 TimeZoneInfo로 검증/변환해야 합니다.
6. 직렬화/전송: ISO 8601로 명확하게
외부 API/메시지에서는 오프셋 또는 Z(UTC)를 포함한 ISO 8601 문자열을 사용합니다. DateTime은 Kind가 Utc가 아니면 모호해집니다. DateTimeOffset은 기본적으로 안전합니다.
var dto = new DateTimeOffset(2024, 5, 1, 9, 0, 0, TimeSpan.FromHours(9));
var dtUtc = new DateTime(2024, 5, 1, 0, 0, 0, DateTimeKind.Utc);
Console.WriteLine(dto.ToString("o")); // 2024-05-01T09:00:00.0000000+09:00
Console.WriteLine(dtUtc.ToString("o")); // 2024-05-01T00:00:00.0000000Z
7. 데이터베이스 저장 가이드
원칙은 두 가지입니다. 첫째, 전송/저장은 항상 instant 기준(UTC)으로. 둘째, 스키마는 이를 보존할 수 있어야 합니다.
SQL Server: datetimeoffset(7)을 권장합니다. 또는 datetime2 UTC 고정 저장도 가능하나, 컬럼/코드에서 UTC 강제를 일관되게 유지해야 합니다.
PostgreSQL: timestamptz를 사용하면 UTC instant를 보존합니다. 드라이버/ORM 설정에 따라 DateTime(UTC) 또는 DateTimeOffset으로 매핑하되, 애플리케이션 내부에서는 UTC 규약을 지키십시오.
public class EventLog
{
public int Id { get; set; }
public DateTimeOffset OccurredAt { get; set; } // SQL Server: datetimeoffset, PG: timestamptz
}
// SQL Server일 때 예시(프로바이더별 설정 필요)
modelBuilder.Entity<EventLog>()
.Property(e => e.OccurredAt)
.HasColumnType("datetimeoffset");
8. 언제 무엇을 쓸까
외부 API/사용자 입력/로그: DateTimeOffset 권장입니다. 오프셋을 포함해 순간을 보존합니다.
내부 로직/DB 저장: UTC로 통일한다면 DateTime(Kind=Utc) 또는 오프셋 0의 DateTimeOffset을 사용합니다. 팀 규칙으로 한 가지를 정해 일관되게 유지하세요.
UI 표시: 저장된 instant를 TimeZoneInfo로 변환해 사용자의 시간대 시각으로 보여줍니다.
9. 빠른 치트시트
비즈니스 경계 밖으로 나간다 → DateTimeOffset 또는 DateTime(Utc)로.
오프셋 포함된 값 비교 → DateTimeOffset을 그대로 비교.
DB 스키마 → SQL Server: datetimeoffset, PostgreSQL: timestamptz.
Unspecified 방지 → 생성/입력 시 즉시 Utc/Offset 지정.
10. 성능·메모리 메모
DateTime과 DateTimeOffset 모두 값 형식이며 가볍습니다. DateTimeOffset은 내부에 UTC ticks와 오프셋을 보유하므로 약간의 연산 비용이 있지만 대부분의 애플리케이션에서 무시 가능합니다. 빈번한 대량 변환만 주의하면 됩니다.
핵심은 "값의 의미"를 잃지 않는 것입니다. DateTimeOffset 또는 UTC 규약을 일관되게 적용하면 시간대, DST, 직렬화/저장 문제를 대부분 예방할 수 있습니다.
'C#' 카테고리의 다른 글
| C# 메모리 매핑 파일(Memory-Mapped File) 사용하기 (0) | 2026.05.18 |
|---|---|
| C# TimeSpan과 Stopwatch로 성능 측정 및 시간 연산 (0) | 2026.05.16 |
| C# BitOperations로 비트 연산 최적화하기 (0) | 2026.05.15 |
| C# Local Function을 활용한 코드 가독성 개선 (0) | 2026.05.15 |
| C# System.Threading.Channels로 비동기 데이터 파이프라인 구성 (0) | 2026.05.14 |