블로그를 시작한 김에 대충 알아보고 넘길만한 것들을 깊게 알아보기 위한 Deep Dive into 시리즈를 연재하기로 결정했다. 첫 주제로 어떤 걸 고를지 고민하던 중 작년에 대표님이 'Date & Time 탐구'란 주제로 사내 발표를 해주셨던 내용이 생각났다. 이번 포스트에서는 그 내용에 개인적인 호기심을 조금 더 보충하여 정리하기로 했다.

시간은 어떻게 결정되는가?

컴퓨터 세계에 존재하는 것들은 거의 대부분 현실에 있는 것을 전산화 한 것이라 볼 수 있다. 마찬가지로 DateTime은 현실의 시간을 전산화 한 것이다. 그렇기 때문에 먼저 현실에서 시간이란 개념이 어떻게 표현되는지 알아야 할 필요가 있다. 시간은 다음 여섯 가지 조건에 의해 정해진다.

  • 물리량
    • 시간은 물리학 관점에서 봤을 때 시각과 시각 사이의 간격을 표현하는 단위를 뜻한다.
    • ex) 지금 이 순간(Instant)은 빅뱅(Epoch) 이후 시간이 얼마나 흘렀나?
  • 위치
    • 시간은 위치에 따라 다르게 표현될 수 있다.
    • ex) 경도 0도(UTC)가 정오일 때 동경 135도의 시각은? (경도상 위치)
    • ex) 런던이 정오일 때 프랑스의 시각은? (국가, 지역)
  • 천문 현상
    • 지구자전속도의 불규칙성, 지구의 자전주기와 공전주기 등 천문 현상으로 인해 시간은 보정이 필요하다.
    • ex) 윤초, 윤달, 윤년
  • 문화
    • 문화에 따라 시간 표현이 다를 수 있다.
    • ex) 태양력, 태음력, 이슬람력, 에티오피아력 등
  • 역사
    • 역사적 사건에 의해 시간은 다르게 표현될 수 있다.
    • ex) 1582년 10월 4일의 다음 날은?
  • 사회
    • 사회적 제도에 의해 시간은 변할 수 있다.
    • ex) 일광 시간 절약제(Summer Time)

보다시피 우리는 꽤 많은 조건을 고려하여 시간을 표시해야 한다. 물리량과 위치, 천문 현상은 그렇다 치더라도 역사적 사건, 사회적 제도같이 규칙이 존재하지 않는 경우도 존재하기에 단순히 수학식만으론 시간을 정확히 구할 수 없다. 게다가 문화에 따라 시간을 표현하는 방법이 다를 수 있다는 점도 골치아프다.

그렇기에 통일된 시간 표기법을 위한 협정 세계시가 탄생했다. UTC는 원자 시계와 윤초 보정을 기반으로 표준화한 시각으로 모든 시간대는 UTC+0을 기준으로 환산한다. 예를 들어, 대한민국의 시간은 UTC 시간에 +9를 더한 시간으로 영국 시간이 오전 1시라면 대한민국은 오전 10시1가 된다. UTC는 ISO 8601을 따라 다음과 같은 표기법을 따른다.

// UTC+0 기준 2021년 3월 20일 9시
2021-03-20T09:00:00.000Z

// UTC+9 (한국 시간) 기준 2021년 3월 20일 9시
2021-03-20T09:00:00.000+09:00

가운데 T는 Time을 뜻하고 시간 뒤 Z는 Zulu time2 을 뜻한다.

국가마다 시간대가 다르다

참고로 국가, 지역마다 경도상 시간과 국가 시간이 다를 수 있다. 아이슬란드의 경우 경도상 위치는 UTC-1이지만 UTC+0을 사용하고 있다.

컴퓨터가 시간을 표현하는 방법

