time_t
고친 사람 github-actions[bot]
time_t 는 C 프로그램이 시각을 수 하나로 담아 주고받게 해 주는 타입입니다. 그 수는 대개 1970년 첫날 0시부터 흐른 초의 개수입니다. 운영체제가 지금 시각을 알려 줄 때도, 파일을 고친 시각을 적어 둘 때도 이 타입에 담아 건넵니다.
쉽고 빠른 이해
무슨 일을 하는 물건인가 — 시각을 날짜가 아니라 수 하나로 들고 다니게 해 줍니다. 1970년 첫날 0시가 0이고, 그 하루 뒤가 86400입니다.
왜 이렇게 하나 — 연·월·일·시·분·초를 따로 들고 있으면 두 시각의 차이를 구할 때 달마다 다른 날수와 윤년을 따라가야 합니다. 수 하나면 뺄셈 한 번으로 끝납니다. 어느 나라 시간대인지도 따질 필요가 없습니다.
어떻게 도나
- 운영체제에 지금 시각을 물으면 이 수 하나가 돌아옵니다
- 저장하고 견주고 빼는 일은 전부 이 수로 합니다
- 사람에게 보여 줄 때만 날짜와 시각 문자열로 바꿉니다
대가 — 수를 담는 칸의 폭이 담을 수 있는 마지막 시각을 정합니다. 32비트 칸은 2038년 1월에 가득 찹니다. 또 운영체제 시계를 누가 고치면 이 수도 같이 뜁니다. 그래서 걸린 시간을 재는 데는 맞지 않습니다.
상세
마을 광장 벽시계는 날짜 대신 마을이 생긴 날부터 흐른 초를 숫자 하나로 보여 줍니다. 마을 사람들은 약속 시각을 이 숫자로 말합니다. 두 약속이 얼마나 떨어져 있는지는 두 숫자를 빼면 나옵니다. 시계가 어긋나면 광장 관리인이 가끔 숫자를 고쳐 맞춥니다.
time_t 가 담는 값이 이 벽시계 숫자입니다. 이 절은 값 표와 짧은 C 코드로 그 숫자가 무엇을 세는지부터 봅니다. 이어서 C 언어가 어디까지 정해 두었는지, 값을 담는 칸의 폭이 왜 문제가 되는지, 사람이 읽는 날짜로 어떻게 바꾸는지를 차례로 봅니다. 끝으로 지금 시각을 얻고 걸린 시간을 재는 법과 다른 언어의 같은 값까지 봅니다.
에포크부터 센 초의 개수
C 언어의 시각 함수들은 시각을 time_t 값 하나로 주고받습니다. 지금 시각을 물어도, 파일을 고친 시각을 읽어도 이 타입의 값이 돌아옵니다.
유닉스 계열 운영체제들은 POSIX(Portable Operating System Interface, 이식 가능한 운영체제 인터페이스)라는 공통 규격을 함께 지킵니다. 리눅스와 macOS 가 그렇습니다. 이 규격을 따르는 시스템에서 time_t 는 정수이고, 정해 둔 한 시점부터 흐른 초의 개수를 담습니다.
그 출발 시점을 에포크라고 부릅니다. time_t 의 에포크는 1970년 1월 1일 0시 0분 0초입니다. 이 에포크부터 센 초의 개수를 유닉스 시간이라고도 부릅니다.
이 0시는 서울의 0시가 아니라 UTC(Coordinated Universal Time, 협정 세계시)의 0시입니다. 협정 세계시는 세계가 시각을 맞출 때 기준으로 삼는 시각입니다. time_t 값 하나는 지구 어디서 읽어도 같은 순간을 가리킵니다.
시각을 이렇게 수 하나로 두는 까닭은 계산에 있습니다. 연·월·일을 따로 들고 있으면 두 시각의 차이를 구할 때 달마다 다른 날수와 윤년을 따라가야 합니다. 수 하나면 뺄셈 한 번으로 끝납니다.
값 몇 개가 어느 순간을 가리키는지 보면 감이 잡힙니다.
time_t 값 |
가리키는 순간 · 협정 세계시 |
|---|---|
| 0 | 1970-01-01 00:00:00 |
| 86400 | 1970-01-02 00:00:00 |
| 1000000000 | 2001-09-09 01:46:40 |
하루는 86400초라서 값이 86400 오를 때마다 날짜가 하루 넘어갑니다. 표의 첫 두 줄이 그 하루입니다.
time_t 를 부호 있는 정수로 두는 시스템이 많아서 음수도 담깁니다. 음수를 에포크 이전으로 읽는 시스템이 많습니다. POSIX 를 따르는 시스템에서도 음수가 가리키는 순간은 정해져 있지 않습니다.
지구 자전에 시계를 맞추려고 가끔 1초를 끼워 넣습니다. 이 1초를 윤초라고 합니다. time_t 는 윤초를 세지 않고 하루를 언제나 86400초로 칩니다. 그래서 사이에 윤초가 낀 두 값을 빼면 실제로 흐른 초보다 그만큼 작게 나옵니다.
이름 끝의 _t 는 type 의 머리글자입니다. size_t 나 pid_t 처럼 C 와 POSIX 가 정해 둔 타입 이름에 붙는 꼬리표입니다.
C 언어와 POSIX 가 정한 범위
C 언어만 놓고 보면 time_t 는 훨씬 느슨합니다. 시각을 담을 수 있는 산술 타입이라는 것까지만 정해져 있습니다. 산술 타입은 정수나 실수처럼 더하고 뺄 수 있는 수 타입입니다. 초 단위와 1970년 출발점은 POSIX 를 따르는 시스템에서 더해지는 약속입니다.
| C 언어만 놓고 볼 때 | POSIX 를 따르는 시스템 | |
|---|---|---|
| 수의 종류 | 정수든 실수든 된다 | 정수 |
| 단위 | 정하지 않는다 | 초 |
| 출발점 | 정하지 않는다 | 1970-01-01 00:00:00 협정 세계시 |
C 언어 규격에만 기대는 코드, 곧 POSIX 를 전제하지 않는 코드는 두 time_t 값을 바로 빼면 안 됩니다. 단위가 초라는 보장이 없어서입니다. 그래서 C 표준 라이브러리에는 두 시각의 차이를 구하는 함수 difftime 이 따로 있습니다. 단위가 무엇이든 차이를 초로 바꿔 실수로 돌려줍니다.
에포크와 그 하루 뒤를 넣어 보면 이렇습니다.
time_t a = 0, b = 86400;
difftime(b, a); // 86400.0
두 값의 차이인 86400초가 실수로 돌아왔습니다. POSIX 시스템에서는 b - a 로 빼도 같은 값이 나옵니다. difftime 은 POSIX 를 따르지 않는 시스템에서도 맞는 값을 줍니다.
칸의 폭과 담을 수 있는 마지막 시각
time_t 의 폭도 시스템마다 다릅니다. 옛 32비트 시스템은 대개 32비트 부호 있는 정수를 썼습니다. 요즘 64비트 시스템은 대개 64비트를 씁니다.
최신 POSIX 를 따르는 시스템에서 time_t 는 64비트 이상이어야 합니다. 32비트를 기본으로 두는 시스템도 아직 남아 있습니다.
칸의 폭은 담을 수 있는 가장 큰 수를 정합니다. 그 수가 곧 담을 수 있는 마지막 시각입니다.
| 폭 | 가장 큰 값 | 그 값이 가리키는 순간 |
|---|---|---|
| 32비트 부호 있는 정수 | 2147483647 | 2038-01-19 03:14:07 협정 세계시 |
| 64비트 부호 있는 정수 | 약 9.2 × 10¹⁸ | 1970년에서 약 2922억 년 뒤 |
32비트 칸은 2038년 1월 19일에 가득 찹니다. 그다음 초는 이 칸에 담을 수 없습니다. 이 일을 2038 문제라고 부릅니다. 64비트 칸은 우주의 나이보다 긴 시간을 담으므로 이 걱정에서 벗어납니다.
사람이 읽는 날짜로 바꾸는 방법
time_t 는 계산에는 편하지만 사람이 읽기는 어렵습니다. 1000000000 을 보고 날짜를 떠올릴 사람은 없습니다. C 표준 라이브러리는 이 수를 날짜와 시각으로 풀어 주는 함수를 함께 둡니다.
풀어 놓은 날짜는 struct tm 이라는 구조체에 담깁니다. 연·월·일·시·분·초를 필드마다 따로 담는 구조체입니다. 사람이 읽는 형태와 time_t 사이에 서는 중간 단계입니다.
time_t 와 struct tm, 문자열 사이를 오가는 함수는 넷입니다. 아래 그림이 넷의 방향입니다.
flowchart TD
T["time_t · 초의 개수"]
M["struct tm · 연월일시분초"]
S["문자열 · 2001-09-09"]
T -->|"gmtime · localtime"| M
M -->|mktime| T
M -->|strftime| S
풀 때는 둘 가운데 하나를 고릅니다. gmtime 은 time_t 를 협정 세계시로 풀어 struct tm 을 만듭니다. localtime 은 그 기계에 설정된 시간대로 풉니다.
나머지 둘은 되돌리고 찍습니다. mktime 은 거꾸로 struct tm 을 time_t 로 되돌립니다. strftime 은 struct tm 을 문자열로 찍습니다.
앞의 표에 있던 1000000000 을 협정 세계시 날짜로 풀어 봅니다. %F 는 연-월-일을, %T 는 시:분:초를 뜻하는 서식입니다.
char s[20];
time_t t = 1000000000;
struct tm *u = gmtime(&t);
strftime(s, 20, "%F", u); // "2001-09-09"
strftime(s, 20, "%T", u); // "01:46:40"
같은 값을 서울 시간대로 설정된 기계에서 localtime 으로 풀면 시각이 10:46:40 이 됩니다. 서울은 협정 세계시보다 9시간 빠르기 때문입니다. time_t 값에는 시간대가 없습니다. 푸는 함수가 시간대를 입힙니다.
지금 시각을 얻는 방법과 걸린 시간을 재는 방법
지금 시각은 time(NULL) 을 부르면 time_t 로 돌아옵니다. 실패하면 (time_t)-1 이 돌아옵니다. 이 -1 은 오류 표시입니다. 에포크 1초 전으로 읽힐 수도 있는 값이라 헷갈리기 쉽습니다.
운영체제가 들고 있는 시계 가운데 사람 세계의 시각을 알려 주는 것을 벽시계라고 부릅니다. time_t 로 받는 지금 시각은 이 벽시계를 읽은 값입니다. 벽시계는 관리자가 고치거나 네트워크로 다른 서버와 시각을 맞출 때 앞뒤로 뜁니다.
그래서 두 time_t 값의 차이는 걸린 시간과 다를 수 있습니다. 시계가 중간에 뒤로 가면 차이가 음수로 나오기도 합니다. 아래 그림은 2초가 지나는 사이에 시계가 5초 늦춰진 경우입니다.
sequenceDiagram
participant 앱
participant 벽시계
participant 맞추는쪽 as 시각을 맞추는 쪽
앱->>벽시계: 지금 시각을 읽는다
벽시계-->>앱: 1000000000
맞추는쪽->>벽시계: 5초 늦춘다
앱->>벽시계: 2초 뒤 다시 읽는다
벽시계-->>앱: 999999997
Note over 앱: 2초가 지났는데 차이는 -3
걸린 시간을 잴 때는 앞으로만 가는 단조 시계를 씁니다. 단조 시계는 사람 세계의 시각을 알려 주지 않습니다. 대신 누가 벽시계를 고쳐도 뒤로 가지 않습니다.
C 에서는 두 시계를 모두 clock_gettime 으로 읽습니다. 부를 때 어느 시계를 읽을지 고릅니다. 벽시계는 CLOCK_REALTIME, 단조 시계는 CLOCK_MONOTONIC 이라는 이름으로 고릅니다.
결과는 struct timespec 이라는 구조체에 담깁니다. 초는 time_t 타입 필드에, 1초 안쪽은 나노초 필드에 따로 들어갑니다. 초보다 잘게 재야 할 때도 이 함수를 씁니다.
다른 언어에서 만나는 같은 값
백엔드 코드에서도 같은 값을 자주 만납니다. 언어마다 에포크는 1970년 첫날 0시로 같습니다. 단위와 담는 타입이 다릅니다.
| 어디서 | 돌려주는 값 | 단위 |
|---|---|---|
C time(NULL) |
time_t 정수 |
초 |
| [[Java | 자바]] System.currentTimeMillis() |
long 정수 |
자바 Instant.getEpochSecond() |
long 정수 |
초 |
| [[Python | 파이썬]] time.time() |
float 실수 |
| [[JavaScript | 자바스크립트]] Date.now() |
number |
단위가 갈리는 것이 흔한 함정입니다. 초 단위 값 1000000000 을 밀리초로 읽으면 2001년이 아니라 1970-01-12 13:46:40 이 됩니다. 반대로 밀리초 값을 초로 읽으면 수만 년 뒤가 됩니다. 두 단위는 1000배 차이라서 틀리면 날짜가 엉뚱한 세기로 튑니다.
관련 항목
time_t 가 세는 기준과 그 값의 이름
에포크 · 유닉스 시간 · 타임스탬프 · UTC · 윤초 · 윤년
time_t 값을 만들고 푸는 C 함수
time(2) · clock_gettime · gmtime · localtime · mktime · strftime · difftime
time_t 와 함께 시각을 담는 C 타입
struct tm · struct timespec · struct timeval · clock_t · time64_t
time_t 를 정해 둔 언어와 규격
C · C 표준 라이브러리 · POSIX · time.h · 유닉스 · 산술 타입
이름 끝에 _t 를 단 이웃 C 타입
size_t · ssize_t · pid_t · off_t
time_t 의 폭이 좁아 터지는 문제
2038 문제 · 정수 오버플로 · 부호 있는 정수 · Y2K 문제 · ABI
time_t 가 읽어 오는 시계와 걸린 시간을 재는 시계
벽시계 · 단조 시계 · CLOCK_REALTIME · CLOCK_MONOTONIC · NTP · 시간과 시계
time_t 값을 사람이 읽는 표기로 옮기는 규격
ISO 8601 · RFC 3339 · 시간대 · 그레고리력 · 서머타임
다른 언어에서 같은 에포크 값을 돌려주는 함수
System.currentTimeMillis · java.time.Instant · Date.now · time.time
다른 이름: time_t 타입