2026년 10월 09일 | DBMS Error 가이드
이 글에서 다루는 내용
0F000 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
0F000 locator exception 는?
PostgreSQL 에러 코드 0F000은 Locator Exception으로, SQL 표준에서 정의한 LOB(Large Object Binary) 또는 대용량 객체 로케이터(Locator)를 다루는 과정에서 발생하는 예외입니다. 이 에러는 주로 대용량 객체(Large Object)의 OID(Object Identifier)가 유효하지 않거나, 이미 해제된 로케이터를 참조하거나, 트랜잭션 범위를 벗어난 Large Object 접근 시 발생합니다. PostgreSQL에서는 lo_open(), lo_read(), lo_write(), lo_close() 등의 Large Object 관련 함수를 잘못 사용할 때 이 에러 계열이 트리거될 수 있습니다.
주요 발생 원인
1. 유효하지 않은 Large Object OID 참조
가장 흔한 원인으로, 존재하지 않거나 이미 삭제된 Large Object의 OID를 사용하여 lo_open() 혹은 관련 함수를 호출하는 경우입니다. PostgreSQL의 pg_largeobject 시스템 카탈로그에 해당 OID가 없는 상태에서 접근을 시도하면, 서버는 해당 로케이터가 유효하지 않다고 판단하고 Locator Exception을 발생시킵니다. 개발 중 테스트 데이터 삭제 후 하드코딩된 OID를 그대로 사용하거나, 외래 키 제약 없이 Large Object OID를 별도 컬럼으로 관리할 때 자주 발생합니다.
2. 트랜잭션 외부에서의 Large Object 접근
PostgreSQL에서 Large Object는 반드시 트랜잭션 내부에서 열고 닫아야 합니다. 트랜잭션이 이미 종료된 상태에서 이전에 열었던 Large Object descriptor(fd)를 계속 사용하거나, AUTOCOMMIT 모드에서 Large Object 작업을 수행하려 할 때 이 에러가 발생합니다. 특히 Python의 psycopg2, Java의 JDBC 드라이버 등을 사용하는 애플리케이션에서 트랜잭션 관리가 제대로 되지 않을 경우 빈번하게 나타납니다.
3. 잘못된 Large Object descriptor 재사용
lo_open()으로 반환된 파일 디스크립터(fd)는 해당 트랜잭션 내에서만 유효합니다. lo_close()로 이미 닫은 디스크립터를 재사용하거나, 서로 다른 세션/트랜잭션에서 같은 fd 값을 공유하려 할 때 Locator Exception이 발생합니다. 멀티스레드 환경에서 디스크립터를 전역 변수로 관리하거나, 커넥션 풀링 환경에서 커넥션 재사용 시 초기화를 제대로 하지 않으면 이 문제가 발생할 수 있습니다.
해결 방법
원인 1 해결: OID 유효성 검증 후 접근
Large Object에 접근하기 전에 반드시 해당 OID가 pg_largeobject_metadata에 존재하는지 확인하세요.
-- Large Object OID 유효성 확인
SELECT oid
FROM pg_largeobject_metadata
WHERE oid = 12345;
-- 안전한 Large Object 접근 패턴 (존재 여부 확인 후 처리)
DO $$
DECLARE
lo_oid OID := 12345;
lo_fd INTEGER;
lo_data BYTEA;
BEGIN
-- OID 유효성 검증
IF NOT EXISTS (
SELECT 1 FROM pg_largeobject_metadata WHERE oid = lo_oid
) THEN
RAISE EXCEPTION 'Large Object OID % does not exist.', lo_oid;
END IF;
-- 트랜잭션 내에서 안전하게 열기
lo_fd := lo_open(lo_oid, 262144); -- 262144 = INV_READ
lo_data := loread(lo_fd, 1024);
PERFORM lo_close(lo_fd);
RAISE NOTICE 'Data length: %', length(lo_data);
END;
$$;
-- Large Object 전체 목록 확인
SELECT loid, pageno, length(data) AS chunk_size
FROM pg_largeobject
ORDER BY loid, pageno;
원인 2 해결: 명시적 트랜잭션 블록 사용
Large Object 작업은 반드시 명시적 트랜잭션 블록 안에서 수행해야 합니다.
-- 올바른 트랜잭션 내 Large Object 작업 패턴
BEGIN;
-- Large Object 생성
SELECT lo_creat(-1) AS new_lo_oid;
-- 생성된 OID로 데이터 쓰기 (예: OID가 99999라 가정)
DO $$
DECLARE
lo_fd INTEGER;
write_result INTEGER;
BEGIN
-- 반드시 BEGIN 이후에 lo_open 호출
lo_fd := lo_open(99999, 131072); -- 131072 = INV_WRITE
write_result := lowrite(lo_fd, 'Hello, Large Object!'::bytea);
PERFORM lo_close(lo_fd);
RAISE NOTICE 'Written bytes: %', write_result;
END;
$$;
COMMIT;
-- 잘못된 패턴 (AUTOCOMMIT 환경에서 Large Object 작업 - 에러 유발 가능)
-- lo_open()을 트랜잭션 없이 단독 실행하면 0F000 에러 발생 위험
원인 3 해결: 디스크립터 안전 관리 및 예외 처리
-- 예외 처리를 포함한 안전한 Large Object 작업 패턴
BEGIN;
DO $$
DECLARE
lo_oid OID;
lo_fd INTEGER := -1; -- 초기값을 -1로 설정하여 미열림 상태 표시
lo_buf BYTEA;
BEGIN
-- Large Object 생성
lo_oid := lo_creat(-1);
RAISE NOTICE 'Created LO with OID: %', lo_oid;
BEGIN
-- 쓰기 모드로 열기
lo_fd := lo_open(lo_oid, 131072); -- INV_WRITE
PERFORM lowrite(lo_fd, decode('48656c6c6f', 'hex')); -- "Hello"
PERFORM lo_close(lo_fd);
lo_fd := -1; -- 닫힌 상태로 초기화
-- 읽기 모드로 다시 열기
lo_fd := lo_open(lo_oid, 262144); -- INV_READ
lo_buf := loread(lo_fd, 1024);
PERFORM lo_close(lo_fd);
lo_fd := -1;
RAISE NOTICE 'Read data: %', encode(lo_buf, 'escape');
EXCEPTION WHEN OTHERS THEN
-- 에러 발생 시 열려있는 fd가 있으면 닫기 시도
IF lo_fd >= 0 THEN
PERFORM lo_close(lo_fd);
END IF;
-- Large Object 정리
PERFORM lo_unlink(lo_oid);
RAISE; -- 에러 재발생
END;
-- 사용 완료 후 Large Object 삭제
PERFORM lo_unlink(lo_oid);
END;
$$;
COMMIT;
-- 孤立된(orphan) Large Object 정리 쿼리
-- 참조되지 않는 Large Object 찾기
SELECT lo.oid
FROM pg_largeobject_metadata lo
LEFT JOIN your_table t ON t.lo_column = lo.oid
WHERE t.lo_column IS NULL;
-- vacuumlo 명령어를 통한 孤立 LO 정리 (터미널에서 실행)
-- vacuumlo -v your_database_name
예방 방법
1. Large Object 수명주기를 트랜잭션과 일치시키는 래퍼 함수 작성
모든 Large Object 작업을 단일 트랜잭션 내에서 처리하는 헬퍼 함수를 만들어 사용하면, 개발자가 실수로 트랜잭션 외부에서 Large Object를 다루는 상황을 방지할 수 있습니다. 또한 pg_largeobject_metadata에 대한 외래 키 참조 또는 트리거를 통해 OID 무결성을 강제하면 유효하지 않은 OID 참조 문제를 사전에 차단할 수 있습니다.
-- Large Object OID를 관리하는 테이블에 무결성 보장 트리거 예시
CREATE OR REPLACE FUNCTION check_lo_exists()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.file_lo_oid IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM pg_largeobject_metadata WHERE oid = NEW.file_lo_oid
) THEN
RAISE EXCEPTION 'Invalid Large Object OID: %', NEW.file_lo_oid
USING ERRCODE = '0F000';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_check_lo_oid
BEFORE INSERT OR UPDATE ON your_file_table
FOR EACH ROW EXECUTE FUNCTION check_lo_exists();
2. 정기적인 고아(Orphan) Large Object 정리 및 모니터링
Large Object는 DROP TABLE이나 DELETE로 참조 행을 삭제해도 자동으로 삭제되지 않습니다. 주기적으로 vacuumlo 유틸리티를 실행하거나, 참조되지 않는 Large Object를 찾아 정리하는 배치 작업을 스케줄링하면 OID 공간 낭비와 잘못된 참조로 인한 에러를 예방할 수 있습니다.
-- 주기적 실행용 고아 LO 정리 프로시저
CREATE OR REPLACE PROCEDURE cleanup_orphan_large_objects()
LANGUAGE plpgsql AS $$
DECLARE
orphan_oid OID;
cleaned_count INTEGER := 0;
BEGIN
FOR orphan_oid IN
SELECT lm.oid
FROM pg_largeobject_metadata lm
LEFT JOIN your_file_table ft ON ft.file_lo_oid = lm.oid
WHERE ft.file_lo_oid IS NULL
LOOP
PERFORM lo_unlink(orphan_oid);
cleaned_count := cleaned_count + 1;
END LOOP;
RAISE NOTICE 'Cleaned up % orphan Large Objects.', cleaned_count;
END;
$$;
-- 매일 새벽 실행 (pg_cron 확장 사용 시)
-- SELECT cron.schedule('cleanup-lo', '0 2 * * *', 'CALL cleanup_orphan_large_objects()');
관련 에러
| 에러 코드 | 이름 | 설명 |
|———–|——|——|
| 0F001 | invalid_locator_specification | 0F000의 하위 에러로, 구체적으로 잘못된 로케이터 명세를 참조할 때 발생합니다. Large Object OID가 문법적으로는 맞지만 실제로 존재하지 않을 때 주로 이 코드가 사용됩니다. |
| 22003 | numeric_value_out_of_range | Large Object 관련 함수에 OID 범위를 벗어난 숫자를 전달할 때 함께 나타날 수 있습니다. |
| 42883 | undefined_function | 잘못된 Large Object 함수 시그니처를 호출할 때 발생하며, 0F000과 혼동될 수 있습니다. |
| 55000 | object_not_in_prerequisite_state | Large Object가 이미 닫혔거나 잘못된 상태일 때 발생하는 유사 에러입니다. |
> 참고: PostgreSQL 공식 문서의 [Large Objects 챕터](https://www.postgresql.org/docs/current/largeobjects.html) 및 [Error Codes 부록](https://www.postgresql.org/docs/current/errcodes-appendix.html)을 함께 참고하면 더 깊이 있는 이해가 가능합니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.