RainyLab
Dev · Oracle

PL/SQL 블록과 변수 — 어디까지가 한 덩어리인가

PL/SQL 을 처음 열면 BEGINEND; 밖에 안 보인다. 그런데 실제로 막히는 곳은 문법이 아니라 경계다 — 어디까지가 한 블록인지, 변수가 어디서 태어나 어디서 사라지는지, 왜 END; 를 쓰고도 아무것도 실행되지 않는지. 2018년에 오라클 9i 환경에서 쓴 노트를 옮기면서, 틀린 곳을 고치고 그때 빠뜨린 것을 채웠다.

Archive · Oracle 9i

원문은 2018년 8월, 오라클 9i 를 쓰는 현장에서 정리한 것이다. 9i 는 확장 지원까지 2010년에 끝났으니 새로 짓는 시스템의 기준은 아니다. 그럼에도 옮기는 이유는 두 가지다 — 블록 구조와 변수 범위는 9i 부터 지금까지 한 글자도 바뀌지 않았고, 아직 9i 를 돌리는 레거시에서는 최신 문법으로 쓴 예제가 그대로 컴파일 오류가 되기 때문이다.

버전에 따라 갈리는 곳은 본문에서 그때그때 표시했고, 마지막 절에 9i 이후에 생긴 것을 모아 두었다.

1. 이건 프로시저가 아니라 익명 블록이다

가장 작은 PL/SQL 프로그램은 이렇게 생겼다.

BEGIN
  NULL;
END;
/

BEGINEND; 사이가 비어 있으면 문법 오류라서 NULL; 같은 문장이 하나는 있어야 한다. 여기까지는 원문 그대로인데, 원문은 이걸 "프로시져"라고 불렀다. 그건 틀렸다.

위 코드는 익명 블록(anonymous block) 이다. 이름이 없고, 데이터베이스에 저장되지 않고, 실행되고 나면 사라진다. 프로시저는 CREATE PROCEDURE 로 만들어 데이터 딕셔너리에 남는 것이다.

익명 블록 — 보내고, 실행하고, 끝DECLARE
  v_msg VARCHAR2(30) := 'hello';
BEGIN
  DBMS_OUTPUT.PUT_LINE(v_msg);
END;
/
프로시저 — 스키마에 남는다. 다음에 이름으로 부른다CREATE OR REPLACE PROCEDURE say_hello (p_msg IN VARCHAR2) IS
BEGIN
  DBMS_OUTPUT.PUT_LINE(p_msg);
END say_hello;
/

EXEC say_hello('hello');

DECLARE 자리가 IS 로 바뀌었다는 것만 봐도 둘이 다른 물건이라는 게 보인다. 이 구분이 중요한 이유는 실용적이다 — 익명 블록을 프로시저라고 부르고 나면 "그럼 이건 어느 스키마에 저장됐지?" 에서 반드시 한 번 막히고, USER_OBJECTS 를 아무리 뒤져도 나오지 않는다.

2. 블록은 두 부분이 아니라 네 부분이다

원문은 DECLAREBEGIN…END 만 다뤘다. 실제 구조는 넷이다.

DECLARE     -- 선언부 (생략 가능)
  ...
BEGIN       -- 실행부 (필수)
  ...
EXCEPTION   -- 예외부 (생략 가능)
  ...
END;        -- 필수
/

선언할 게 없으면 DECLARE 를 아예 쓰지 않는다. EXCEPTION 절이 없으면 블록 안에서 난 예외는 잡히지 않고 그대로 바깥(호출자)으로 튄다 — 조용히 무시되는 게 아니다. 이 성질이 3절과 5절에서 두 번 문제가 된다.

3. 변수는 선언한 자리에서 이미 값을 가진다

변수는 DECLAREBEGIN 사이에서만 선언한다. 선언과 동시에 초기화할 수도 있고, 실행부에 들어가서 할당할 수도 있다.

DECLARE
  v_name   VARCHAR2(30) := 'jongmin';   -- 선언과 동시에
  v_role   VARCHAR2(30) DEFAULT 'dev';  -- := 와 DEFAULT 는 같다
  v_dept   VARCHAR2(30);                -- 초기화하지 않으면 NULL 이다
  c_limit  CONSTANT NUMBER := 100;      -- CONSTANT 는 초기화가 필수
  v_id     NUMBER NOT NULL := 0;        -- NOT NULL 도 초기화가 필수
