서브프로그램의 다양한 기능 - 전방 선언, 자치 트랜잭션, 권한 모델 등
전방 선언
PL/SQL에서 하나의 서브프로그램이 다른 서브프로그램을 호출할 수 있기 위해서는 호출될 서브프로그램이 호출하는 서브프로그램보다 앞에서 선언되어야 한다. 자바같은 언어는 클래스 내에서 메소드의 배치 순서에 전혀 신경쓰지 않고도 프로그래밍이 가능하지만, PL/SQL은 프로시저 간 의존성에 따라 배치 순서를 정해 주지 않으면 오류가 발생한다.
--두 개의 프로시저가 상호 호출하는 경우에 컴파일 오류 발생
DECLARE
-- 프로시저 p1은 프로시저 p2를 호출한다.
PROCEDURE p1(a_num1 NUMBER) IS
BEGIN
p2(a_num1); -- p2가 뒤에 선언되므로 이 지점에서는 식별자가 유효하지 않아서 오류가 발생
END;
-- 프로시저 p2는 프로시저 p1을 호츨한다.
PROCEDURE p2(a_num2 NUMBER) IS
BEGIN
p1(a_num2); -- p1이 먼저 정의되었으므로 오류가 발생하지 않는다.
END;
BEGIN
NULL;
END;
p1은 p2를 호출하고 p2는 p1을 호출하고 있어서 p1의 입장에서는 p1보다 p2가 먼저 선언되어야 하고, p2의 입장에서는 p2보다 p1이 먼저 선언되어야 한다. PL/SQL에서는 전방 선언을 사용하여 해결한다. 서브프로그램을 선언하기 앞서, 사용할 서브프로그램을 먼저 선언만 하고, 실제 정의는 나중에 하는 방법이다.
DECLARE
-- 프로시저 p2를 전방선언하여 오류를 방지
PROCEDURE p2(a_num2 NUMBER);
-- 프로시저 p1은 프로시저 p2를 호출한다.
PROCEDURE p1(a_num1 NUMBER) IS
BEGIN
p2(a_num1); -- p2가 전방선언되었으므로 오류 발생하지 않는다.
END;
-- 프로시저 p2는 프로시저 p1을 호출한다.
PROCEDURE p2(a_num2 NUMBER) IS
BEGIN
p1(a_num2); -- p1이 먼저 정의되었으므로 오류 발생하지 않는다.
END;
BEGIN
NULL;
END;
자치 트랜잭션
자치 트랜잭션(Autonomous Transaction)은 서브프로그램을 호출하는 상위 프로그램의 트랜잭션(메인 트랜잭션)에서 서브프로그램의 트랜잭션을 분리시키는 기능이다. 자치 트랜잭션을 사용하는 서브프로그램에서는 메인 트랜잭션에서 독립된 별도의 트랜잭션이 생성되며 이렇게 생성된 자치 트랜재션은 메인 트랜잭션에 영향을 주지 않고 독립적으로 커밋, 롤백, 세이브포인트 정의가 가능하다.
자치 트랜잭션을 사용하는 대표적인 예는 로깅이다. 배치 프로그램에서 작업 중간중간에 작업 상태를 로그 테이블에 기록하는 경우를 생각해 보면 작업을 커밋하는 경우라면 로그 테이블에 저장된 작업 상태가 같이 커밋되어 문제가 없지만, 롤백하는 경우라면 로그까지도 같이 롤백되어 버려 로그 테이블에 기록한 작업 상태를 잃어버리게 된다. 작업 상태를 로그 테이블에 기록하는 주 목적이 주로 예외가 발생하거나 롤백해야 하는 경우에서 문제 원인을 파악하는 것임을 생각하면 곤란한 문제가 아닐 수 없다. 이 문제를 해결하는 방법이 자치 트랜잭션을 사용하는 것이다. 자치 트랜잭션을 사용하면 메인 트랜잭션에서 분리된 별도의 자치 트랜잭션에서 로깅을 수행한 후에 자치 트랜잭션만 커밋하기 때문에 메인 트랜잭션을 롤백하더라도 로그는 사라지지 않는다.
자치 트랜잭션을 사용하기 위해서는 프로그램 선언부에 PRAGMA AUTONOMOUS_TRANSACTION을 선언하면 된다.
자치 트랜잭션은 서브프로그램과 최상위 익명 PL/SQL 블록에서 사용가능하다.(중첩된 블록에서는 사용할 수 없다.)
REM 로깅용 테이블
CREATE TABLE log_table(
timestmp TIMESTAMP WITH TIME ZONE,
log_text VARCHAR2(4000)
);
REM 자치 트랜잭션을 활용한 로깅 프로시저
CREATE OR REPLACE PROCEDURE log_msg(a_log_text VARCHAR2)
IS
PRAGMA AUTONOMOUS_TRANSCATION; -- 자치 트랜잭션 선언
BEGIN
INSERT INTO log_table(timestmp, log_text)
VALUES (SYSTIMESTAMP, a_log_text);
COMMIT; -- 자치 트랜잭션을 COMMIT한다. 메인 트랜잭션은 COMMIT되지 않는다.
END;
/
메인 트랜잭션의 INSERT문을 롤백하기 때문에 emp 테이블의 건수는 변함이 없음에도 불구하고 로그 테이블에는 로그가 한 건 증가했음을 할 수 있다.
SCOTT> REM 초기 건수
SCOTT> SELECT (SELECT COUNT(*) FROM emp) "emp 건수"
, (SELECT COUNT(*) FROM log_table) "로그 건수"
FROM DUAL;
emp 건수 로그 건수
-------- ----------
14 0
SCOTT> REM 자치 트랜잭션 함수 log_msg 실행
SCOTT> BEGIN
INSERT INTO emp(empno, ename, sal) VALUES(9000, '홍길동', 9000);
log_msg('홍길동을 추가했습니다.');
ROLLBACK;
END;
/
PL/SQL 처리가 정상적으로 완료되었습니다.
SCOTT> REM 로깅 + 롤백 후 건수
SCOTT> SELECT (SELECT COUNT(*) FROM emp) "emp 건수"
, (SELECT COUNT(*) FROM log_table) "로그 건수"
FROM DUAL;
emp 건수 로그 건수
--------- ----------
14 1
패키지의 경우 패키지 명세에서는 자치 트랜잭션의 명시를 필요로 하지 않으며, 패키지 본체에서만 PRAGMA AUTONOMOUS_TRANSACTION을 선언하면 된다.
CREATE OR REPLACE PACKAGE pkg_emp
IS -- 패키지 명세에서는 자치 트랜잭션 선언이 없다.
PROCEDURE raise_salary(a_empno NUMBER, a_amt NUMBER);
END;
/
CREATE OR REPLACE PACKAGE BODY pkg_emp
IS
PROCEDURE raise_salary(a_empno NUMBER, a_amt NUMBER)
-- 사원의 급여를 인상하는 프로시저
IS
-- 패키지 본체의 서브프로그램에서 자치 트랜잭션을 선언
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
-- 급여를 인상
IF a_amt IS NOT NULL
THEN
UPDATE emp
SET sal = sal + a_amt
WHERE empno = a_empno;
COMMIT; -- 트랜잭션을 COMMIT한다. 메인 트랜잭션은 COMMIT되지 않는다.
END IF;
END;
END;
/
-- 익명 PL/SQL에서 자치 트랜잭션의 사용
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
v_empno NUMBER := 7788;
v_amt NUMBER := 100;
BEGIN
UPDATE emp
SET sal = sal + v_amt
WHERE empno = v_empno;
COMMIT; -- 트랜잭션을 COMMIT한다. 메인 트랜잭션은 COMMIT되지 않는다.
END;
함수 속성 DETERMINISTIC, PARALLEL_ENABLE, RESULT_CACHE
서브프로그램 유형 중 저장 함수에만 사용할 수 있는 몇 가지 추가적인 지시자들이다.
이들은 옵티마이저에게 저장함수의 특성을 알려주기 위해 사용된다. 지시자의 위치는 저장 함수의 RETURN절과 IS(AS)사이다.
CREATE OR REPLACE FUNCTION factorial(a_num PLS_INTEGER)
RETURN NUMBER
DETERMINISTIC PARALLEL_ENABLE RESULT_CACHE
IS
BEGIN
IF a_num <= 1 THEN
RETURN 1;
ELSE
RETURN a_num * factorial(a_num-1);
END IF;
END;
DETERMINISTIC
지시자 DETERMINISTIC의 사전적 의미는 '결정론적인'이다. 함수에 이 지시자를 명시하는 것은 해당 함수의 입력(입력 매개변수를 말한다,)값에 따라 출력 값이 유일하게 결정된다.(입력 매개변수 값이 동일하면 언제나 동일한 값을 반환한다는 ) 것을 컴파일러에게 알려주는 것이다. 입력이 동일하면 출력이 동일하다는 특성은 함수의 사용성에 중요한 의미를 가진다. 예를 들어 DETERMINISTIC 함수만이 함수 기반 인덱스나 머티리얼라이즈드 뷰에 사용될 수 있으며 뒤에 나오는 RESULT_CACHE에서 설명할 결과 캐싱을 가능하게 한다.
입력이 동일하면 항상 결과가 동일한 예로는 삼각 함수, TRIM함수 SUBSTR 함수등을 들 수 있다. 반대로 함수의 결과가 다르다면 입력도 다르다고 확신할 수 있다.
PARALLEL_ENABLE
지시자 PARALLEL_ENABLE은 함수가 병렬 처리에 사용될 수 있음을 컴파일러에게 알려 준다,
병렬 처리는 하나의 작업을 여러 개의 세션이 나누어서 처리하는 것을 의미한다. 병렬 처리가 가능하려면 함수의 실행 결과가 특정 세션에 의존적이거나 세션별로 다르지 않아야 한다. 이것이 보장되지 않으면 병렬 작업을 어떻게 나누느냐에 따라 처리 결과가 달라질 수 있으므로 병렬 처리가 허용되지 않는다. 병렬 처리가 불가능한 대표적 예는 패키지 변수를 참조하는 함수이다. 패키지 변수는 세션별로 독립적으로 유지되기 때문에 이를 참조하는 함수는 일관된 결과를 얻을 수 없다.
RESULT_CACHE
지시자 RESULT_CACHE는 함수의 실행 결과를 데이터베이스 서버에 캐싱하고 다음 번 실행 때 재사용할 것을 지시한다.
이는 오라클의 결과 캐싱(Result Caching)의 핵심 기능 중의 하나인 데, 결과 캐싱은 저장 함수의 결과는 물론이고 SELECT문의 결과를 서버에 캐싱하여 동일한 호출이 다시 한번 실행되면 서버에 캐시된 결과를 바로 반환하는 기능이다. 결과 캐싱을 사용하면 쿼리의 응답 시간을 크게 개션시킬 수 있다.
RESULT_CACHE 지시자를 사용하여 함수의 결과를 캐시에 저장하고 재사용할 수 있으려면 다음 조건을 만족해야 한다.
- 함수의 입력이 동일하면 출력도 항상 동일하다.(앞에 설명한 DETERMINISTIC 조건이다.)
- OUT이나 IN OUT모드의 매개변수를 가지지 않는다.
- 버전 11.2 이하에서는 호출자 권한으로 정의되지 않아야 한다.
- 파이프라인(Pipeline)된 함수가 아니다.
- LOB, REF CURSOR, Object, 레코드 타입을 입력 매개변수나 반환값으로 가지지 않는다.
권한 모델: 정의자 권한과 실행자 권한
scott 계정으로 접속하여 테이블 emp에서 사원의 수를 조회하는 저장함수 count_emp를 만드는 경우를 생각해보자. 함수는 쿼리 SELECT COUNT(*) FROM emp를 실행한다. 잘 컴파일되었다면 이 함수는 의도한 대로 동작할 것이다. 이 데이터베이스에 tiger라는 다른 계정이 만들어져 있다. tiger 계정에는 scott 계정의 테이블 emp와 구조가 완전히 동일하고 이름도 똑같은 테이블 emp가 있다. scott의 함수 count_emp의 실행권한을 tiger에게 부여했다. tiger가 scott의 함수 count_emp를 실행한다면 함수 count_emp는 scott 계정의 테이블 emp에 들어있는 사원의 수를 조회할까? 아니면 tiger 계정의 테이블 emp에 들어있는 사원의 수를 조회할까?
서브프로그램 정의자(작성자)의 권한으로 실행된다면 scott 계정의 테이블 emp에 등록된 사원 수를 조회할 것이고, 서브프로그램 실행자의 권한으로 실행되면 tiger 계정의 테이블 emp에 등록된 사원 수를 조회할 것이다.
오라클 PL/SQL은 이와 같은 상활에서 발생하는 혼란을 방지하기 위한 권한 모델을 도입했는데, 저장 서브프로그램을 작성하는 시점에서 정의자 권한(Definer's Right)과 실행자 권한(Invoker's Right) 중 하나를 선택하도록 한다.
비교 항목 | 정의자 권한 | 실행자 권한 |
권한 정의 방법 | AUTHID DEFINER(기본값) | AUTHID CURRENT_USER |
컴파일 시 권한 체크 | 정의자의 권한을 적용하여 컴파일 | |
실행 시 권한 체크 | 정의자의 권한을 적용하여 실행 | 실행자의 권한을 적용하여 실행 |
ROLE을 통해 부여받은 권한 | 미적용 | 적용 |
AUTHID를 사용한 권한 정의는 서브프로그램 정의 시 IS(또는 AS)의 바로 앞에 AUTHID DEFINER 또는 AUTHID CURRENT_USER를 명시함으로써 이루어진다. 명시하지 않을 경우 AUTHID DEFINER가 사용된다.
서브프로그램의 컴파일 시에는 정의자의 권한을 사용하여 권한을 체크한다.(서브프로그램을 다른 누가 사용할지를 사전에 알 수 없을뿐더러, 사용자가 여러 계정일 수 있으므로 다른 선택의 여지가 없다.) 실행 시에는 선택한 권한 모델에 따라서 적용되는 권한이 달라진다. 정의자 권한으로 컴파일된 서브프로그램을 실행 시에는 정의자의 권한을 사용하여 다시 한 번 권한 체크를 수행하여 정의자의 권한에 맞는 데이터베이스 오브젝트를 매핑하여 사용한다.
실행자 권한으로 컴파일된 서브프로그램의 실행 시 실행자의 권한을 이용해 권한 체크를 수행하고, 실행자의 권한에 맞는 데이터베이스 오브젝트를 매핑해 사용한다, 권한 체크에서 특이하면서도 실제로 많은 사용자들을 혼란에 빠뜨리는 사항 하나는 AUTHID절 생략 시 기본으로 사용되는 정의자 권한에서는 ROLE을 통해 부여받은 권한은 적용되지 않음으로써 발생하는 오류이다.
-- 권한 모델 예제를 위해 tiger 계정 생성과 ROLE을 통한 권한 부여
SYS> CREATE USER tiger IDENTIFIED BY scott;
사용자가 생성되었습니다.
SYS> CRANT RESOURCE, CONNECT, CREATE SYNONYM TO tiger;
권한이 부여되었습니다.
SYS> CREATE TABLE tiger.emp AS
SELECT *
FROM scott.emp
WHERE ROWNUM = 0;
테이블이 생성되었습니다.
SYS> CREATE ROLE scott_role;
롤이 생성되었습니다.
SYS> GRANT SELECT ON scott.emp TO scott_role;
권한이 부여되었습니다.
SYS> GRANT scott_role TO tiger;
권한이 부여되었습니다.
주의할 것은 테이블 scott.emp에는 14건의 데이터가 들어있고, 테이블 tiger.emp에는 데이터가 없다는 것이다.
정의자 권한
정의자 권한을 사용하여 작성된 저장 서브프로그램은 서브프로그램의 실행 시 해당 서브프로그램을 정의한 사용자(서브프로그램을 컴파일한 계정을 의미)의 권한으로 실행된다. 정의자 권한은 AUTHID DEFINER절을 사용하여 명시한다.
정의자 권한은 저장 서브프로그램의 기본 권한이므로 명시하지 않으면 정의자 권한이 사용된다.
밑의 서브프로그램은 scott 계정에서 컴파일한다.
-- 정의자 권한 함수 생성
CREATE OR REPLACE FUNCTION scott.count_auth_definer RETURN PLS_INTEGER
AUTHID DEFINER -- 정의자 권한을 사용하도록 명시
AS
v_cnt PLS_INTEGER;
BEGIN
SELECT COUNT(*)
INTO v_cnt
FROM emp;
RETURN v_cnt;
END;
-- 정의자 권한 함수 권한을 tiger에게 부여하고 SYNONYM 생성
C:>sqlplus scott/tiger
SCOTT> GRANT EXECUTE ON scott.count_auth_definer TO tiger;
권한이 부여되었습니다.
SCOTT> CONN tiger/scott
연결되었습니다.
TIGER> CREATE SYNONYM count_auth_definer FOR scott.count_auth_definer;
동의어가 생성되었습니다.
위의 스크립트를 실행하여 이 함수의 실행 권한을 tiger에게 부여하고 tiger 계정에서 이 함수에 대한 SYNONYM을 생성
-- 정의자 권한 함수의 실행 결과
C:>sqlplus scott/tiger
SCOTT> SELECT count_auth_definer() FROM dual;
COUNT_AUTH_DEFINER
------------------
14
SCOTT> conn tiger/scott
연결되었습니다.
TIGER> SELECT count_auth_definer() FROM dual;
COUNT_AUTH_DEFINER
------------------
14
scott 계정에서 실행하건 tiger 계정에서 실행하건 정의자 권한으로 생성된 서브프로그램은 이를 생성한 scott의 권한으로 실행되어 scott 계정의 테이블 emp의 데이터 건수를 조회한다는 것을 알 수 있다.
실행자 권한
실행자 권한을 사용하여 작성된 저장 서브프로그램은 서브프로그램의 시행 시 해당 서브프로그램을 실행하는 사용자의 권한으로 실행된다. 실행자 권한은 반드시 AUTHID CURRENT_USER절을 사용하여 명시해 주어야 한다,
밑의 서브프로그램도 scott계정에서 컴파일한다.
CREATE OR REPLACE FUNCTION scott.count_auth_current_user RETURN PLS_INTEGER
AUTHID CURRENT_USER
AS
v_cnt PLS_INTEGER;
BEGIN
SELECT COUNT(*)
INTO v_cnt
FROM emp;
RETURN v_cnt;
END;
C:>sqlplus scott/tiger
SCOTT> GRANT EXECUTE ON scott.count_auth_current_user TO tiger;
권한이 부여되었습니다.
SCOTT> CONN tiger/scott
연결되었습니다.
TIGER> CREATE SYNONYM count_auth_current_user FOR scott.count_auth_current_user;
동의어가 생성되었습니다.
-- 실행자 권한 함수의 시행 결과
C:>sqlplus scott/tiger
SCOTT> SELECT count_auth_current_user() FROM dual;
COUNT_AUTH_CURRENT_USER
-----------------------
14
SCOTT> CONN tiger/scott
연결되었습니다.
TIGER> SELECT count_auth_current_user() FROM dual;
COUNT_AUTH_CURRENT_USER
-----------------------
0
동일한 함수를 scott 계정으로 실행한 경우에는 scott 계정의 테이블 emp의 건수를 조회하여 14건이라는 결과를 보여 주었지만, tiger 계정으로 실행한 경우에는 tiger의 테이블 emp의 건수를 조회하여 0건이라는 결과를 보여주었다.
저장 서브프로그램을 정의한 계정에서 서브프로그램이 실행되는 경우, 정의자 권한 저장 서브프로그램과 실행자 권한 서브프로그램의 실행 결과는 동일하다.
-- 권한 모델별 결과 비교: scott 계정에서 실행
C:>sqlplus scott/tiger
SCOTT> SELECT count_auth_definer() AS 정의자권한
, count_auth_current_user AS 생성자권한
FROM DUAL;
정의자권한 생성자권한
---------- ----------
14 14
저장 서브프로그램을 정의한 계정과 다른 계정에서 서브프로그램이 실행되는 경우, 정의자 권한 저장 서브프로그램과 실행자 권한 서브프로그램의 실행 결과는 동일하지 않을 수 있다. 서브프로그램 정의 계정이 아닌 다른 계정에서 실행되는 경우에는 적용되는 권한이 서로 다르므로 저장 서브프로그램에서 참조하는 데이터베이스 오브젝트가 달라질 수 있기 때문이다.
SCOTT> CONN tiger/scott
연결되었습니다.
TIGER> SELECT count_auth_definer() AS 정의자권한
, count_auth_current_user AS 실행자권한
FROM DUAL;
정의자권한 실행자권한
--------- ----------
14 0
ROLE을 통해 부여받은 권한: SQL은 실행되는데 서브프로그램에 포함시키면 오류 발생
예제를 통해 이 문제에 대해 알아보자. 먼저 tiger에게 부여된 권한을 확인해보자.
SCOTT> SELECT COUNT(*) FROM scott.emp;
COUNT(*)
------------
14
-- ROLE을 통해 부여받은 권한: 저장 함수에 컴파일 오류 발생
CREATE OR REPLACE FUNCTION tiger.count_auth_definer_tiger RETURN PLS_INTEGER
AS
v_cnt PLS_INTEGER;
BEGIN
SELECT COUNT(*)
INTO v_cnt
FROM scott.emp;
RETURN v_cnt;
END;
이 서브프로그램을 tiger 계정에서 컴파일하면 다음같은 오류가 발생한다.
TIGER> CREATE OR REPLACE FUNCTION tiger.count_auth_definer_tiger RETURN PLS_INTEGER
AS
v_cnt PLS_INTEGER;
BEGIN
SELECT COUNT(*)
INTO v_cnt
FROM scott.emp;
RETURN v_cnt;
END;
/
경고: 컴파일 오류와 함께 함수가 생성되었습니다.
TIGER> SHOW ERROR
FUNCTION TIGER.COUNT_AUTH_DEFINER_TIGER에 대한 오류:
LINE/COL ERROR
-------- ----------------------------------------------------
5/3 PL/SQL: SQL Statement ignored
7/16 PL/SQL: ORA-00942: 테이블 또는 뷰가 존재하지 않습니다.
SQL*Plus에서는 쿼리 SELECT COUNT(*) FROM scott.emp가 문제없이 실행되었음에도 불구하고(다시 말해 조회 권한을 가지고 있음에도 불구하고) 이를 서브프로그램에 넣으면 테이블에 조회 권한으로 생성된 서브프로그램의 컴파일과 실행 시에는 ROLE을 통해 부여받은 권한을 사용하지 못한다는 데에 있다.(ROLE을 통해 실행자의 권한이 불합리하게 상승되는 보안상의 취약점을 막기 위한 오라클의 선택이다.) 이 문제를 해결하는 방법은 두 가지가 있는데 첫 번째 방법은 ROLE을 통하지 않고 tiger 계정에 scott.emp에 대한 SELECT 권한을 직접 주는 방법이다.
SCOTT> GRANT SELECT ON scott.emp TO tiger;
권한이 부여되었습니다.
SCOTT> CONN tiger/scott
연결되었습니다.
TIGER> CREATE OR REPLACE FUNCTION tiger.count_auth_definer_tiger RETURN PLS_INTEGER
AS
v_cnt PLS_INTEGER;
BEGIN
SELECT COUNT(*)
INTO v_cnt
FROM scott.emp;
RETURN v_cnt;
END;
/
함수가 생성되었습니다.
두 번째 방법은 실행자 권한으로 정의된 서브프로그램을 생성하는 방법이다. 서브프로그램을 실행자 권한으로 정의하더라도 여전히 남는 문제는 서브프로그램의 컴파일 시에는 항상 정의자 권한을 적용한다는 사실이다. 따라서 정적 SQL을 사용하는 한은 컴파일 시에 발생하는 권한 오류를 피할 수 없다. 이 문제는 동적 SQL을 사용해서 해결할 수 있다. 동적 SQL에 포함된 SQL문은 컴파일 시에 권한 검증을 하지 않으므로 컴파일을 통과할 수 있고 실행시에는 실행자의 권한으로 실행되므로 역시 권한의 문제는 없다.
SCOTT> REVOKE SELECT ON scott.emp FROM TIGER;
권한이 취소되었습니다.
SCOTT> CONN tiger/scott
연결되었습니다.
TIGER> CREATE OR REPLACE FUNCTION tiger.count_auth_current_user_tiger
RETURN PLS_INTEGER
AUTHID CURRENT_USER -- 실행자 권한으로 정의
AS
v_cnt PLS_INTEGER;
BEGIN
-- 동적SQL로 작성하여 컴파일 시 오류를 피한다.
-- 실행 시 실행자 권한이 사용되므로 scott.emp 테이블을 읽을 수 있다.
EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM scott.emp'
INTO v_cnt;
RETURN v_cnt;
END;
/
함수가 생성되었습니다.
TIGER> SELECT count_auth_current_user_tiger() FROM DUAL;
COUNT_AUTH_CURRENT_USER_TIGER()
-------------------------------
14