PostgreSQL 22009 오류 원인과 해결 방법 완벽 가이드

22009
2026년 06월 10일 | DBMS Error 가이드

이 글에서 다루는 내용

22009 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.

22009 invalid time zone displacement value 는?

PostgreSQL 에러 코드 22009invalid time zone displacement value로, 시간대 오프셋(displacement) 값이 허용된 범위를 벗어나거나 형식이 잘못된 경우 발생합니다. 주로 AT TIME ZONE 절, timestamptz 변환, 또는 ISO 8601 형식의 타임스탬프를 파싱할 때 잘못된 오프셋 값이 입력될 때 나타납니다. 예를 들어 +99:00처럼 실제로 존재하지 않는 UTC 오프셋 값을 사용하거나, 분 단위 오프셋이 0~59 범위를 초과하는 경우 이 에러가 트리거됩니다.


주요 발생 원인

1. 허용 범위를 초과한 UTC 오프셋 사용

PostgreSQL에서 허용하는 UTC 오프셋의 범위는 -15:59:59에서 +15:59:59까지입니다. 이 범위를 벗어난 오프셋 값(예: +16:00, -20:00)을 타임스탬프 리터럴에 포함하거나 AT TIME ZONE 절에 전달하면 즉시 22009 에러가 발생합니다. 외부 시스템(API, 레거시 DB 등)에서 넘어온 데이터를 검증 없이 그대로 삽입할 때 가장 빈번하게 나타납니다.

2. 잘못된 형식의 시간대 오프셋 문자열

오프셋 문자열의 형식이 ±HH:MM 또는 ±HH:MM:SS를 따르지 않는 경우에도 이 에러가 발생합니다. 예를 들어 분(minute) 값이 60 이상이거나(+05:75), 콜론 구분자 없이 숫자만 나열된 경우(+0530 형식을 잘못 해석한 경우) PostgreSQL 파서가 이를 유효하지 않은 오프셋으로 판단합니다. 애플리케이션에서 문자열을 직접 조합하여 타임스탬프를 생성할 때 포맷 오류가 섞여 들어오는 경우가 많습니다.

3. 애플리케이션의 동적 오프셋 계산 오류

백엔드 애플리케이션에서 사용자의 로컬 타임존 오프셋을 분 단위로 계산한 후 이를 SQL 쿼리에 동적으로 주입할 때 계산 실수가 발생할 수 있습니다. 예를 들어 JavaScript의 getTimezoneOffset()은 UTC와의 차이를 반대 부호로 반환하기 때문에, 이를 그대로 SQL에 적용하면 잘못된 오프셋 문자열이 생성될 수 있습니다. 이런 경우 범위 검증 없이 쿼리가 실행되면 22009 에러로 이어집니다.


해결 방법

원인 1 해결: 오프셋 범위 검증 후 변환

외부에서 유입된 타임스탬프 문자열을 삽입하기 전에 pg_catalog 함수나 CASE 문을 활용해 사전에 유효성을 검사하세요.

-- 잘못된 오프셋 값으로 인한 에러 재현
SELECT TIMESTAMPTZ '2024-01-15 12:00:00+16:00';
-- ERROR: invalid time zone displacement value: "+16:00"
-- SQLSTATE: 22009

-- 해결: 오프셋 삽입 전 범위 검증 함수 작성
CREATE OR REPLACE FUNCTION safe_parse_timestamptz(p_input TEXT)
RETURNS TIMESTAMPTZ AS $$
DECLARE
    v_result TIMESTAMPTZ;
BEGIN
    BEGIN
        v_result := p_input::TIMESTAMPTZ;
    EXCEPTION
        WHEN invalid_datetime_format OR datetime_field_overflow THEN
            RAISE WARNING '잘못된 타임스탬프 형식: %', p_input;
            RETURN NULL;
        WHEN SQLSTATE '22009' THEN
            RAISE WARNING '유효하지 않은 시간대 오프셋: %', p_input;
            RETURN NULL;
    END;
    RETURN v_result;
END;
$$ LANGUAGE plpgsql;

-- 사용 예시
SELECT safe_parse_timestamptz('2024-01-15 12:00:00+16:00');  -- NULL 반환
SELECT safe_parse_timestamptz('2024-01-15 12:00:00+09:00');  -- 정상 반환

원인 2 해결: 오프셋 문자열 정규화

애플리케이션에서 넘어온 오프셋 문자열을 PostgreSQL에 전달하기 전에 정규화하는 로직을 추가합니다.

-- 분 단위 오프셋 값을 ±HH:MM 형식으로 변환하는 함수
CREATE OR REPLACE FUNCTION normalize_tz_offset(p_minutes INT)
RETURNS TEXT AS $$
DECLARE
    v_hours   INT;
    v_mins    INT;
    v_sign    TEXT;
BEGIN
    -- 오프셋 허용 범위 검증 (-959 ~ +959분)
    IF p_minutes < -959 OR p_minutes > 959 THEN
        RAISE EXCEPTION 'UTC 오프셋 범위 초과: % 분', p_minutes
            USING ERRCODE = '22009';
    END IF;

    v_sign  := CASE WHEN p_minutes >= 0 THEN '+' ELSE '-' END;
    v_hours := ABS(p_minutes) / 60;
    v_mins  := ABS(p_minutes) % 60;

    RETURN format('%s%02d:%02d', v_sign, v_hours, v_mins);