BEGIN
  v_dept := 'platform';
  DBMS_OUTPUT.PUT_LINE(v_name || ' / ' || v_role || ' / ' || v_dept);
END;
/

원문에서 명시하지 않은 것 하나. 초기화하지 않은 변수는 쓰레기값이 아니라 NULL 이다. 그래서 v_dept := v_dept || 'x' 같은 문장은 오류가 아니라 조용히 'x' 가 되고, 숫자 변수라면 v_n := v_n + 11이 아니라 NULL 이 된다. PL/SQL 에서 가장 자주 밟는 지뢰다.

원문에서 빠뜨린 것 — %TYPE

실무 코드에서 VARCHAR2(30) 처럼 타입을 직접 박는 일은 거의 없다. 테이블 컬럼을 담을 변수라면 컬럼의 타입을 그대로 빌려 오는 게 정석이다.

DECLARE
  v_last_name  employees.last_name%TYPE;   -- 그 컬럼과 같은 타입·길이
  v_emp        employees%ROWTYPE;          -- 행 전체를 담는 레코드
BEGIN
  SELECT * INTO v_emp FROM employees WHERE employee_id = 100;
  v_last_name := v_emp.last_name;
END;
/

이유는 유지보수다. last_nameVARCHAR2(25) 에서 VARCHAR2(50) 으로 늘어나는 순간, 타입을 박아 둔 코드는 컴파일은 통과하고 실행 중에 ORA-06502: character string buffer too small 로 죽는다. %TYPE 이면 다음 컴파일에 알아서 따라간다. 9i 에도 있던 기능이다.

4. 실행 — SET SERVEROUTPUT ON 과 슬래시

Toad 라면 F5(Run as script) 나 F9(Execute statement)로 바로 돌려 볼 수 있다. SQL*Plus 라면 스크립트로 저장해 실행한다. 원문의 SQL*Plus 예제는 그대로 쓰면 동작하지 않는다.

원문 — 세 줄 모두 문제가 있다set serverout on;
@ ./printout.sql;
/
고친 것SET SERVEROUTPUT ON
@printout.sql

이유가 중요하다. SQL*Plus 는 보통 세미콜론을 문장의 끝으로 보는데, PL/SQL 블록은 안쪽에도 세미콜론이 잔뜩 들어 있다. 그래서 END; 하나만으로는 끝을 알 수 없고, 별도의 종결자인 슬래시를 한 줄에 혼자 두어 "여기까지가 한 덩어리"라고 알려 준다. 슬래시를 빼먹으면 오류도 안 나고 그냥 아무 일도 일어나지 않는다 — 초보자가 가장 오래 헤매는 지점이다.

printout.sqlDECLARE
  v_name VARCHAR2(30) := 'jongmin';
BEGIN
  DBMS_OUTPUT.PUT_LINE(v_name);
END;
/          ← 이 줄이 없으면 블록이 실행되지 않는다

그리고 DBMS_OUTPUT.PUT_LINE 은 값을 서버 쪽 버퍼에 쌓을 뿐이다. SET SERVEROUTPUT ON 은 그 버퍼를 꺼내 화면에 뿌리라는 클라이언트 설정이라, 켜지 않으면 코드는 정상 실행되는데 화면에는 아무것도 안 나온다.

9i 에서만 밟는 함정 — 버퍼 크기

9i 의 SET SERVEROUTPUT ON 은 버퍼 기본값이 2,000바이트고 최대가 1,000,000바이트다. 루프에서 로그를 찍다 넘기면 ORA-20000: ORU-10027: buffer overflow 로 죽는다. 9i 에서는 처음부터 크게 잡아 두는 게 안전하다.

SET SERVEROUTPUT ON SIZE 1000000

한 줄 길이도 9i 에서는 255바이트로 막혀 있어서 긴 문자열을 그대로 찍으면 ORU-10028: line length overflow 가 난다. 10g Release 2 부터는 SIZE UNLIMITED 가 생기고 줄 길이 제한도 32,767바이트로 풀린다.

