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

22009
2026년 08월 14일 | DBMS Error 가이드

이 글에서 다루는 내용

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

22009 invalid time zone displacement value 는?

PostgreSQL 에러 코드 22009는 invalid time zone displacement value로, 타임존 오프셋(변위값)이 유효하지 않은 범위를 벗어났을 때 발생하는 에러입니다. 주로 AT TIME ZONE 절이나 타임스탬프 리터럴에서 잘못된 형식의 오프셋 값을 사용할 때 트리거됩니다. SQL 표준에서 허용하는 타임존 변위값의 범위는 -16:00에서 +16:00 사이이며, 이를 벗어나거나 형식이 올바르지 않은 경우 이 에러가 발생합니다.


주요 발생 원인

  • 허용 범위를 벗어난 오프셋 값 입력

타임존 오프셋은 -16:00부터 +16:00까지만 유효합니다. 예를 들어 +25:00 또는 -20:00처럼 범위를 초과하는 값을 입력하면 PostgreSQL은 즉시 이 에러를 반환합니다. 애플리케이션에서 사용자 입력을 그대로 타임존 오프셋으로 사용하는 경우 특히 자주 발생하며, 입력 유효성 검사가 없을 때 문제가 커집니다.

  • 잘못된 형식의 타임존 문자열 사용

타임존 변위값은 ±HH:MM 또는 ±HH 형식을 따라야 합니다. +5.5, UTC+9, 9:00 같이 표준 형식을 따르지 않는 문자열을 오프셋으로 사용하면 파싱 단계에서 실패하여 22009 에러가 발생합니다. 특히 외부 시스템이나 레거시 데이터베이스에서 마이그레이션한 데이터에 이런 형식의 값이 포함되어 있는 경우가 많습니다.

  • 동적 SQL 또는 함수 내부에서 변수를 통한 오프셋 주입

PL/pgSQL 함수나 동적 SQL에서 외부로부터 받은 파라미터를 타임존 오프셋으로 직접 사용할 때, 충분한 검증 없이 쿼리에 주입되면 이 에러가 발생할 수 있습니다. 특히 API나 배치 작업에서 타임존 정보를 동적으로 처리할 때 예상치 못한 값이 들어오는 경우가 많습니다.


해결 방법

원인 1: 범위를 벗어난 오프셋 수정

허용 범위 내의 올바른 오프셋 값을 사용하거나, 입력 전에 범위를 검증합니다.

-- 에러 발생 예시 (범위 초과)
SELECT TIMESTAMPTZ '2024-01-15 10:00:00+25:00';
-- ERROR: invalid time zone displacement value

-- 올바른 오프셋 사용
SELECT TIMESTAMPTZ '2024-01-15 10:00:00+09:00';

-- AT TIME ZONE에서 올바른 오프셋 사용
SELECT NOW() AT TIME ZONE '+09:00';

-- 타임존 이름 사용 (권장 방법)
SELECT NOW() AT TIME ZONE 'Asia/Seoul';
SELECT NOW() AT TIME ZONE 'America/New_York';

원인 2: 잘못된 형식의 타임존 문자열 수정

표준 형식(±HH:MM)으로 변환하거나, 명시적인 타임존 이름을 사용하세요.

-- 에러 발생 예시 (잘못된 형식)
SELECT '2024-01-15 10:00:00' AT TIME ZONE 'UTC+9';
-- 위는 일부 버전에서 동작하지 않거나 예기치 않은 결과를 낼 수 있음

-- 올바른 형식으로 변환
SELECT TIMESTAMP '2024-01-15 10:00:00' AT TIME ZONE 'Asia/Seoul';

-- 오프셋 형식으로 변환하는 함수 예시
CREATE OR REPLACE FUNCTION safe_timezone_convert(
    p_timestamp TIMESTAMP,
    p_offset_hours INTEGER,
    p_offset_minutes INTEGER DEFAULT 0
) RETURNS TIMESTAMPTZ AS $$
DECLARE
    v_offset TEXT;
BEGIN
    -- 범위 검증
    IF p_offset_hours < -16 OR p_offset_hours > 16 THEN
        RAISE EXCEPTION '유효하지 않은 타임존 오프셋: %시간', p_offset_hours;
    END IF;
    
    v_offset := format('%s%02d:%02d',
        CASE WHEN p_offset_hours >= 0 THEN '+' ELSE '-' END,
        ABS(p_offset_hours),
        p_offset_minutes
    );
    
    RETURN p_timestamp AT TIME ZONE v_offset;
END;
$$ LANGUAGE plpgsql;

-- 함수 사용 예시
SELECT safe_timezone_convert('2024-01-15 10:00:00', 9, 0);
SELECT safe_timezone_convert('2024-01-15 10:00:00', -5, 30);

원인 3: 동적 SQL에서의 오프셋 검증

동적으로 오프셋을 처리하는 경우 반드시 사전 검증 로직을 추가하세요.

-- 동적 SQL에서 안전한 타임존 처리
CREATE OR REPLACE FUNCTION convert_with_dynamic_offset(
    p_timestamp TIMESTAMP,
    p_tz_offset TEXT
) RETURNS TIMESTAMPTZ AS $$
DECLARE
    v_result TIMESTAMPTZ;
    v_hours   INTEGER;
    v_minutes INTEGER;
    v_sign    INTEGER;
