2024 경북대학교 대동제 '하푸르나' 안내 웹사이트를 개발하면서, 출시 하루 전이었다. 메인화면에서 축제가 종료될 때 까지 얼마나 남았는지 알려주는 기능이 있었기에 현재 서버 시간을 응답하는 api가 필요했다.
그래서 아래와 같이 api 를 작성하였다.
Instant?
java 8 버전부터 java.time 패키지가 등장하면서, java 애플리케이션에서 시간을 다루기 좀 더 용이해졌고
Instant 는 기존에 사용하던 Date 객체를 대체하기 위해서 만들어졌다. Date 객체는 UNIX TimeStamp를 대체하기 위해 만들어졌는데 타임스탬프(TimeStamp)는 현재 시각을 나타내는 문자열로써 사람이 이해하기 보다는 기계가 이해하기 쉽다. 이를 사람이 보기 쉽게 바꾸려면 format 객체를 사용해야한다. 어쨌든 Instant 는 현재 시각을 나타내는 기능의 객체이며, UTC time 을 사용한다. 우리가 일반적으로 사용하는 단위는 영국 그리니치 천문대를 기준으로 한 GMT 시간대인데, 소프트웨어 상에서는 자전주기 변화 등의 이슈로 UTC가 보다 정확하기 때문에 UTC를 사용한다고 한다.
UNIX TimeStamp 를 바로 갖다쓰면 되는거 아닌가? 싶은데, 이는 2038년 1월 19일 화요일 까지 표현가능하기 때문에 Instant 가 이를 해결해줄 수 있다고 한다.
( UNIX TimeStamp 는 Unix 운영체제에서 UTC 기준점인 1970년 1월 1일 자정 (= EPOCH 타임) 으로부터 경과한 시간을 저장한 값 )
JVM 을 사용해서 System 즉, OS 상에서 UTC time 의 기준점으로 부터 얼마나 시간이 경과했는지를 밀리초 단위로 가져오는 System 패키지의 메서드인 currentTimeMillis 메서드를 사용한다. 이 메서드는 단순히 경과 시간을 단순한 숫자의 배열로 나타내기 때문에 Instant 는 이를 나노초 단위로 변경하여 추가로 저장하고 아래와 같은 format 으로 변경해 반환한다.
2024-07-05T09:53:30.234521Z ==> 2024년 7월 5일 18시 53분 30초 (대한민국 기준)
처음 시간을 나타내기 위해 Instant 를 사용한 이유는 Long 형태를 사용하기 때문에 연산이 빠르고 시간단위가 기존의 밀리초 단위보다 더 정밀한 단위인 나노초 단위를 사용하기 때문에 보다 정확한 시점을 알 수 있다는 장점이 있다고 생각했기 때문이다.
그런데 문제가 생겼다. 시간이 맞지 않다는 것이었다. 앞서 설명했지만 Instant 의 now는 우리나라의 시차보다 9시간 전을 반환하기 때문에 이를 보정할 필요가 있었다. 그래서 구글링을 해보니 시차를 해결하기 위해서는 Instant 보다 LocalDateTime 을 사용하라는 자료가 많아서 LocalDateTime 을 사용하기로 하였다.
그리고 생각해보면 이번 프로젝트에서 시간을 사용하는 경우는 딱 2가지
1. 단순히 현재 서버 시간을 반환
2. Data가 생성되거나 수정되는 시간을 timestamp 형식으로 저장
결국 비교, 등의 연산이 필요가 없다는 점과, 현재 local 시스템의 시간과 동일한 시간을 사용하는 LocalDateTime을 사용하기위해 코드를 뜯어봤다.
LocalDateTime?
java 8 버전부터 java.time 패키지에 속한 객체이다. 날짜와 시간을 모두 포함하고 있으며 현재 시스템의 system clock에서 시간을 참조하여 반환한다.
코드를 뜯어보니 결국에 LocalDateTime 또한 Instant 객체를 생성하고 거기에다가 TimeZone 이라는 객체를 추가적으로 담아 반환하는 것이었다. TimeZone이 나타내는 것은 우리가 시간을 다루기위해서 java.time 관련 객체를 사용했을 때 반환하는 시간은 모두 특정 시점을 기준으로 얼마나 많은 시간이 지났는 지를 다양한 형태, 단위 등으로 반환하는 것인데 바로 그 특정 시점을 의미한다.
Instant 의 경우 별다른 TimeZone 설정은 없지만 기본적으로 에포크타임(=EPOC Time) 이 기준 시점인 것이고, TimeZone 을 통해 그 기준시점을 정해주면 그로부터 에포크타임 간의 시차를 결과값에 더해주는 것이다.
우리가 LocalDateTime을 사용하면서 별도로 TimeZone을 설정해주지 않으면, 아래와 같이 기본 TimeZone을 정한다.
OS로 부터 user.timezone 속성을 불러와서 그 정보를 반환할 시간 값에 보정하여 반환하는 것이다. 이렇게 하면 이제 정상적으로 내 PC (Window 11) 환경의 시간과 일치하는 시간을 반환한다.
그런데 또 문제가 생겼다. EC2 인스턴스에 배포를 한 후에 다시 test를 진행해보니 여전히 시간이 9시간 전으로 반환되는 것이였다. 아뿔싸! EC2 인스턴스의 시차가 어떤지를 생각하지 못한 것이다. 이 문제는 아직 정확히 모르겠다. EC2를 생성할 때 서울 리젼으로 생성 했을텐데? 그럼 Linux Ubuntu 환경에서 자동으로 시차가 바뀌는 거 아닌가? 그건 아닌 것 같다. 그저 이미지를 사용해서 인스턴스를 생성하는 것 뿐인 것 같다. 이 문제는 나중에 한번 더 조사해봐야겠다.
하여튼 그래서 직접 TimeZone을 설정해줄 필요가 있었다.
다양한 방법이 있을 것이다. 그저 하드코딩으로 시차를 더해주는 방식이나, ZonedDateTime 이라는 객체는 생성할 때 TimeZone에 대한 정보를 넘겨주면 해당 TimeZone에 맞는 시간을 보여준다고 한다. LocalDateTime은 안되는건가?
그래서 코드를 뜯어봤더니
아직은 둘이 뭐가 다른건지 잘 모르겠다... 세부적으로 다른 게 분명히 있겠지만 내가 찾아볼 수 있는 수준에서는 다름을 느끼지 못해서 LocalDateTime을 계속 사용하기로 하였고 그럼 그래서 시차를 어떤 식으로 설정 해줘야될까?
now 메서드를 실행할 때마다 Service 레벨에서 ZoneId 를 직접 넘겨줄 수도 있겠지만 우리 서비스는 대한민국에서만 사용될 서비스이기 때문에 시차 설정은 서버가 실행되는 순간 그때 한번이면 된다.
그래서 서버가 실행되는 그 순간 한번 TimeZone 의 setDefault 메서드를 활용하여 TimeZone을 설정해주면 된다.
그래서 2가지 방법을 생각해봤다.
1. SPA(스프링부트애플리케이션) 객체의 main 메서드 내부에서 run 메서드 전에 실행
2. @PostConstruct 를 통해 SPA 객체 빈이 완성되는 순간 실행
@PostConstruct 에 대해 잠깐 설명하면 스프링 컨테이너가 해당 객체를 완벽히 생성(의존 주입 완료) 하자마자 바로 자동으로 해당 메서드를 실행시켜줄 수 있게 하는 어노테이션이다.
위 2방법 중에서 고민하다가 큰 차이는 없겠지만 SPA 객체가 생성될 때는 별다른 의존주입이 필요가 없고, 서버가 동작하는 순간이 SpringApplicaiton.run 메서드의 실행이지 해당 객체가 생성되어 스프링 컨테이너에 의해 관리되는 그 시점이 아니기 때문에 그냥 1번 방법을 선택하기로 했다.
의미있는 고민은 아닌 것 같다. 그냥 더 깔끔해보이고 자기 스타일에 맞는 방법을 선택하면 될 것 같다.
그래서 결론은
- 단순히 DB에 timestamp 로써 저장을 한다거나 복잡하고 정교한 비교 등의 연산이 필요한 경우 좀 더 가볍고 정교한 Instant 가 필요하다. 시차를 적용하여 저장해야 한다면, 연산이 끝난 뒤에 atZone 메서드를 통해 ZoneId를 넘겨주어 이를 ZonedDateTime 으로 원하는 시차를 적용하여 변환할 수 있다.
- 일반적으로 우리나라에서만 서비스하는 경우 LocalDateTime 을 사용하여 서버 전체에 시차를 한번만 설정해주는 것이 제일 편한 것 같다.
- 여러 나라에서 서비스 해야한다면, now 메서드를 실행할 때 ZoneId 를 넘겨주어 여러 개의 시차를 적용할 수 있을 것이다. 그때는 LocalDateTime 이나 ZonedDateTime 뭐 아무거나 써도 될 것 같다. (아직 크게 다른 점을 모르겠다...)
'스프링' 카테고리의 다른 글
Spring 협업을 위한 프로젝트 디렉토리 구조 (Project Directory Structure) (0) | 2024.07.16 |
---|---|
SpringBoot 생성자 패턴 (Lombok) (0) | 2024.07.08 |