5. 중첩과 범위 — 안에서 밖은 보이고, 밖에서 안은 안 보인다

블록은 다른 블록 안에 들어갈 수도 있고 나란히 놓일 수도 있다.

DECLARE
  foo VARCHAR2(10);
BEGIN
  foo := 'hello';

  DECLARE
    bar VARCHAR2(10);
  BEGIN
    foo := 'hi';       -- 바깥 변수는 보인다
    bar := 'world';
  END;

  -- bar := 'x';  ← 여기서는 컴파일 오류. bar 는 위 END; 에서 사라졌다

  BEGIN
    foo := 'greeting';
  END;
END;
/

변수는 선언된 블록의 END; 를 지나면 사라진다. foo 는 최상위에서 선언됐으니 안쪽 어디서나 보이지만, bar 는 자기가 선언된 블록 밖에서는 이름조차 존재하지 않는다. 안에서 밖은 보이고, 밖에서 안은 보이지 않는다 — 이 한 방향이 전부다.

같은 이름이 겹치면

안쪽에서 같은 이름을 다시 선언하면 바깥 것이 가려진다(shadowing). 원문에 없던 이야기인데, 실제로는 이쪽이 훨씬 자주 사고를 낸다. 블록에 레이블을 붙여 두면 가려진 바깥 변수를 이름으로 다시 꺼낼 수 있다.

<<outer>>
DECLARE
  v_val VARCHAR2(10) := 'outer';
BEGIN
  DECLARE
    v_val VARCHAR2(10) := 'inner';   -- 바깥 v_val 을 가린다
  BEGIN
    DBMS_OUTPUT.PUT_LINE(v_val);        -- inner
    DBMS_OUTPUT.PUT_LINE(outer.v_val);  -- outer  ← 레이블로 꺼낸다
  END;
END outer;
/

선언부에서 난 예외는 그 블록이 못 잡는다

범위 이야기의 연장인데, 알아 두면 하루를 아낀다. 예외부는 실행부만 지킨다. 선언부에서 터진 예외는 자기 블록의 EXCEPTION건너뛰고 바깥으로 튄다.

DECLARE
  v_x VARCHAR2(3) := 'abcd';   -- ORA-06502 — 여기서 터진다
BEGIN
  NULL;
EXCEPTION
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE('잡았다');  ← 실행되지 않는다
END;
/

선언부 초기화가 실패할 수 있는 코드라면 선언은 비워 두고 값 할당을 실행부로 내리는 것이 답이다. 그래야 같은 블록의 예외부가 지킨다.

6. 9i 이후에 생긴 것

9i 레거시에 손을 댈 때 최신 문법을 그대로 옮겨 붙이면 컴파일이 깨지는 항목들이다. 반대로 버전을 올릴 계획이라면 여기가 걷어낼 우회 코드의 목록이기도 하다.

기능도입9i 에서는
CONTINUE11g IF 로 나머지를 감싸거나 GOTO 레이블을 쓴다
PL/SQL 식에서 seq.NEXTVAL11g SELECT seq.NEXTVAL INTO v_id FROM dual;
SIMPLE_INTEGER11g PLS_INTEGER (9i 에도 있다)
REGEXP_LIKE 등 정규식10g LIKE·INSTR·TRANSLATE 조합
SERVEROUTPUT … SIZE UNLIMITED10gR2 최대 SIZE 1000000, 한 줄 255바이트
컴파일 경고 (PLSQL_WARNINGS)10g 없다. 죽은 코드·타입 변환을 알려 주지 않는다

%TYPE·%ROWTYPE·CONSTANT·블록 레이블·예외 전파는 전부 9i 에도 있다. 이 글에서 원문에 보탠 내용 중 버전 때문에 못 쓰는 것은 하나도 없다.

7. 한 줄로 줄이면

블록의 경계가 곧 변수의 수명이고, 슬래시가 그 경계를 서버에 알려 주는 신호다. 이름 없는 블록은 프로시저가 아니고, 선언부는 예외부의 보호를 받지 않는다. 나머지 문법은 이 두 가지를 알고 나면 대체로 짐작한 대로 동작한다.

변수를 SQL 쪽에서 다루는 이야기 — & 치환과 : 바인드 변수가 어디서 갈리는지는 다음 글에서 이어진다.