Go

sqlx와 PostgreSQL 타임존 완벽 정복!

드리프트2 2025. 1. 12. 13:54

sqlx와 PostgreSQL 타임존 완벽 정복!

안녕하세요!

 

오늘은 Go 언어에서 sqlx와 Postgres를 사용해 타임존을 다루는 방법에 대해 알아보겠습니다.

사실 거의 database/sql 패키지 이야기가 될 것 같긴 한데요.

 

UTC 기준으로 데이터를 저장하는 PostgreSQL에서, Go 쪽에서 특정 타임존을 기준으로 PostgreSQL의 timestamp 데이터를 가져오고 싶습니다.

원본 데이터

sample-db=# \d test_timezone
                     테이블 "public.test_timezone"
  컬럼   |           타입           | 콜Collation | Nullable | 기본값
-----------+--------------------------+-----------+----------+---------
 test_time | timestamp with time zone |           |          |

sample-db=# SELECT * from test_timezone;
       test_time
------------------------
 2025-01-12 15:00:00+00
(1 row)

얻고 싶은 결과

2025-01-13 00:00:00 +0900 KST

그대로 가져오기

결과를 그대로 가져와 봤는데요.

var examTime time.Time
db.Get(&examTime, "SELECT test_time from test_timezone")
fmt.Println(examTime)

// 출력
// 2025-01-12 15:00:00 +0000 UTC

 

time.Now() 같은 경우에는 Go를 실행하는 서버의 타임존에 맞춰지기도 하는데요.

 

이 경우에는 서버 자체의 타임존은 영향을 주지 않는 것 같아요.

 

timestamp 타입 OR timestamptz 타입

데이터 타입이 timestamptz 타입이어야만 타임존 정보를 얻을 수 있는데요.

var examTime time.Time
db.Get(&examTime, "SELECT test_time::timestamp from test_timezone")
fmt.Println(examTime)

// 출력
// 2025-01-12 15:00:00 +0000 +0000

 

PostgreSQL의 timezone 함수를 사용하면 다음과 같은데요.

 

시간은 원하는 대로 나왔지만, timezone 함수를 사용했음에도 불구하고 반환되는 값의 타입이 timestamp이기 때문에 타임존 정보를 얻을 수는 없네요.

 

😥

[참고 자료: PostgreSQL 8.4.4 문서 9.9.

날짜/시간 함수와 연산자](https://www.postgresql.org/docs/8.4/functions-datetime.html)

var examTime time.Time
db.Get(&examTime, "SELECT timezone('KST', test_time::timestamptz) FROM test_timezone;")
fmt.Println(examTime)

// 출력
// 2025-01-13 00:00:00 +0000 +0000

해결 방법

세션의 타임존을 원하는 값으로 설정해주면 예상했던 값을 얻을 수 있습니다.

 

다만, 한번 변경하면 명시적으로 되돌리기 전까지는 해당 연결에서는 계속 타임존이 변경된 상태로 유지되니 주의해야 합니다.

var examTime time.Time
db.Exec("SET SESSION timezone TO 'Asia/Tokyo';")
db.Get(&examTime, "SELECT test_time from test_timezone")
fmt.Println(examTime)

// 출력
// 2025-01-13 00:00:00 +0900 JST

다른 해결 방법

이 방법은 단순한 PostgreSQL 팁이 되어버리는데요.

 

실용성은 떨어지지만, Go에서 타임존을 제어하는 방법을 알아볼게요.

 

이 내용을 참고해서 Scan 함수를 구현하면 데이터를 읽어올 때 자체적으로 처리 로직을 작성할 수 있습니다.

 

type MyTime time.Time

const TZ = "Asia/Seoul"

// println을 위한 함수입니다.
func (m MyTime) String() string {
        return time.Time(m).String()
}

// 읽기 함수를 구현합니다.
func (m *MyTime) Scan(value any) error {
        if value == nil {
                m = &MyTime{}
                return nil
        }
        t, ok := value.(time.Time)
        if !ok {
                return fmt.Errorf("타입이 맞지 않는데요.")
        }
        tz, _ := time.LoadLocation(TZ)
        mt := MyTime(t.In(tz))
        *m = mt
        return nil
}

func createMyType(db *sqlx.DB) {
        var examTime MyTime
        db.Get(&examTime, "SELECT test_time from test_timezone")
        fmt.Println(examTime)
}

// 출력
// 2025-01-13 00:00:00 +0900 KST

 

직접 정의한 타입을 사용하는 것은 번거로울 수도 있어서 저는 timestamp 타입 그대로 사용할 것 같은데요.

 

하지만 JSON 타입이나 배열 타입을 SELECT해서 바로 구조체에 넣고 싶을 때 Scan 메서드 내에서 Unmarshal 하는 식으로 활용하면 꽤 유용한 팁이 될 수 있습니다.

 

그럼.