BEGIN
    -- 오프셋 형식 검증 (정규식 활용)
    IF p_tz_offset !~ '^[+-][0-9]{2}:[0-9]{2}$' THEN
        RAISE EXCEPTION '잘못된 타임존 오프셋 형식: %. 형식은 +HH:MM 또는 -HH:MM 이어야 합니다.', p_tz_offset;
    END IF;

    v_sign    := CASE WHEN LEFT(p_tz_offset, 1) = '+' THEN 1 ELSE -1 END;
    v_hours   := v_sign * SPLIT_PART(p_tz_offset, ':', 1)::INTEGER;
    v_minutes := SPLIT_PART(p_tz_offset, ':', 2)::INTEGER;

    -- 범위 검증
    IF ABS(v_hours) > 16 OR v_minutes >= 60 THEN
        RAISE EXCEPTION '타임존 오프셋 범위 초과: %', p_tz_offset;
    END IF;

    v_result := p_timestamp AT TIME ZONE p_tz_offset;
    RETURN v_result;

EXCEPTION
    WHEN SQLSTATE '22009' THEN
        RAISE EXCEPTION '타임존 변환 실패 (22009): 오프셋 값 % 은(는) 유효하지 않습니다.', p_tz_offset;
END;
$$ LANGUAGE plpgsql;

-- 정상 동작 예시
SELECT convert_with_dynamic_offset('2024-06-01 09:00:00', '+09:00');

-- 에러 트리거 예시 (검증에 의해 차단됨)
SELECT convert_with_dynamic_offset('2024-06-01 09:00:00', '+25:00');

-- 기존 데이터에서 잘못된 오프셋 탐지 쿼리
SELECT id, timezone_offset
FROM user_settings
WHERE timezone_offset !~ '^[+-][0-1][0-9]:[0-5][0-9]$'
   OR SPLIT_PART(timezone_offset, ':', 1)::INTEGER NOT BETWEEN -16 AND 16;

예방 방법

  • 오프셋 대신 IANA 타임존 이름 사용

숫자 오프셋(+09:00) 대신 IANA 타임존 데이터베이스 이름(Asia/Seoul, America/New_York, Europe/London)을 사용하면 오프셋 범위 문제를 원천 차단할 수 있습니다. PostgreSQL은 pg_timezone_names 뷰를 통해 지원하는 모든 타임존 이름을 조회할 수 있으며, 애플리케이션 레이어에서 화이트리스트 기반 검증과 함께 사용하면 가장 안전합니다.

“`sql

— 유효한 타임존 이름 목록 조회

SELECT name, abbrev, utc_offset, is_dst

FROM pg_timezone_names

WHERE name LIKE ‘Asia/%’

ORDER BY utc_offset;

— 입력값이 유효한 타임존 이름인지 검증하는 함수

CREATE OR REPLACE FUNCTION is_valid_timezone(p_tz TEXT)

RETURNS BOOLEAN AS $$

BEGIN

RETURN EXISTS (SELECT 1 FROM pg_timezone_names WHERE name = p_tz);

END;

$$ LANGUAGE plpgsql STABLE;

— 사용 예시

SELECT is_valid_timezone(‘Asia/Seoul’); — true

SELECT is_valid_timezone(‘Invalid/Zone’); — false

“`

  • CHECK 제약 조건 및 도메인 타입으로 데이터 무결성 보장

타임존 오프셋을 저장하는 컬럼에 CHECK 제약 조건을 추가하거나, 별도의 도메인 타입을 정의하여 잘못된 값이 데이터베이스에 저장되는 것을 원천 차단하세요. 이는 애플리케이션 레이어의 버그나 직접 SQL 실행 시 발생하는 실수를 방지하는 데 효과적입니다.

“`sql

— 타임존 오프셋을 위한 도메인 타입 정의

CREATE DOMAIN valid_tz_offset AS TEXT

CHECK (

VALUE ~ ‘^[+-][0-1][0-9]:[0-5][0-9]$’

AND (LEFT(VALUE, 1) || SPLIT_PART(VALUE, ‘:’, 1))::INTEGER BETWEEN -16 AND 16

);

— 도메인 타입 사용 예시

CREATE TABLE user_preferences (

user_id BIGINT PRIMARY KEY,

tz_offset valid_tz_offset NOT NULL DEFAULT ‘+00:00’,

tz_name TEXT NOT NULL DEFAULT ‘UTC’,

CONSTRAINT chk_tz_name CHECK (tz_name IN (SELECT name FROM pg_timezone_names))

);

— 정상 삽입

INSERT INTO user_preferences VALUES (1, ‘+09:00’, ‘Asia/Seoul’);

— 에러 발생 (도메인 제약 위반)

INSERT INTO user_preferences VALUES (2, ‘+25:00’, ‘Invalid’);

“`


관련 에러

  • 22007 (invalid_datetime_format): 날짜/시간 리터럴의 형식 자체가 잘못된 경우 발생하며, 22009와 함께 타임스탬프 파싱 과정에서 자주 동반됩니다.
  • 22008 (datetime_field_overflow): 날짜/시간 필드의 값이 유효 범위를 초과할 때 발생합니다. 예를 들어 월이 13인 경우가 해당됩니다.
  • 42703 (undefined_column): 동적 SQL에서 타임존 관련 컬럼을 잘못 참조하는 경우 함께 발생할 수 있습니다.
  • 22P02 (invalid_text_representation): 문자열을 타임스탬프나 오프셋으로 캐스팅하는 과정에서 형식이 맞지 않을 때 발생합니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기