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

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

이 글에서 다루는 내용

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

22009 invalid time zone displacement value 는?

PostgreSQL 에러 코드 22009는 invalid time zone displacement value로, 시간대 오프셋(displacement) 값이 PostgreSQL이 허용하는 범위를 벗어났을 때 발생하는 에러입니다. 주로 AT TIME ZONE 구문이나 timestamptz 타입을 처리할 때, 또는 외부 시스템에서 전달된 시간대 문자열이 잘못된 형식이나 범위를 가질 경우 이 에러가 트리거됩니다. PostgreSQL은 UTC 기준으로 -15:59:59부터 +15:59:59까지의 시간대 오프셋만 유효하게 처리하며, 이 범위를 초과하거나 잘못된 형식의 값이 입력되면 즉시 에러가 발생합니다.


주요 발생 원인

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

PostgreSQL은 시간대 오프셋으로 -15:59 ~ +15:59 범위만 허용합니다. 일부 레거시 시스템이나 외부 API에서 +16:00, +24:00, -20:00 같은 잘못된 오프셋 값을 전달하거나, 개발자가 실수로 분(minute) 단위 오프셋을 시간(hour) 단위로 잘못 변환하여 입력하는 경우가 많습니다. 예를 들어, IST(인도 표준시)의 오프셋인 +5:30을 +530과 같이 잘못 입력하면 이 에러가 발생합니다.

  • 잘못된 형식의 타임존 문자열 입력

INTERVAL 또는 시간대 리터럴 문자열을 직접 조작하는 과정에서 형식이 어긋나는 경우입니다. 예를 들어, '+05:30'::interval은 올바르지만 '+5.5'처럼 소수점 표기를 시간대 오프셋으로 사용하거나, 콜론(:) 구분자 없이 숫자만 입력하면 파싱 실패로 이 에러가 발생할 수 있습니다. 특히 애플리케이션에서 문자열을 동적으로 생성하여 SQL에 삽입할 때 이런 실수가 자주 발생합니다.

  • 외부 데이터 파이프라인 또는 ETL 과정에서의 데이터 오염

CSV, JSON, XML 등 외부 소스에서 데이터를 COPY 명령이나 INSERT로 대량 적재할 때, 원본 데이터에 비정상적인 타임존 오프셋 값이 포함된 경우입니다. 특히 다국적 서비스를 운영하는 환경에서 서로 다른 클라이언트 시스템이 각자의 방식으로 시간대 정보를 인코딩하여 전송하다 보면, 표준에서 벗어난 값이 유입되는 상황이 자주 발생합니다. 이런 경우 배치 작업 전체가 롤백되는 심각한 운영 장애로 이어질 수 있습니다.


해결 방법

원인 1 해결: 오프셋 범위 검증 및 보정

입력값을 DB에 저장하기 전에 오프셋이 허용 범위 내에 있는지 검증하는 함수를 작성합니다.

-- 잘못된 오프셋 예시 (에러 발생)
SELECT TIMESTAMPTZ '2024-01-15 12:00:00+16:00';
-- ERROR:  invalid time zone displacement value: "+16:00"

-- 올바른 오프셋 범위 내 사용 예시
SELECT TIMESTAMPTZ '2024-01-15 12:00:00+05:30';  -- 인도 표준시 (IST)
SELECT TIMESTAMPTZ '2024-01-15 12:00:00-08:00';  -- 미국 태평양 표준시 (PST)

-- 오프셋 유효성 검사 함수 생성
CREATE OR REPLACE FUNCTION is_valid_timezone_offset(offset_str TEXT)
RETURNS BOOLEAN AS $$
BEGIN
    -- 형식 체크: +HH:MM 또는 -HH:MM
    IF offset_str !~ '^[+-][0-1][0-9]:[0-5][0-9]$' THEN
        RETURN FALSE;
    END IF;
    
    -- 범위 체크: -15:59 ~ +15:59
    PERFORM ('2024-01-01 00:00:00' || offset_str)::TIMESTAMPTZ;
    RETURN TRUE;
EXCEPTION
    WHEN SQLSTATE '22009' THEN
        RETURN FALSE;
END;
$$ LANGUAGE plpgsql;

-- 함수 테스트
SELECT is_valid_timezone_offset('+05:30');  -- true
SELECT is_valid_timezone_offset('+16:00');  -- false
SELECT is_valid_timezone_offset('-08:00');  -- true

원인 2 해결: 안전한 타임존 변환 처리

잘못된 형식의 타임존 문자열을 안전하게 변환하는 방법입니다.

-- 잘못된 형식의 오프셋 (에러 발생 케이스)
-- SELECT '2024-01-15 12:00:00+530'::TIMESTAMPTZ;  -- 콜론 없음

-- 안전한 변환: 분 단위 정수 오프셋을 HH:MM 형식으로 변환하는 함수
CREATE OR REPLACE FUNCTION minutes_to_tz_offset(offset_minutes INTEGER)
RETURNS TEXT AS $$
DECLARE
    v_sign    TEXT;
    v_hours   INTEGER;
    v_minutes INTEGER;
BEGIN
    IF offset_minutes < -959 OR offset_minutes > 959 THEN
        RAISE EXCEPTION 'Offset minutes % is out of valid range (-959 to 959)', offset_minutes
            USING ERRCODE = '22009';
    END IF;

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

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

