2026년 06월 01일 | DBMS Error 가이드
이 글에서 다루는 내용
0F000 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
0F000 locator exception 는?
PostgreSQL 에러 코드 0F000은 Locator Exception으로, SQL 표준에서 LOB(Large Object, 대용량 객체) 로케이터(locator)를 잘못 사용하거나 유효하지 않은 로케이터에 접근하려 할 때 발생하는 에러입니다. PostgreSQL에서는 주로 Large Object(lo) 기능을 사용할 때, 이미 닫혔거나 존재하지 않는 LOB 디스크립터 또는 로케이터를 참조하는 경우에 이 에러가 트리거됩니다. 트랜잭션 범위 밖에서 LOB 연산을 시도하거나, 삭제된 Large Object의 OID를 계속 사용할 때 대표적으로 나타납니다.
주요 발생 원인
1. 유효하지 않은 Large Object OID 참조
PostgreSQL의 Large Object는 pg_largeobject 시스템 카탈로그에 저장되며, 각 객체는 고유한 OID로 식별됩니다. 이미 lo_unlink()로 삭제된 LOB의 OID를 다시 열거나 읽으려 하면, 해당 OID가 더 이상 존재하지 않기 때문에 Locator Exception이 발생합니다. 특히 배치 처리나 여러 트랜잭션에 걸친 작업에서 OID를 변수에 저장해두고 나중에 재사용할 때 자주 발생합니다.
2. 트랜잭션 외부에서의 LOB 작업 시도
PostgreSQL에서 Large Object 연산(열기, 읽기, 쓰기 등)은 반드시 명시적인 트랜잭션 블록 내에서 수행되어야 합니다. BEGIN ~ COMMIT/ROLLBACK 블록 없이 lo_open(), loread(), lowrite() 등의 함수를 호출하면, 로케이터가 세션 또는 트랜잭션 컨텍스트에 바인딩되지 않아 에러가 발생합니다. 자동 커밋(autocommit) 환경에서 LOB을 다루는 애플리케이션 코드에서 이 문제가 특히 많이 나타납니다.
3. 세션 종료 후 로케이터 재사용
LOB 로케이터(파일 디스크립터 역할)는 세션 단위의 수명(lifetime)을 가집니다. 세션이 종료되거나 연결이 끊어진 후에 이전 세션에서 열었던 LOB 디스크립터를 다른 세션이나 재연결된 세션에서 재사용하려 하면 Locator Exception이 발생합니다. 커넥션 풀링 환경(예: PgBouncer)에서 LOB 디스크립터를 애플리케이션 레벨 캐시에 저장하고 재사용하는 패턴에서 자주 관찰됩니다.
해결 방법
원인 1 해결: LOB OID 유효성 검증 후 접근
LOB에 접근하기 전에 해당 OID가 pg_largeobject_metadata에 실제로 존재하는지 확인하는 습관을 들이세요.
-- LOB OID가 유효한지 먼저 확인
DO $$
DECLARE
v_loid OID := 12345; -- 접근하려는 LOB의 OID
v_exists BOOLEAN;
v_fd INTEGER;
v_data BYTEA;
BEGIN
-- pg_largeobject_metadata에서 존재 여부 확인
SELECT EXISTS (
SELECT 1 FROM pg_largeobject_metadata WHERE oid = v_loid
) INTO v_exists;
IF NOT v_exists THEN
RAISE EXCEPTION 'Large Object OID % does not exist. It may have been deleted.', v_loid;
END IF;
-- 유효한 경우에만 열기
v_fd := lo_open(v_loid, 262144); -- 262144 = INV_READ
v_data := loread(v_fd, 1024);
PERFORM lo_close(v_fd);
RAISE NOTICE 'Successfully read % bytes from LOB OID %', octet_length(v_data), v_loid;
END;
$$;
-- 안전한 LOB 삭제 패턴 (삭제 전 존재 확인)
DO $$
DECLARE
v_loid OID := 12345;
BEGIN
IF EXISTS (SELECT 1 FROM pg_largeobject_metadata WHERE oid = v_loid) THEN
PERFORM lo_unlink(v_loid);
RAISE NOTICE 'LOB OID % successfully deleted.', v_loid;
ELSE
RAISE NOTICE 'LOB OID % not found, skipping deletion.', v_loid;
END IF;
END;
$$;
원인 2 해결: 명시적 트랜잭션 블록 사용
모든 LOB 작업은 반드시 명시적 트랜잭션 내에 포함시키세요.
-- 올바른 LOB 생성 및 쓰기 패턴
BEGIN;
DO $$
DECLARE
v_loid OID;
v_fd INTEGER;
BEGIN
-- Large Object 생성
v_loid := lo_create(0); -- 0: 새 OID 자동 할당
RAISE NOTICE 'Created LOB with OID: %', v_loid;
-- 쓰기 모드로 열기 (INV_WRITE = 131072)
v_fd := lo_open(v_loid, 131072);
-- 데이터 쓰기
PERFORM lowrite(v_fd, 'Hello, Large Object World!'::BYTEA);
-- 반드시 닫기
PERFORM lo_close(v_fd);
RAISE NOTICE 'Data written to LOB OID: %', v_loid;
END;
$$;
COMMIT;
-- 잘못된 패턴 예시 (트랜잭션 없이 LOB 작업 - 에러 유발 가능)
-- lo_open(12345, 262144); -- 트랜잭션 외부에서는 동작 보장 안됨
-- LOB 읽기: 트랜잭션 + 예외 처리 패턴
BEGIN;
DO $$
DECLARE
v_loid OID := 12345;
v_fd INTEGER;
v_buf BYTEA;
v_result TEXT := '';
BEGIN
v_fd := lo_open(v_loid, 262144); -- INV_READ
-- 청크 단위로 읽기
LOOP
v_buf := loread(v_fd, 8192); -- 8KB씩 읽기
EXIT WHEN octet_length(v_buf) = 0;
v_result := v_result || convert_from(v_buf, 'UTF8');
END LOOP;
PERFORM lo_close(v_fd);
RAISE NOTICE 'LOB content: %', v_result;
EXCEPTION
WHEN OTHERS THEN
-- 에러 발생 시에도 디스크립터 정리 시도
BEGIN
PERFORM lo_close(v_fd);
EXCEPTION WHEN OTHERS THEN
NULL; -- 이미 닫혀있을 수 있음
END;
RAISE;
END;
$$;
COMMIT;
원인 3 해결: 세션별 LOB 디스크립터 관리
-- 세션 수명 내에서만 유효한 LOB 작업 패턴
-- 애플리케이션에서 매 세션마다 새로 열고 닫는 방식
CREATE OR REPLACE FUNCTION safe_read_lob(p_loid OID)
RETURNS TEXT
LANGUAGE plpgsql
AS $$
DECLARE
v_fd INTEGER;
v_buf BYTEA;
v_result TEXT := '';
BEGIN
-- LOB 존재 확인
IF NOT EXISTS (SELECT 1 FROM pg_largeobject_metadata WHERE oid = p_loid) THEN
RETURN NULL;
END IF;
-- 현재 세션의 새로운 트랜잭션에서 LOB 열기
v_fd := lo_open(p_loid, 262144);
LOOP
v_buf := loread(v_fd, 65536);
EXIT WHEN octet_length(v_buf) = 0;
v_result := v_result || encode(v_buf, 'escape');
END LOOP;
PERFORM lo_close(v_fd);
RETURN v_result;
EXCEPTION
WHEN OTHERS THEN
RAISE WARNING 'Error reading LOB OID %: %', p_loid, SQLERRM;
RETURN NULL;
END;
$$;
-- 사용 예
BEGIN;
SELECT safe_read_lob(12345);
COMMIT;
예방 방법
1. LOB 참조 무결성 관리 및 트리거 활용
애플리케이션 테이블에서 LOB OID를 외래 참조로 관리하고, 행 삭제 시 연관된 LOB도 자동으로 정리하는 트리거를 설정하면 유효하지 않은 OID 참조로 인한 에러를 원천 차단할 수 있습니다.
-- LOB OID를 관리하는 테이블 예시
CREATE TABLE document_store (
id SERIAL PRIMARY KEY,
file_name TEXT NOT NULL,
lob_oid OID NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 행 삭제 시 LOB 자동 정리 트리거
CREATE OR REPLACE FUNCTION trg_delete_lob()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF OLD.lob_oid IS NOT NULL THEN
BEGIN
PERFORM lo_unlink(OLD.lob_oid);
EXCEPTION WHEN OTHERS THEN
RAISE WARNING 'Could not unlink LOB OID %: %', OLD.lob_oid, SQLERRM;
END;
END IF;
RETURN OLD;
END;
$$;
CREATE TRIGGER trg_after_delete_document
AFTER DELETE ON document_store
FOR EACH ROW
EXECUTE FUNCTION trg_delete_lob();
2. LOB 고아 객체 정기 점검 및 정리
주기적으로 pg_largeobject_metadata와 애플리케이션 테이블을 조인하여 참조되지 않는 LOB(고아 객체)를 찾아내고 삭제하는 유지보수 루틴을 운영하세요.
-- 고아 LOB 탐지 쿼리 (document_store 테이블 기준)
SELECT lm.oid AS orphan_lob_oid,
pg_size_pretty(SUM(octet_length(lo.data))) AS size
FROM pg_largeobject_metadata lm
JOIN pg_largeobject lo ON lo.loid = lm.oid
WHERE lm.oid NOT IN (SELECT lob_oid FROM document_store WHERE lob_oid IS NOT NULL)
GROUP BY lm.oid
ORDER BY SUM(octet_length(lo.data)) DESC;
-- 고아 LOB 일괄 삭제 (신중하게 사용!)
DO $$
DECLARE
v_loid OID;
BEGIN
FOR v_loid IN
SELECT lm.oid
FROM pg_largeobject_metadata lm
WHERE lm.oid NOT IN (
SELECT lob_oid FROM document_store WHERE lob_oid IS NOT NULL
)
LOOP
PERFORM lo_unlink(v_loid);
RAISE NOTICE 'Unlinked orphan LOB OID: %', v_loid;
END LOOP;
END;
$$;
관련 에러
0F001(invalid_locator_specification):0F000의 하위 에러로, 구체적으로 잘못된 로케이터 값이 지정된 경우 발생합니다. 존재하지 않는 LOB OID나 올바르지 않은 형식의 로케이터를 사용할 때 나타납니다.58030(io_error): LOB 데이터를 물리적으로 읽거나 쓸 때 디스크 I/O 오류가 발생하면 함께 나타날 수 있습니다.42703(undefined_column): LOB OID를 저장하는 컬럼이 잘못 참조될 때 연관될 수 있습니다.55000(object_not_in_prerequisite_state): LOB 작업 전 필요한 상태(예: 트랜잭션 활성화)가 갖춰지지 않은 경우 함께 발생하기도 합니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.