END;
$$ LANGUAGE plpgsql;

-- 사용 예시: JavaScript getTimezoneOffset() 반환값(-540 = UTC+9) 처리
SELECT normalize_tz_offset(-540);   -- 결과: +09:00
SELECT normalize_tz_offset(330);    -- 결과: -05:30 (인도 표준시 역방향 주의)

-- 동적 타임스탬프 생성에 적용
SELECT ('2024-01-15 12:00:00' || normalize_tz_offset(-540))::TIMESTAMPTZ;

원인 3 해결: AT TIME ZONE 절에서 안전한 오프셋 사용

동적으로 생성된 오프셋을 AT TIME ZONE에 전달할 때는 반드시 유효성을 먼저 검사하세요.

-- 잘못된 오프셋을 AT TIME ZONE에 사용한 경우 에러 발생
SELECT NOW() AT TIME ZONE '+99:00';
-- ERROR: invalid time zone displacement value

-- 해결: 오프셋 유효성을 검사하는 뷰 또는 래퍼 쿼리 사용
WITH validated_offset AS (
    SELECT
        offset_str,
        CASE
            WHEN offset_str ~ '^[+-](0[0-9]|1[0-5]):[0-5][0-9]$' THEN TRUE
            ELSE FALSE
        END AS is_valid
    FROM (VALUES ('+09:00'), ('+16:00'), ('-05:30'), ('+99:99')) AS t(offset_str)
)
SELECT
    offset_str,
    is_valid,
    CASE
        WHEN is_valid THEN NOW() AT TIME ZONE offset_str
        ELSE NULL
    END AS converted_time
FROM validated_offset;

-- 결과: 유효한 오프셋만 변환, 나머지는 NULL 처리

예방 방법

1. CHECK 제약 조건 및 도메인 타입으로 입력 값 통제

데이터베이스 레벨에서 타임스탬프 컬럼에 대한 검증 로직을 내장하면, 애플리케이션의 실수를 DB가 방어막으로 차단할 수 있습니다.

-- 타임존 오프셋 문자열을 위한 도메인 생성
CREATE DOMAIN valid_tz_offset AS TEXT
    CHECK (VALUE ~ '^[+-](0[0-9]|1[0-5]):[0-5][0-9]$');

-- 외부 데이터 수집 테이블에 CHECK 제약 추가
CREATE TABLE external_events (
    id          SERIAL PRIMARY KEY,
    event_name  TEXT NOT NULL,
    event_time  TIMESTAMPTZ NOT NULL,
    tz_offset   valid_tz_offset  -- 도메인 타입 적용
);

-- 잘못된 오프셋 삽입 시도 시 DB 레벨에서 차단
INSERT INTO external_events (event_name, event_time, tz_offset)
VALUES ('test', NOW(), '+16:00');
-- ERROR: value for domain valid_tz_offset violates check constraint

2. 애플리케이션 레이어에서 pg_exception_sqlstate로 에러 핸들링 표준화

운영 환경에서는 22009 에러를 포함한 날짜/시간 관련 에러들을 한데 묶어 처리하는 표준 에러 핸들러를 구축하고, 에러 발생 시 원본 입력값을 별도 테이블에 기록하여 사후 분석이 가능하게 합니다.

-- 에러 로그 테이블 생성
CREATE TABLE tz_error_log (
    id          SERIAL PRIMARY KEY,
    input_value TEXT,
    error_code  TEXT,
    error_msg   TEXT,
    occurred_at TIMESTAMPTZ DEFAULT NOW()
);

-- 에러 로깅을 포함한 안전한 삽입 프로시저
CREATE OR REPLACE PROCEDURE insert_event_safe(
    p_event_name TEXT,
    p_event_time TEXT
)
LANGUAGE plpgsql AS $$
BEGIN
    INSERT INTO external_events (event_name, event_time, tz_offset)
    VALUES (p_event_name, p_event_time::TIMESTAMPTZ, '+00:00');
EXCEPTION
    WHEN SQLSTATE '22009' THEN
        INSERT INTO tz_error_log (input_value, error_code, error_msg)
        VALUES (p_event_time, '22009', 'invalid time zone displacement value');
        RAISE WARNING '[22009] 시간대 오프셋 오류 기록됨: %', p_event_time;
    WHEN OTHERS THEN
        INSERT INTO tz_error_log (input_value, error_code, error_msg)
        VALUES (p_event_time, SQLSTATE, SQLERRM);
        RAISE;
END;
$$;

관련 에러

  • 22007 (invalid_datetime_format): 날짜/시간 문자열 자체의 형식이 잘못된 경우 발생합니다. 22009가 오프셋 값의 범위 문제라면, 22007은 전체 날짜 형식 파싱 실패입니다.
  • 22008 (datetime_field_overflow): 월, 일, 시, 분, 초 등의 개별 필드 값이 허용 범위를 초과했을 때 발생합니다. 22009와 함께 날짜/시간 데이터 정제 로직에서 자주 함께 다루어집니다.
  • 42883 (undefined_function): AT TIME ZONE 절에 잘못된 타입의 인자를 전달했을 때 오버로딩 실패로 발생할 수 있으며, 간접적으로 시간대 처리 로직과 연관됩니다.

DBMS 에러 코드 시리즈

주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.

본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.

댓글 남기기