-- 사용 예시
SELECT minutes_to_tz_offset(330);   -- '+05:30' (IST)
SELECT minutes_to_tz_offset(-480);  -- '-08:00' (PST)
SELECT minutes_to_tz_offset(0);     -- '+00:00' (UTC)

-- 변환 결과를 이용한 타임스탬프 생성
SELECT ('2024-01-15 12:00:00' || minutes_to_tz_offset(330))::TIMESTAMPTZ AS ist_time;

원인 3 해결: ETL/COPY 과정에서의 예외 처리

대량 데이터 적재 시 잘못된 타임존 값을 처리하는 패턴입니다.

-- 스테이징 테이블을 활용한 안전한 데이터 적재 패턴
CREATE TABLE IF NOT EXISTS staging_events (
    id          SERIAL PRIMARY KEY,
    event_time  TEXT,          -- 원본 문자열 그대로 저장
    payload     JSONB,
    loaded_at   TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE IF NOT EXISTS events (
    id          SERIAL PRIMARY KEY,
    event_time  TIMESTAMPTZ NOT NULL,
    payload     JSONB,
    loaded_at   TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE IF NOT EXISTS events_error_log (
    id            SERIAL PRIMARY KEY,
    raw_event_time TEXT,
    error_message TEXT,
    logged_at     TIMESTAMPTZ DEFAULT now()
);

-- 스테이징 → 본 테이블 이관 시 에러 핸들링 프로시저
CREATE OR REPLACE PROCEDURE migrate_staging_to_events()
LANGUAGE plpgsql AS $$
DECLARE
    rec RECORD;
BEGIN
    FOR rec IN SELECT * FROM staging_events LOOP
        BEGIN
            INSERT INTO events (event_time, payload, loaded_at)
            VALUES (rec.event_time::TIMESTAMPTZ, rec.payload, rec.loaded_at);
        EXCEPTION
            WHEN SQLSTATE '22009' THEN
                -- 에러 로그 테이블에 기록하고 계속 진행
                INSERT INTO events_error_log (raw_event_time, error_message)
                VALUES (rec.event_time,
                        'Invalid timezone displacement: ' || SQLERRM);
                RAISE WARNING 'Skipping record id=% due to invalid timezone: %',
                              rec.id, rec.event_time;
        END;
    END LOOP;
    COMMIT;
END;
$$;

-- 프로시저 실행
CALL migrate_staging_to_events();

-- 에러 로그 확인
SELECT * FROM events_error_log ORDER BY logged_at DESC;

예방 방법

  • 입력 레이어에서 타임존 오프셋 정규화

애플리케이션 레이어 또는 DB 트리거를 통해 데이터가 저장되기 전에 모든 시간대 값을 UTC(+00:00)로 정규화하는 정책을 수립하세요. 내부적으로는 항상 TIMESTAMPTZ 타입으로 저장하고, 표시(presentation) 레이어에서만 지역 시간대로 변환하는 방식이 가장 안전합니다. 아래와 같이 CHECK 제약 조건을 활용할 수도 있습니다.

“`sql

— 컬럼 레벨에서 유효한 TIMESTAMPTZ만 받도록 강제

— (TIMESTAMPTZ 자체가 유효성 검사를 수행하므로 타입 지정만으로도 충분)

CREATE TABLE user_sessions (

session_id UUID DEFAULT gen_random_uuid() PRIMARY KEY,

user_id BIGINT NOT NULL,

created_at TIMESTAMPTZ NOT NULL DEFAULT now(), — 항상 UTC 기준 저장

expires_at TIMESTAMPTZ NOT NULL

);

— 세션 생성 시 올바른 방법: 항상 named timezone 사용 권장

INSERT INTO user_sessions (user_id, expires_at)

VALUES (42, now() AT TIME ZONE ‘Asia/Seoul’ + INTERVAL ‘1 hour’);

“`

  • Named Timezone 사용 권장 및 오프셋 리터럴 지양

+05:30과 같은 수치형 오프셋 리터럴보다는 'Asia/Kolkata', 'America/Los_Angeles'와 같이 IANA 타임존 데이터베이스 기반의 Named Timezone을 사용하세요. Named Timezone은 DST(서머타임) 전환도 자동으로 처리하며, 오프셋 범위 오류가 발생할 가능성이 없습니다. pg_timezone_names 뷰를 통해 PostgreSQL이 지원하는 모든 타임존 이름을 확인할 수 있습니다.

“`sql

— 지원되는 타임존 목록 조회

SELECT name, abbrev, utc_offset, is_dst

FROM pg_timezone_names

WHERE name LIKE ‘Asia/%’

ORDER BY utc_offset;

“`


관련 에러

  • 22007 invalid_datetime_format: 날짜/시간 값 자체의 형식이 잘못된 경우로, 22009와 함께 자주 발생합니다. 타임존 오프셋은 올바르지만 날짜 부분이 잘못된 경우 이 에러가 발생합니다.
  • 22008 datetime_field_overflow: 날짜/시간 필드 값이 허용 범위를 초과할 때 발생합니다. 예를 들어 월(month)에 13을 입력하거나 시(hour)에 25를 입력할 때 발생하며, 22009와 유사한 컨텍스트에서 나타납니다.
  • 22P02 invalid_text_representation: 텍스트를 타임스탬프나 타임존 타입으로 캐스팅할 때 형식 자체를 파싱하지 못하는 경우 발생하며, 22009보다 더 이른 단계(파싱 단계)에서 발생하는 에러입니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기