발단
레거시 코드에서 예상했던 시간보다 TTL이 더 길게 잡혀있는 버그를 발견하였다.
데이터베이스 레벨부터 올바른 명령어가 들어가고 있는지 확인했다.
redis
monitor
명령어를 통해 실시간으로 레디스 서버에 들어오는 명령어를 확인하였다.$ redis-cli -h {host} -p 6379 -a {password}
$ redis monitor
monitor로 확인 결과, EXPIREDAT 명령어에서 만료일시를 확인할 수 있었다.
"EXPIREDAT" "PRODUCT:K000514521" "1729258105"
1729258105 라는 값은 unix-time-seconds(=Epoch) 형태이고, 변환해보면 아래와 같다.
의도했던 시간은 2024-10-18 13:28:25 였는데, 2024-10-18 22:29:25 으로 +9시간이 추가되어서 전달되고 있었다.
실제 코드 상에서 잘못 계산하여 전달하는 것으로 보였다.
// 예시
public void expire(String key) {
LocalDateTime expireAt = LocalDateTime.now().plusDays(90);
reactiveRedisTemplate.expireAt(key, expireAt.toInstant(ZoneOffset.UTC));
}
왜 이런 문제가 생겼을까?
글로벌 대상이 아닌 대부분의 서비스에서는 timezone 을 고려할 필요 없다.
모든 시간은 KST 기준임을 가정하게 되고, 그래서 LocalDateTime 을 사용한다.
LocalDateTime은 timezone이 없는 시간을 의미한다.
A date-time without a time-zone in the ISO-8601 calendar system
LocalDateTime이 time-zone 이 없는 시간을 의미하지만, 그렇다고 time-zone 을 고민하지 않고 쓰면 안된다.
그러나 이러한 서비스들도 UTC를 사용하는 구간이 있다.
UTC를 쓰는 구간과 통신할 때는 time-zone을 고려해서 변환해주어야 한다.
redis는 별도의 서버 time-zone 이 없다.
이는 LocalDateTime 을 쓴다는 의미가 아니라,
UTC를 기준으로 계산한다.
라는 뜻이다.LocalDateTime은 KST 기준이며, Redis는 UTC 기준이므로 적절한 변환이 필요하다.
Spring에서 우리가 무심코 사용하는 ReactiveRedisTemplate를 다시 보자. expiredAt 메서드의 파라미터의 타입이 Instant 가 눈에 들어온다.

Instant는 어떤 타입일까?
Instant java-doc의 설명이 길어 중요한 부분을 발췌하면, 아래와 같다.
epoch-seconds를 쓰는데, 이는 1970-01-01T00:00:00Z를 기준으로 한다. (Z는 UTC 시간대를 의미한다.)
* The range of an instant requires the storage of a number larger than a {@code long}.
* To achieve this, the class stores a {@code long}
LocalDateTime과 Instant
그럼 잘 사용하던 LocalDateTime 으로 Instant 로 변환한다는 것은 무엇을 의미할까?
LocalDateTime.toInstant() 메서드에서는
local date-time + offset = Instant
라고 표현하고 있다.올바른 offset을 고려하여 전달했어야 했다.

결론
- LocalDateTime 대신 Instant를 검토할 것.
LocalDateTime 만 쓰는 우물에서 고개를 들어보니 꽤나 많은 곳에서 Instant를 사용하고 있다.
선배 개발자께 들은 말로는 Instant로 데이터를 저장, 관리하고, 응답을 내려줄 때는 timezone을 붙여서 ZonedDateTime 으로 사용하는 것이 나름의 best practice 라고 한다.
- 기본적인 정의를 잘 알자. 공식문서를 잘 보자.
우리는 대부분 대단히 어려운 기능을 개발하고 있지 않다.
기본만으로 어지간한 트러블슈팅은 가능하다.