컴퓨터는 시간을 표시하기 위해 하드웨어의 시스템 클럭을 이용한다. 주로 특정 시간(Epoch)를 기준으로 시스템 클럭의 틱을 세는 것으로 구현되고 이를 시스템 시간이라고 부른다. 그리고 시스템 시간을 값으로 표현한 것을 Timestamp라고 부른다. 주의할 점으로 Timestamp는 운영체제마다 기준 시간과 단위가 다를 수 있다. 대표적으로 유닉스에선 1970년 1월 1일 00:00:00이 기준 시간이며 초 단위로 시간이 증가하지만 윈도우즈는 1601년 1월 1일 00:00:00이 기준 시간이며 100 나노초 단위로 증가한다.

유닉스 계열 운영체제에서 시간을 표시하는 방법을 Unix time이라 부른다. 내용을 보면 몇 가지 의문이 생길 수 있다.

  • 1970년 이전 시간은 어떻게 표현할까?
  • 왜 하필 '1970년 1월 1일'일까?
  • 현재 시간을 어떻게 알지?
  • 시간대는 어떻게 고려할까?

충분히 의문을 품을 수 있는 내용들이다. 하나씩 파해쳐보자.

1970년 이전 시간은 어떻게 표현할까?

-1616254565은 1918년 10월 14일이다

이 의문은 굉장히 쉽게 풀린다. 답은 '음수'를 사용하는 것이다. EpochConverter라는 사이트에서 음수 값을 넣어 확인해보자.

왜 하필 '1970년 1월 1일'일까?

사실 필자는 이유를 알기위해 많은 리서치를 했지만 딱히 명쾌한 이유은 없었다. 단지 Stack Overflow 글 하나를 통해 유닉스를 개발한 데니스 리치의 인터뷰 기사를 하나 발견했는데 이를 통해 알아낸 이유는 다음과 같다.

"At the time we didn't have tapes and we had a couple of file-systems running and we kept changing the origin of time. So finally we said, 'Let's pick one thing that's not going to overflow for a while.' 1970 seemed to be as good as any." - Dennis Ritchie

핵심만 해석하면 "그냥 1970년이 좋아 보였다"로 요약할 수 있다. 즉, 정리하자면 1970년 1월 1일인 이유에 별다른 이유는 없다.

현재 시간을 어떻게 알지?

시스템 시간은 네트워크 타임 프로토콜(NTP)를 통해 현재 시간을 동기화할 수 있다. 당연하지만 네트워크 연결이 되어야하며 UDP 123번 포트를 통해 통신한다. NTP 서버는 여러 곳이 존재하며 이 서버가 기준 시간을 만들어낸다.

NTP 서버는 트리 구조로 상위 NTP 서버에서 하위 NTP 서버로 동기화된다. 여기서 계층을 Stratum으로 표현하는데 가장 최상위에 해당하는 Stratum 0은 Primary Reference Clock(PRC)이라고도 부르며 정교한 원자 시계를 사용하기에 매우 정밀한 시간을 만들 수 있다. 다음 계층인 Stratum 1은 물리적으로 Stratum 0과 동기화되며 기준 시간을 만드는 1차 타임 서버가 된다. 이후 Stratum 2 ~ 15에 해당하는 서버들은 상위 Stratum과 동기화되며 낮은 계층일수록 정밀도가 떨어진다.

NTP 클라이언트는 NTP 서버를 주기적으로 Polling 하는데 클라이언트는 서버에서 받아온 데이터를 보정해야 한다. 이를 해결하기 위한 은 다음과 같다.

// 왕복 지연(d)와 클럭 오프셋(c)을 계산한다.
d = (t2 - t3) - (t1 - t0);
c = ((t2 - t3) + (t1 - t0)) / 2;

// t0는 클라이언트가 서버로 요청 보낸 시점의 Timestamp다.
// t1은 서버가 클라이언트에게 요청 받은시점의 Timestamp다.
// t2는 서버가 클라이언트에게 응답하는 시점의 Timestamp다.
// t3는 클라이언트가 서버로부터 응답받은 시점의 Timestamp다.

