Oracle ORA-01003 오류 원인과 해결 방법 완벽 가이드

ORA-01003
2026년 06월 26일 | DBMS Error 가이드

이 글에서 다루는 내용

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

ORA-01003 no statement parsed 는?

ORA-01003은 Oracle 데이터베이스에서 커서(Cursor)를 실행하거나 패치(Fetch)하려 할 때, 해당 커서에 아직 SQL 문이 파싱(Parse)되지 않은 상태에서 작업을 시도할 경우 발생하는 에러입니다. 쉽게 말해, SQL 문을 커서에 준비(Prepare/Parse)하지 않은 채로 EXECUTE나 FETCH를 호출했을 때 Oracle이 “파싱된 문장이 없다”고 알려주는 것입니다. 주로 Pro*C, OCI(Oracle Call Interface), JDBC, PL/SQL 동적 SQL 처리 코드에서 커서 라이프사이클 관리가 잘못되었을 때 자주 나타납니다.


주요 발생 원인

1. 커서 열기(OPEN) 전 FETCH 또는 EXECUTE 시도

가장 흔한 원인으로, 커서를 선언한 후 OPEN 없이 바로 FETCH를 시도하거나, 동적 SQL에서 PARSE 단계를 건너뛰고 EXECUTE를 호출하는 경우입니다. OCI나 Pro*C 환경에서는 커서 핸들을 할당받은 뒤 반드시 파싱 단계를 거쳐야 하며, 이 순서가 어긋나면 ORA-01003이 발생합니다. 특히 코드 리팩토링 과정에서 커서 초기화 로직이 누락되거나 조건 분기로 인해 파싱 단계가 스킵되는 경우가 많습니다.

2. 커서 재사용 시 파싱 없이 EXECUTE 호출

한 번 사용한 커서를 닫지 않고 재사용하거나, 닫은 커서를 다시 파싱하지 않고 바로 실행하려 할 때 발생합니다. 프로그램 내에서 동일한 커서 변수를 루프 안에서 반복 사용할 때 CLOSE 후 OPEN(또는 PARSE)을 다시 수행하지 않으면 두 번째 실행부터 이 에러가 나타날 수 있습니다. 특히 커넥션 풀링 환경에서 커서 상태가 초기화되지 않은 채 재사용되는 케이스가 실무에서 자주 보고됩니다.

3. PL/SQL 동적 SQL(DBMS_SQL)에서 잘못된 커서 핸들 사용

DBMS_SQL 패키지를 사용하는 동적 SQL 코드에서 커서 ID를 잘못 관리하거나, 이미 닫힌 커서 ID를 재사용하려 할 때 발생합니다. DBMS_SQL.OPEN_CURSOR로 커서를 열었더라도 DBMS_SQL.PARSE를 호출하지 않고 DBMS_SQL.EXECUTE를 수행하면 ORA-01003이 트리거됩니다. 복잡한 동적 SQL 로직에서 예외 처리 블록 안에서 커서가 비정상 종료된 이후 동일 커서 핸들을 재사용하는 패턴도 원인이 됩니다.


해결 방법

원인 1 해결: 커서 OPEN → PARSE → EXECUTE → FETCH 순서 준수

PL/SQL에서 동적 커서를 사용할 때는 아래와 같이 DBMS_SQL의 정확한 순서를 지켜야 합니다.

DECLARE
  v_cursor   INTEGER;
  v_result   INTEGER;
  v_empno    NUMBER;
  v_ename    VARCHAR2(50);
BEGIN
  -- 1단계: 커서 열기
  v_cursor := DBMS_SQL.OPEN_CURSOR;

  -- 2단계: 반드시 PARSE 수행 (이 단계를 빠뜨리면 ORA-01003 발생)
  DBMS_SQL.PARSE(
    v_cursor,
    'SELECT empno, ename FROM emp WHERE deptno = :deptno',
    DBMS_SQL.NATIVE
  );

  -- 3단계: 바인드 변수 설정
  DBMS_SQL.BIND_VARIABLE(v_cursor, ':deptno', 10);

  -- 4단계: 컬럼 정의
  DBMS_SQL.DEFINE_COLUMN(v_cursor, 1, v_empno);
  DBMS_SQL.DEFINE_COLUMN(v_cursor, 2, v_ename, 50);

  -- 5단계: 실행
  v_result := DBMS_SQL.EXECUTE(v_cursor);

  -- 6단계: FETCH
  LOOP
    EXIT WHEN DBMS_SQL.FETCH_ROWS(v_cursor) = 0;
    DBMS_SQL.COLUMN_VALUE(v_cursor, 1, v_empno);
    DBMS_SQL.COLUMN_VALUE(v_cursor, 2, v_ename);
    DBMS_OUTPUT.PUT_LINE('EMPNO: ' || v_empno || ', ENAME: ' || v_ename);
  END LOOP;

  -- 7단계: 커서 닫기
  DBMS_SQL.CLOSE_CURSOR(v_cursor);

EXCEPTION
  WHEN OTHERS THEN
    -- 예외 발생 시 커서 반드시 닫기
    IF DBMS_SQL.IS_OPEN(v_cursor) THEN
      DBMS_SQL.CLOSE_CURSOR(v_cursor);
    END IF;
    RAISE;
