민간 시각
고친 사람 github-actions[bot]
민간 시각은 한 지역의 사람들이 일상의 약속에 쓰는 시각입니다. 기차 시간표가 이 시각으로 적힙니다. 지역마다 정한 규칙이 달라서 같은 순간에도 도시마다 다른 값을 가리킵니다.
쉽고 빠른 이해
민간 시각은 「오전 9시 출근」처럼 사람들이 약속을 잡을 때 쓰는 시각입니다. 서울역 전광판의 「09:00 출발」도 이 시각입니다.
세계가 함께 쓰는 기준 시각 하나로만 약속하면 하루의 모양이 지역마다 어긋납니다. 서울의 오전 9시가 기준 시각으로는 0시라서 「0시 출근」이 됩니다. 그래서 지역마다 해의 움직임에 맞춘 시각을 따로 정합니다.
어떻게 도나:
- 세계 공통 기준 시각을 받습니다
- 그 지역이 정한 시간 차이를 더합니다
- 여름에 시계를 앞당기는 지역이면 그 기간에 한 시간을 더 얹습니다
대가는 두 가지입니다. 하나는 나라가 이 규칙을 바꾸기도 한다는 것입니다. 다른 하나는 규칙이 그대로여도 시계를 옮기는 날에는 없는 시각과 두 번 오는 시각이 생긴다는 것입니다. 이 둘 때문에 기록을 이 시각으로만 남기면 앞뒤가 꼬일 수 있습니다.
상세
이 절은 먼저 민간 시각이 무엇으로 정해지는지를 서울과 뉴욕의 시계를 가지고 봅니다. 그다음 프로그램이 이 시각을 어떤 값으로 담는지, 그 값이 어디서 어긋나는지 봅니다. 끝으로 백엔드에서 시각을 저장할 때 무엇을 고르는지 봅니다.
기차표에 적힌 「서울 09:00 출발」을 떠올리면 됩니다. 승객도 기관사도 역무원도 이 숫자 하나로 움직입니다. 누구도 이 숫자를 원자시계나 해의 위치로 다시 계산하지 않습니다.
민간 시각(civil time)은 한 지역이 법이나 관습으로 정해 사회생활에 공식으로 쓰는 시각입니다. 상용시라고도 부릅니다. 「민간」은 천문 관측 같은 특수한 용도가 아니라 일상 용도를 가리킵니다.
한 지역의 민간 시각을 흔히 지역 시각이나 현지 시각이라고 부릅니다. 해외 경기 중계표에 적힌 「현지 시각 20시」가 그 도시의 민간 시각입니다.
민간 시각은 연·월·일·시·분·초 여섯 칸으로 적습니다. 달력의 날짜와 벽시계의 시각을 한 번에 읽은 값이라고 보면 됩니다.
날짜 칸은 대부분의 지역에서 그레고리력을 따릅니다. 그레고리력은 1년을 365일로 두고 대개 4년마다 하루를 더하는 달력입니다. 다만 100으로 나누어떨어지는 해는 이 하루를 뺍니다. 400으로 나누어떨어지는 해는 다시 넣습니다.
민간 시각을 이루는 세 겹
오늘날 대부분 지역의 민간 시각은 UTC(Coordinated Universal Time, 협정 세계시) 위에 세웁니다. UTC 는 원자시계가 세는 초를 바탕으로 세계가 함께 쓰는 기준 시각입니다. 어느 나라에 있든 지금 이 순간의 UTC 값은 하나입니다.
UTC 는 흐르는 시간의 모든 순간에 값을 하나씩 매깁니다. 「7월 1일 00:00 UTC」는 세계 어디서나 같은 한 순간을 가리킵니다. 이처럼 순간마다 번호를 매기는 규칙이 타임스케일입니다.
그 위에 지역마다 UTC 에 더할 시간 차이를 정합니다. 이 차이를 UTC 오프셋이라고 부릅니다. 한국은 UTC 보다 9시간 앞섭니다. 그래서 한국의 UTC 오프셋은 +09:00 입니다.
UTC 에 그 오프셋을 더해 얻는 시각이 그 지역의 표준시입니다. 한국 표준시는 UTC 0시일 때 오전 9시입니다. 서머타임이 없는 철에는 표준시가 곧 그 지역의 시계입니다.
일부 지역은 여름 동안 시계를 한 시간 앞당깁니다. 이 제도가 서머타임입니다. 해가 일찍 뜨는 철에 밝은 시간을 저녁 쪽으로 옮기려는 것입니다. 한국은 지금 서머타임을 쓰지 않습니다.
아래 그림은 UTC 로 7월 1일 0시인 한 순간이 두 도시에서 어떤 민간 시각이 되는지 보입니다. 뉴욕의 표준시는 UTC 보다 5시간 늦습니다. 여름에는 서머타임으로 한 시간을 앞당깁니다.
flowchart TD
U["UTC · 7월 1일 00:00"]
U -->|"표준시 +9시간"| S["서울 민간 시각 · 7월 1일 09:00"]
U -->|"표준시 -5시간"| N1["뉴욕 표준시 · 6월 30일 19:00"]
N1 -->|"서머타임 +1시간"| N2["뉴욕 민간 시각 · 6월 30일 20:00"]
서울은 표준시가 곧 민간 시각입니다. 뉴욕은 표준시에 서머타임이 한 겹 더 얹혀서 저녁 8시가 됩니다. 두 도시는 같은 순간에 날짜부터 다릅니다.
규칙을 정하는 주체
민간 시각의 규칙은 나라나 지역이 법이나 행정 결정으로 정합니다. 기술 표준이 아니라 정치가 정하는 값이라는 뜻입니다. 그래서 규칙이 바뀝니다. 서머타임을 새로 들이거나 없애기도 합니다. UTC 오프셋 자체를 옮기기도 합니다.
바뀔 때 예고 기간이 짧을 수도 있습니다. 그래서 앞날의 어느 순간이 그 지역에서 몇 시가 될지는 지금 규칙으로 계산한 값일 뿐입니다. 그날까지 규칙이 바뀌면 답도 바뀝니다.
세계 각 지역의 규칙을 과거부터 모아 둔 공용 자료가 tz database 입니다. 운영체제와 프로그래밍 언어의 날짜 라이브러리가 이 자료를 가져다 씁니다. 덕분에 프로그램마다 나라별 법을 따로 챙기지 않아도 됩니다.
tz database 는 지역마다 Asia/Seoul 이나 America/New_York 같은 이름을 붙입니다. 이름 하나가 그 지역이 지금까지 써 온 오프셋과 서머타임 규칙 전부를 가리킵니다. 이 이름을 타임존이라고 부릅니다.
규칙이 바뀌면 이 자료도 새 판이 나옵니다. 서버가 낡은 판을 들고 있으면 바뀐 지역의 민간 시각을 틀리게 계산합니다.
순간과 민간 시각
프로그램이 다루는 시각 값은 두 종류로 갈립니다. 하나는 순간입니다. 순간은 타임스케일 위의 한 점입니다. 세계 어디서 읽어도 같은 값입니다. 1970-01-01 00:00 UTC 부터 흐른 초를 세는 유닉스 시간이 대표적인 순간 값입니다.
다른 하나가 민간 시각입니다. 민간 시각은 여섯 칸짜리 값입니다. 어느 지역의 것인지 모르면 어느 순간인지도 모릅니다.
「7월 1일 09:00」만 적힌 값을 예로 들어 봅니다. 서울의 것이면 UTC 0시입니다. 뉴욕의 것이면 UTC 13시입니다.
둘을 잇는 것이 타임존입니다. 순간에 타임존을 주면 그 지역의 민간 시각이 나옵니다. 민간 시각에 타임존을 주면 순간이 나옵니다. 그런데 두 방향의 답이 똑같이 깔끔하지는 않습니다.
| 방향 | 답의 개수 |
|---|---|
| 순간 + 타임존 → 민간 시각 | 언제나 하나 |
| 민간 시각 + 타임존 → 순간 | 대개 하나 · 시계를 옮기는 날에는 없거나 둘 |
순간에서 민간 시각으로 가는 쪽은 그 순간의 규칙을 찾아 더하기만 하면 됩니다. 거꾸로 가는 쪽은 아래 소절에서 보듯 답이 사라지거나 둘로 갈리는 때가 있습니다.
없는 시각과 두 번 오는 시각
서머타임이 시작하는 날 뉴욕은 새벽 2시에 시계를 3시로 넘깁니다. 그날 2시 30분이라는 민간 시각은 오지 않습니다. 이런 구간을 갭(gap)이라고 부릅니다.
서머타임이 끝나는 날에는 새벽 2시에 시계를 1시로 되돌립니다. 그래서 1시 30분이 두 번 옵니다. 앞의 것은 서머타임 오프셋이고 뒤의 것은 표준시 오프셋입니다. 이런 구간을 오버랩(overlap)이라고 부릅니다.
백엔드에서는 예약 작업이 여기에 걸립니다. 「매일 지역 시각 2시 30분」에 도는 cron 작업은 봄에 하루를 건너뛰거나 엉뚱한 시각에 돌 수 있습니다. 「1시 30분」에 도는 작업은 가을에 두 번 돌 수 있습니다.
프로그램에서 민간 시각을 담는 값
날짜 라이브러리는 대개 순간을 담는 타입과 민간 시각을 담는 타입을 따로 둡니다. 자바에서는 Instant 가 순간이고 LocalDateTime 이 타임존 없는 민간 시각입니다. 아래 코드는 같은 순간 하나를 두 도시의 민간 시각으로 바꿉니다.
var t = Instant.parse("2026-07-01T00:00:00Z");
var seoul = t.atZone(ZoneId.of("Asia/Seoul"));
var ny = t.atZone(ZoneId.of("America/New_York"));
seoul.toLocalDateTime(); // 2026-07-01T09:00
ny.toLocalDateTime(); // 2026-06-30T20:00
순간 t 하나에서 민간 시각 둘이 나왔습니다. 앞의 그림과 같은 값입니다. 이 방향은 언제나 답이 하나라서 라이브러리가 고민할 일이 없습니다.
거꾸로 민간 시각에서 순간으로 갈 때는 라이브러리가 갭과 오버랩을 어떻게든 처리해야 합니다. 아래는 뉴욕의 서머타임이 시작하는 날 오지 않는 2시 30분을 넣어 본 코드입니다.
var ny = ZoneId.of("America/New_York");
var at = LocalDateTime.of(2026, 3, 8, 2, 30);
at.atZone(ny).toLocalTime(); // 03:30
자바는 오류를 내지 않고 갭의 길이만큼 뒤로 밀어 3시 30분을 돌려줍니다. 오버랩에서는 기본으로 앞의 것을 고릅니다. 라이브러리마다 이 선택이 다르므로 쓰는 쪽에서 확인해야 합니다.
데이터베이스도 둘을 가릅니다. SQL(Structured Query Language, 구조화 질의 언어)의 TIMESTAMP WITHOUT TIME ZONE 은 민간 시각을 담습니다. TIMESTAMP WITH TIME ZONE 은 순간을 담으려고 만든 타입입니다. 다만 들어온 오프셋을 값과 함께 남기는지, UTC 로 바꿔 순간만 남기는지는 데이터베이스마다 다릅니다.
기록 종류별 저장 값
무엇을 저장할지는 그 값이 무엇을 기록하는지에 달렸습니다. 이미 일어난 일인지, 앞으로 사람이 지킬 약속인지가 가르는 기준입니다. 생일처럼 시각 없이 하루만 가리키는 값은 셋째 경우로 따로 둡니다.
| 저장하는 것 | 담는 값 | 까닭 |
|---|---|---|
| 로그 · 결제 · 가입 시각 | UTC 순간 | 지역이 달라도 바로 견주고 정렬된다 |
| 앞날의 회의 · 영업 시작 | 민간 시각 + 타임존 이름 | 그사이 규칙이 바뀌어도 약속한 시각이 지켜진다 |
| 생일 · 공휴일 | 날짜만 | 어느 순간이 아니라 그 지역의 하루를 가리킨다 |
이미 일어난 일은 순간으로 남깁니다. 서버가 어느 지역에 있든 값이 같습니다. 서머타임 전환일에도 앞뒤가 꼬이지 않습니다. 사람에게 보여 줄 때만 보는 사람의 타임존으로 민간 시각을 만듭니다.
앞날의 약속은 반대입니다. 「서울에서 내년 3월 2일 오전 9시 회의」를 지금 규칙으로 순간으로 바꿔 두면, 그사이 규칙이 바뀔 때 저장된 순간이 더는 오전 9시가 아닙니다. 사람이 지키는 것은 순간이 아니라 민간 시각이기 때문입니다.
관련 항목
민간 시각이 바탕으로 삼는 기준 시각
UTC · 타임스케일 · TAI · 윤초 · 그리니치 표준시 · 원자시계
민간 시각을 정하는 지역 규칙
표준시 · 한국 표준시 · UTC 오프셋 · 서머타임 · 타임존 · tz database
민간 시각과 겹쳐 쓰이는 이름
지역 시각 · 현지 시각 · 벽시계
민간 시각과 맞세워지는 순간 값
유닉스 시간 · 에포크 · 타임스탬프 · 시스템 시계 · 단조 시계
민간 시각을 적고 담는 형식과 타입
그레고리력 · ISO 8601 · RFC 3339 · LocalDateTime · Instant · TIMESTAMP WITH TIME ZONE
민간 시각에서 터지는 버그
서머타임 갭 · 서머타임 오버랩 · cron · 시계 동기화
민간 시각이 속하는 상위 주제
시간과 시계 · 날짜와 시간 · 국제화 · 지역화
다른 이름: civil time · 상용시 · 민간시