위 식을 통해 클라이언트는 네트워크 지연 시간과 응답 받은 시점의 클럭 오프셋을 보정할 수 있다. 이때 통신 왕복 시간이 대칭적3일 때 정확한 동기화가 이루어진다. 만약 통신 왕복 시간이 대칭적이지 않은 경우엔 송수신 시간의 차 / 2만큼 Systematic bias가 발생한다고 한다. 혹시라도 해당 로직에 대한 구현이 궁금하다면 링크를 확인해보자.

참고로 우리가 주로 사용하는 리눅스에선 rdate를 통해 시간 동기화를 할 수 있다. 요즘은 거의 대부분 클라우드 시스템을 사용하기에 직접 설정할 일은 거의 없긴하다.

Time Zone

그렇다면 국가, 지역에 따른 시간 표시는 어떻게 할까? 앞서 설명했듯이 국가, 지역은 경도를 따르지 않고 시간대를 지정한 경우도 있다. 거기에 프랑스4나 러시아, 미국처럼 시간대가 여러 개거나 정책에 따라 바뀔 수 있기에 초기 설정만으론 문제가 생길 수 있다.

이를 해결하기 위해 멋진 사람들이 국가, 지역별 시간대 설정을 위한 tz database라는 데이터베이스를 구축했다. 우리는 tz database를 통해 세계 각국의 Time Zone 데이터를 받아와서 국가, 지역별 시간을 설정할 수 있다. Time Zone은 다음처럼 표기할 수 있다.

// 서울
Asia/Seoul

// 뉴욕
America/New_York

// 파리
Europe/Paris

보다시피 대륙/도시 형태를 따른다. 이 값을 Zone Id라고 부른다. 해당 국가, 지역의 정확한 시간을 표기하기 위해선 UTC 시간과 Zone Id를 저장해두고 클라이언트에서 계산하는 것이 가장 좋은 방법이다.

시간을 어떤 기준으로 사용해야 하는가?

국제화를 고려하지 않아도되는 서비스의 경우 모든 것을 한국 기준으로 시간을 기록한다면 크게 문제될 일은 없다. 하지만 우리가 만약 글로벌 서비스를 준비해야 한다면 가장 중요한 것은 시간이라고 할 수 있다.

"그냥 전부 Time Zone쓰면 되는 것 아닌가요?"

그렇지 않다. 우리는 서비스에서 사용되는 시간을 용도에 맞춰서 기록할 필요가 있다. 하나씩 예시를 살펴보자.

순수한 시간

뜬금없지만 필자의 생일은 1월 26일이다. 갑자기 생일 이야기를 한 것은 예시에 적합하기 때문이다. 보다시피 보통 생일은 시간대를 고려하지 않고 말한다. 만약 필자가 "내가 한국에서 1월 26일에 태어났지만 미국 시간으로 따지면 1월 25일에 태어났으니 미국 생일은 1월 25일이야~"라고 말하고 다닌다면 많은 사람들이 정말 이상한 사람이라고 생각할 것이다.

이처럼 시간대와 지역, 문화, 사회를 고려하지 않고 순수한 시간과 날짜를 사용해야하는 경우가 있다. 좀 더 예를 들어보자면 다음과 같은 사례에 쓰일 수 있다.

  • 생일
  • 기업 설립일
  • 기념일
  • 국경일

혹은 기록을 위해 시간을 사용하지 않는 경우에도 순수한 시간을 사용할 때가 있다. 보통 벤치마킹을 하기 위해 시간을 사용하거나 유니크한 값을 만들어내기 위해 사용하기도 한다. 혹은 랜덤 값 SEED 용도로 사용될 수도 있다.

UTC

역사, 사회, 문화에 대한 맥락 없이 '사건이 발생한 시각'만을 고려할 땐 UTC를 사용하여 시간을 기록하는 것이 좋다. 가장 대표적인 예시로 로깅을 들 수 있다. 로그는 분산되어 저장될 수 있기에 발생한 순서를 쉽게 알기 위해선 UTC처럼 기준이 되는 시간대를 정하는 것이 좋다. 만약 로그가 기록된 시간에 타임존이 적용되어 발생한 곳 마다 다르게 시간이 적용된다면 큰 혼란이 생길 수 있다. 마찬가지로 자주 사용하는 createdAt, updatedAt도 UTC로 기록하는 것이 좋다.

