2026년 10월 15일 | DBMS Error 가이드
이 글에서 다루는 내용
22018 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
22018 invalid character value for cast 는?
PostgreSQL 에러 코드 22018 (invalid_character_value_for_cast) 는 특정 문자열 값을 다른 데이터 타입으로 형변환(CAST)할 때, 해당 값이 대상 타입의 유효한 형식과 맞지 않을 경우 발생하는 에러입니다. 예를 들어 'abc'라는 문자열을 INTEGER나 DATE 타입으로 변환하려 할 때, 변환 자체가 불가능한 값이 포함되어 있으면 이 에러가 트리거됩니다. 주로 데이터 마이그레이션, ETL 작업, 또는 외부 시스템에서 유입된 비정형 데이터를 처리할 때 현업에서 빈번하게 마주치는 에러입니다.
주요 발생 원인
- 숫자가 아닌 문자열을 숫자 타입으로 캐스팅
가장 흔한 원인입니다. 데이터베이스에 VARCHAR 또는 TEXT 컬럼으로 저장된 값이 실제로는 숫자가 아닌 문자, 특수기호, 공백 등을 포함하고 있을 때 INTEGER, NUMERIC, FLOAT 등의 타입으로 변환을 시도하면 발생합니다. 예를 들어 '1,000'(쉼표 포함), '$500'(달러 기호 포함), 'N/A'와 같은 값들이 대표적인 케이스입니다. 외부 CSV 파일을 임포트하거나 레거시 시스템과 연동하는 경우 이런 데이터가 유입되는 경우가 매우 많습니다.
- 유효하지 않은 날짜/시간 문자열을 DATE 또는 TIMESTAMP로 캐스팅
'2024-13-45', '31/02/2024', 'tomorrow'처럼 날짜 형식에 맞지 않거나 실제로 존재하지 않는 날짜 문자열을 DATE, TIMESTAMP, TIMESTAMPTZ 타입으로 캐스팅할 때 발생합니다. 특히 다국가 서비스를 운영하는 환경에서는 날짜 포맷이 MM/DD/YYYY, DD-MM-YYYY, YYYY.MM.DD 등으로 혼재되어 있어 예상치 못한 에러가 자주 발생합니다. DateStyle 파라미터 설정에 따라 동일한 값이 어떤 환경에서는 성공하고 다른 환경에서는 실패하는 경우도 있어 운영 환경과 개발 환경 간의 차이로 인해 더욱 혼란스러울 수 있습니다.
- ENUM 또는 도메인 타입 캐스팅 시 허용되지 않는 값 사용
PostgreSQL의 ENUM 타입이나 사용자 정의 도메인(DOMAIN)으로 캐스팅할 때, 해당 타입에 정의된 허용 값 목록에 없는 문자열을 변환하려 하면 이 에러가 발생합니다. 예를 들어 'ACTIVE', 'INACTIVE'만 허용하는 ENUM 타입에 'active'(소문자) 또는 'DELETED'(미정의 값)를 캐스팅하면 실패합니다. 애플리케이션 로직 변경이나 신규 상태 값 추가 시 ENUM 타입을 업데이트하지 않은 채로 데이터를 삽입하는 경우에 자주 발생합니다.
해결 방법
원인 1: 숫자 캐스팅 실패 해결
REGEXP_REPLACE나 NULLIF, CASE 구문을 활용하여 변환 전에 값을 정제하거나, 변환이 불가능한 경우 NULL로 처리합니다.
-- 문제가 되는 쿼리 예시
SELECT CAST('1,500' AS INTEGER); -- ERROR: 22018
-- 해결 방법 1: 특수문자 제거 후 캐스팅
SELECT CAST(REGEXP_REPLACE('1,500', '[^0-9]', '', 'g') AS INTEGER);
-- 결과: 1500
-- 해결 방법 2: 변환 불가 값은 NULL로 처리 (PostgreSQL 함수 활용)
CREATE OR REPLACE FUNCTION safe_cast_to_int(p_value TEXT)
RETURNS INTEGER AS $$
BEGIN
RETURN p_value::INTEGER;
EXCEPTION
WHEN invalid_character_value_for_cast OR numeric_value_out_of_range THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- 함수 사용 예시
SELECT safe_cast_to_int('1500'); -- 결과: 1500
SELECT safe_cast_to_int('N/A'); -- 결과: NULL
SELECT safe_cast_to_int('$500'); -- 결과: NULL
-- 해결 방법 3: 테이블 전체 데이터 정제 후 타입 변환
SELECT
id,
CASE
WHEN amount ~ '^[0-9]+(\.[0-9]+)?$' THEN amount::NUMERIC
ELSE NULL
END AS amount_numeric
FROM raw_data_table;
원인 2: 날짜/시간 캐스팅 실패 해결
TO_DATE, TO_TIMESTAMP 함수를 사용하면 포맷을 명시적으로 지정할 수 있어 보다 안전하게 변환할 수 있습니다.
-- 문제가 되는 쿼리 예시
SELECT CAST('31/02/2024' AS DATE); -- ERROR: 22018
-- 해결 방법 1: TO_DATE로 포맷 명시
SELECT TO_DATE('15/06/2024', 'DD/MM/YYYY'); -- 결과: 2024-06-15
-- 해결 방법 2: 날짜 유효성 검사 함수 생성
CREATE OR REPLACE FUNCTION safe_cast_to_date(p_value TEXT, p_format TEXT DEFAULT 'YYYY-MM-DD')
RETURNS DATE AS $$
BEGIN
RETURN TO_DATE(p_value, p_format);
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- 함수 사용 예시
SELECT safe_cast_to_date('2024-06-15', 'YYYY-MM-DD'); -- 결과: 2024-06-15
SELECT safe_cast_to_date('31/02/2024', 'DD/MM/YYYY'); -- 결과: NULL (존재하지 않는 날짜)
SELECT safe_cast_to_date('abc'); -- 결과: NULL
-- 해결 방법 3: 정규식으로 날짜 형식 사전 검증
SELECT
date_str,
CASE
WHEN date_str ~ '^\d{4}-\d{2}-\d{2}$'
THEN safe_cast_to_date(date_str, 'YYYY-MM-DD')
ELSE NULL
END AS parsed_date
FROM import_staging_table;
원인 3: ENUM 캐스팅 실패 해결
ENUM 타입 캐스팅 오류는 값 정규화 또는 ENUM 타입 확장으로 해결합니다.
-- ENUM 타입 정의 예시
CREATE TYPE user_status AS ENUM ('ACTIVE', 'INACTIVE', 'SUSPENDED');
-- 문제가 되는 쿼리 예시
SELECT CAST('active' AS user_status); -- ERROR: 22018 (소문자)
SELECT CAST('DELETED' AS user_status); -- ERROR: 22018 (미정의 값)
-- 해결 방법 1: 값을 대문자로 정규화 후 캐스팅
SELECT CAST(UPPER('active') AS user_status); -- 결과: ACTIVE
-- 해결 방법 2: 허용된 값인지 확인 후 캐스팅
SELECT
CASE
WHEN UPPER(status_str) IN ('ACTIVE', 'INACTIVE', 'SUSPENDED')
THEN UPPER(status_str)::user_status
ELSE 'INACTIVE'::user_status -- 기본값 처리
END AS safe_status
FROM user_import_table;
-- 해결 방법 3: ENUM 타입에 새 값 추가 (순서 중요)
ALTER TYPE user_status ADD VALUE IF NOT EXISTS 'DELETED' AFTER 'SUSPENDED';
예방 방법
- 스테이징 테이블을 활용한 데이터 유효성 검증 레이어 구축
외부에서 데이터를 임포트할 때는 항상 모든 컬럼을 TEXT 타입으로 받는 스테이징 테이블을 먼저 거치도록 설계하세요. 스테이징 단계에서 정규식 검사, NULL 체크, 범위 검증 등을 수행한 후 검증이 완료된 데이터만 실제 테이블에 INSERT ... SELECT 방식으로 이관합니다. 이 방식은 에러 발생 시 전체 트랜잭션이 롤백되는 것을 방지하고, 불량 데이터만 별도 에러 로그 테이블에 기록할 수 있어 운영 안정성을 크게 향상시킵니다.
“`sql
— 스테이징 테이블 (모두 TEXT 타입)
CREATE TABLE staging_users (
id TEXT,
name TEXT,
age TEXT,
created_at TEXT,
status TEXT
);
— 유효성 검증 후 실제 테이블로 이관
INSERT INTO users (id, name, age, created_at, status)
SELECT
safe_cast_to_int(id),
name,
safe_cast_to_int(age),
safe_cast_to_date(created_at, ‘YYYY-MM-DD’),
CASE WHEN UPPER(status) IN (‘ACTIVE’,’INACTIVE’,’SUSPENDED’)
THEN UPPER(status)::user_status
ELSE ‘INACTIVE’::user_status END
FROM staging_users
WHERE id ~ ‘^[0-9]+$’ — 숫자 ID만 허용
AND age ~ ‘^[0-9]+$’; — 숫자 나이만 허용
“`
- 애플리케이션 레벨에서의 입력값 타입 강제 및 DB 제약 조건 활용
애플리케이션 단에서 입력값을 데이터베이스에 전달하기 전에 타입 검증을 수행하고, CHECK 제약 조건과 DOMAIN 타입을 적극 활용하여 잘못된 값이 데이터베이스에 저장되는 것 자체를 차단하세요. 이는 사후 치료보다 사전 예방이 훨씬 효율적이라는 원칙을 DB 레벨에서 구현하는 방법입니다.
“`sql
— 도메인 타입으로 유효성 규칙 캡슐화
CREATE DOMAIN positive_integer AS INTEGER
CHECK (VALUE > 0);
CREATE DOMAIN valid_email AS TEXT
CHECK (VALUE ~ ‘^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$’);
— 테이블에 도메인 타입 적용
CREATE TABLE members (
id positive_integer PRIMARY KEY,
email valid_email NOT NULL,
age positive_integer
);
“`
관련 에러
- 22007 (invalid_datetime_format): 날짜/시간 문자열의 형식 자체가 잘못된 경우 발생하며, 22018과 함께 날짜 처리 시 자주 동반됩니다.
- 22003 (numeric_value_out_of_range): 숫자 변환은 성공했지만 대상 타입의 허용 범위를 초과할 때 발생합니다.
SMALLINT에 100,000을 넣으려 할 때가 대표적입니다. - 42804 (datatype_mismatch): 함수 인자나 컬럼 정의와 실제 전달된 값의 타입이 일치하지 않을 때 발생하며, 묵시적 캐스팅이 실패할 경우 22018과 함께 등장할 수 있습니다.
- 22P02 (invalid_text_representation):
'abc'::integer처럼 텍스트 표현 자체가 유효하지 않을 때 발생하며, 22018과 매우 유사한 상황에서 발생하므로 혼동하지 않도록 주의가 필요합니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.