END;
/

원인 2 해결: 커서 재사용 시 올바른 재파싱

루프 내에서 커서를 재사용할 경우, 반드시 CLOSE 후 OPEN과 PARSE를 다시 수행하거나, 아래처럼 REF CURSOR를 활용하는 방식으로 전환하는 것이 안전합니다.

DECLARE
  TYPE ref_cur IS REF CURSOR;
  v_cur    ref_cur;
  v_ename  VARCHAR2(50);
  v_sal    NUMBER;

  TYPE dept_list IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
  v_depts dept_list;
BEGIN
  -- 부서 목록 초기화
  v_depts(1) := 10;
  v_depts(2) := 20;
  v_depts(3) := 30;

  FOR i IN 1 .. v_depts.COUNT LOOP
    -- 반복 시마다 커서를 새로 OPEN (재파싱 보장)
    OPEN v_cur FOR
      SELECT ename, sal
        FROM emp
       WHERE deptno = v_depts(i)
       ORDER BY sal DESC;

    DBMS_OUTPUT.PUT_LINE('=== DEPTNO: ' || v_depts(i) || ' ===');
    LOOP
      FETCH v_cur INTO v_ename, v_sal;
      EXIT WHEN v_cur%NOTFOUND;
      DBMS_OUTPUT.PUT_LINE(v_ename || ' : ' || v_sal);
    END LOOP;

    -- 반드시 닫고 다음 반복으로
    CLOSE v_cur;
  END LOOP;
END;
/

원인 3 해결: DBMS_SQL 커서 핸들 안전하게 관리

예외 처리 후 커서 핸들의 유효성을 확인하고 재파싱하는 패턴을 적용합니다.

DECLARE
  v_cursor  INTEGER := -1;  -- 초기값을 -1로 설정하여 미할당 상태 구분
  v_ret     INTEGER;
  v_sql     VARCHAR2(1000);
BEGIN
  v_sql := 'UPDATE emp SET sal = sal * 1.1 WHERE deptno = :dept';

  -- 커서가 열려있지 않을 때만 새로 열기
  IF v_cursor = -1 OR NOT DBMS_SQL.IS_OPEN(v_cursor) THEN
    v_cursor := DBMS_SQL.OPEN_CURSOR;
  END IF;

  -- 파싱 수행 (재사용 시에도 반드시 파싱)
  DBMS_SQL.PARSE(v_cursor, v_sql, DBMS_SQL.NATIVE);
  DBMS_SQL.BIND_VARIABLE(v_cursor, ':dept', 10);

  v_ret := DBMS_SQL.EXECUTE(v_cursor);
  DBMS_OUTPUT.PUT_LINE(v_ret || '건 업데이트 완료');

  DBMS_SQL.CLOSE_CURSOR(v_cursor);
  COMMIT;

EXCEPTION
  WHEN OTHERS THEN
    ROLLBACK;
    IF DBMS_SQL.IS_OPEN(v_cursor) THEN
      DBMS_SQL.CLOSE_CURSOR(v_cursor);
    END IF;
    DBMS_OUTPUT.PUT_LINE('오류 발생: ' || SQLERRM);
    RAISE;
END;
/

예방 방법

1. 커서 라이프사이클 표준 템플릿 적용

팀 내 PL/SQL 코딩 표준에 “OPEN → PARSE → BIND → DEFINE → EXECUTE → FETCH → CLOSE” 순서를 명시하고, 모든 동적 SQL 코드에 EXCEPTION 블록에서 DBMS_SQL.IS_OPEN() 체크 후 CLOSE_CURSOR를 수행하는 패턴을 코드 리뷰 체크리스트에 포함시키세요. 커서 핸들 변수는 항상 -1과 같은 명시적 초기값으로 선언하여 미초기화 상태를 쉽게 감지할 수 있도록 합니다.

2. 가능하면 DBMS_SQL 대신 Native Dynamic SQL(NDS) 사용

EXECUTE IMMEDIATEOPEN ... FOR 구문을 사용하는 Native Dynamic SQL은 커서 관리를 Oracle 엔진이 내부적으로 처리하므로, 파싱 순서 오류로 인한 ORA-01003 발생 위험이 현저히 줄어듭니다. 단, BULK COLLECT나 다중 바인드 변수가 필요한 복잡한 경우에만 DBMS_SQL을 선택적으로 사용하고, 그 외에는 NDS를 기본으로 채택하는 개발 가이드라인을 수립하는 것이 좋습니다.


관련 에러

  • ORA-01000: maximum open cursors exceeded — 커서를 닫지 않고 계속 열 때 발생하며, ORA-01003과 함께 커서 관리 문제에서 동반 발생하는 경우가 많습니다.
  • ORA-01001: invalid cursor — 유효하지 않은 커서 핸들에 접근할 때 발생하며, ORA-01003과 유사한 커서 상태 오류입니다.
  • ORA-06502: PL/SQL: numeric or value error — DBMS_SQL 사용 중 커서 ID 타입 불일치 시 함께 발생할 수 있습니다.
  • ORA-01002: fetch out of sequence — FETCH 순서가 맞지 않을 때 발생하며, 커서 라이프사이클 위반 에러군에 속합니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기