주식 차트는 대표적인 시계열 데이터다

또 다른 사용 예시로 시계열 데이터를 들 수 있다. 시계열 데이터의 경우 시간에 종속된 연속적인 데이터를 말하는데 UTC처럼 시간대가 정해진 상태로 기록된게 아니라면 언제 저장된 시간인지 알 수 없어 잘못된 분석으로 이어질 수 있다. 추가로 UTC는 다음과 같은 사례에 쓰일 수 있다.

  • 로그
  • 감사
  • 시계열 데이터
  • createdAt, updatedAt

Time Zone이 적용된 시간

반면 사용자가 이용한 시각을 정확히 알기위해 Time Zone을 사용해야 할 떄도 있다. 예를 들면, 사용자가 상품을 결제했을 때 UTC 시간만 기록했다면 사용자가 정확히 몇시 몇분 몇초에 주문했는지 알 수가 없다. 이때 정확한 시각을 알기 위해선 데이터베이스에 Zone Id도 함께 기록해야 한다. 또한, UI에 표시되는 시간을 사용자 기준으로 보여주기 위해서도 Time Zone은 필요하다.

예를 들어, 같은 페이스북 게시물을 보더라도 사는 나라가 다르다면 작성한 시간이 다를 수 있다. 필자가 3월 15일 오후 12시 31분에 작성한 게시물은 영국 사람이 보기엔 새벽 3시 31분에 작성한 것으로 보일 것이고 하와이에 사는 사람이 보기엔 3월 14일 오후 5시 31분에 작성한 것으로 보일 것이다.

그렇기 때문에 글로벌 서비스를 준비한다면 사용자 혹은 사용자가 포함되는 그룹 데이터에 Zone Id 정보를 추가해야 한다. 추가로 Time Zone이 적용된 시간은 다음과 같은 사례에 쓰일 수 있다.

  • 결제 시각
  • 푸시 알림 시각
  • UI 시각 표시
  • 캘린더

UI에 내려주는 JSON 데이터 예시는 다음과 같다.

{
  "userId": 1,
  "name": "이선협",
  "zoneId": "Asia/Seoul", // Time Zone을 위한 Zone Id
  "birthdate": "1994-01-26", // 순수한 시간
  "createdAt": "2021-03-20T04:59:25Z", // 기준 시간 UTC
  "updatedAt": "2021-03-20T05:12:38Z",
  "posts": [
    {
      "postId": 1,
      "publishedAt": "2021-03-20T06:00:00Z", // 기준 시간 오전 6시에 발행됨
      // publishedAt은 user의 zondId에 따라 다르게 보일 수 있다.
    }
  ]
}

마치며

사실 가볍게 작성하려한 글이지만 Deep Dive into라는 주제답게 파다 보니 생각보다 알아야할 내용들이 많았다. 그 모든 내용을 글에 담으려면 끝이 없을 것 같아 어느정도 자른 부분도 꽤 있다. 고작 DateTime을 이해하기 위해 이렇게까지 공부해야 할까라는 의문도 들었고 자료를 조사하고 글을 작성하는 시간을 합쳐 8시간 정도 걸려 할 일이 많음에도 꽤 시간을 많이 빼앗겼다.

하지만 인생이란 어떻게 될지 아무도 모르는 거다. 오늘 시간에 대해 공부한 덕분에 훗날 시스템에 치명적인 0.1%의 버그를 수정할 수 있다면 얼마나 멋진 일일까. 우리 모두 매 순간을 소중히 여기는 멋진 개발자가 되기로 하자.

Footnotes

  1. 참고로 한국 표준 시간은 KST라고 부른다

  2. UTC, GMT, Zulu time 모두 같은 시간대를 나타낸다

  3. 송수신 시간이 같을 때

  4. 프랑스는 무려 12개의 시간대를 가지고 있다