UDF 와 저장 프로시저 (언어, 권한 모드, 외부 액세스)
SQL, Python, Java, JavaScript, Scala 로 만드는 UDF, UDTF, 저장 프로시저의 차이, 소유자 권한과 호출자 권한, Snowflake Scripting, 외부 API 를 부르는 External Access Integration, Code Bundles 까지 정리합니다.
무엇을 언제 쓰나
| 종류 | 호출 방법 | 반환 | 쓰임 |
|---|---|---|---|
| 스칼라 UDF | SELECT f(col) | 행마다 값 하나 | 계산식, 정규화, 마스킹 보조 |
| UDTF (테이블 함수) | FROM TABLE(f(...)) | 여러 행 | 파싱, 펼치기, 행 생성 |
| 집계 UDF (UDAF) | SELECT f(col) ... GROUP BY | 그룹마다 값 하나 | 맞춤 집계 |
| 저장 프로시저 | CALL p(...) | 값 하나 또는 테이블 | DDL, DML 여러 단계, 관리 자동화 |
UDF 는 쿼리 안에서 데이터에 붙어 돌고, 프로시저는 SQL 문을 여러 번 실행하는 절차입니다. 프로시저 안에서는 CREATE, GRANT, MERGE 같은 문을 실행할 수 있지만 UDF 에서는 할 수 없습니다.
지원 언어
| 언어 | UDF | 프로시저 | 메모 |
|---|---|---|---|
| SQL | 가능 | Snowflake Scripting | 가장 가볍고 옵티마이저가 안을 들여다봄 |
| Python | 가능 (벡터화 가능) | 가능 | Anaconda 채널 패키지 |
| Java, Scala | 가능 | 가능 | JAR 를 스테이지에. Java 21 지원은 2026년 7월 Preview |
| JavaScript | 가능 | 가능 | 숫자 정밀도 주의 (double) |
SQL UDF 와 Python UDF 예
CREATE OR REPLACE FUNCTION util.normalize_phone(p STRING)
RETURNS STRING
AS $$ REGEXP_REPLACE(p, '[^0-9]', '') $$;
CREATE OR REPLACE FUNCTION util.mask_email(e STRING)
RETURNS STRING
LANGUAGE PYTHON
RUNTIME_VERSION = '3.12'
HANDLER = 'mask'
AS $$
def mask(e):
if e is None or '@' not in e:
return e
name, domain = e.split('@', 1)
return name[:1] + '***@' + domain
$$;RUNTIME_VERSION 에 넣을 수 있는 Python 버전은 계속 바뀌므로 만들기 전에 공식 문서의 지원 버전 표를 확인합니다.
Snowflake Scripting 프로시저
CREATE OR REPLACE PROCEDURE ops.purge_old_rows(days INT)
RETURNS STRING
LANGUAGE SQL
EXECUTE AS OWNER
AS
$$
DECLARE
n INT;
BEGIN
DELETE FROM raw.events WHERE loaded_at < DATEADD(day, -:days, CURRENT_TIMESTAMP());
n := SQLROWCOUNT;
RETURN 'deleted ' || n || ' rows';
EXCEPTION
WHEN OTHER THEN
RETURN 'failed: ' || SQLERRM;
END;
$$;
CALL ops.purge_old_rows(90);DECLARE,IF,FOR,LOOP,CURSOR,RESULTSET,EXCEPTION블록을 씁니다.- 바인드 변수는 SQL 문 안에서
:이름으로 참조합니다. - 2026년 3월부터 DML 오류 기록(DML error logging)이 들어와 대량 INSERT 중 일부 행이 실패해도 전체를 버리지 않고 오류 행을 따로 남길 수 있습니다.
소유자 권한과 호출자 권한
| 모드 | 실행 권한 | 맞는 상황 |
|---|---|---|
EXECUTE AS OWNER (기본) | 프로시저 소유 역할의 권한 | 사용자에게 테이블 권한을 주지 않고 정해진 작업만 허용 |
EXECUTE AS CALLER | 호출한 사용자의 현재 역할 | 관리 유틸리티, 사용자 권한 범위 안에서만 동작해야 할 때 |
EXECUTE AS RESTRICTED CALLER | 호출자 권한 중 명시적으로 허용한 것만 | 앱이나 Native App 에서 호출자 권한을 좁게 쓸 때 |
소유자 권한 프로시저 안에서는 세션 변수 변경, 일부 SHOW 와 DESCRIBE 같은 제약이 있었는데, 2026년 2월부터 소유자 권한 문맥에서도 INFORMATION_SCHEMA, SHOW, DESCRIBE 를 쓸 수 있게 되었습니다.
주의 소유자 권한 프로시저는 소유 역할의 권한을 빌려 주는 것입니다. 동적 SQL 에 사용자 입력을 그대로 이어 붙이면 권한 상승 통로가 됩니다. 식별자는
IDENTIFIER(:name)로, 값은 바인드로 넘깁니다.
외부 API 호출 (External Access Integration)
UDF 와 프로시저는 기본적으로 인터넷에 나갈 수 없습니다. 외부 호출이 필요하면 네트워크 규칙, 시크릿, 외부 액세스 통합을 만들고 함수에 붙입니다.
CREATE NETWORK RULE ops.api_rule MODE = EGRESS TYPE = HOST_PORT VALUE_LIST = ('api.example.com');
CREATE SECRET ops.api_key TYPE = GENERIC_STRING SECRET_STRING = '...';
CREATE EXTERNAL ACCESS INTEGRATION api_eai
ALLOWED_NETWORK_RULES = (ops.api_rule)
ALLOWED_AUTHENTICATION_SECRETS = (ops.api_key)
ENABLED = TRUE;
CREATE OR REPLACE FUNCTION ops.lookup(id STRING)
RETURNS VARIANT
LANGUAGE PYTHON RUNTIME_VERSION = '3.12'
HANDLER = 'run' PACKAGES = ('requests')
EXTERNAL_ACCESS_INTEGRATIONS = (api_eai)
SECRETS = ('key' = ops.api_key)
AS $$
import _snowflake, requests
def run(id):
k = _snowflake.get_generic_secret_string('key')
return requests.get(f"https://api.example.com/items/{id}", headers={"x-api-key": k}, timeout=10).json()
$$;팁 외부 API UDF 를 수백만 행에 걸면 행마다 HTTP 요청이 나갑니다. 고유 키만 먼저 뽑아 결과를 테이블에 캐시하고 조인하는 편이 빠르고 상대 API 도 덜 괴롭힙니다.
Code Bundles
2026년 9월 Preview 로 나온 Code Bundles 는 Python 이나 Java 파일을 저장 프로시저로 감싸지 않고 Snowflake 안에서 바로 실행하는 기능입니다. 2026년 8월에는 기존 Notebook Project 객체가 Code Bundles 로 이름이 바뀌었습니다. 아직 Preview 이므로 운영 파이프라인은 프로시저와 태스크 조합을 유지하는 편이 안전합니다.
비용과 성능
- UDF 실행 시간은 그 쿼리를 돌리는 웨어하우스 크레딧으로 잡힙니다. 프로시저도 안에서 실행한 쿼리가 웨어하우스를 씁니다.
- SQL UDF 는 인라인되어 옵티마이저가 최적화하지만, Python 과 Java UDF 는 블랙박스라 프루닝과 조인 최적화를 막을 수 있습니다.
- 메모리가 큰 Python UDF 는 Snowpark-optimized 웨어하우스에서 돌립니다. 자세한 것은 Snowpark 에 있습니다.
트러블슈팅
- 증상: 프로시저 안의
SHOW결과가 비어 있거나 권한 오류가 납니다. - 원인: 소유자 권한으로 실행되어 소유 역할이 그 객체를 볼 권한이 없습니다.
- 해결: 소유 역할에 필요한 권한을 주거나, 관리용 유틸리티라면
EXECUTE AS CALLER로 바꿉니다.