요즘 데이터베이스 시스템에서는 비즈니스 로직을 서버 쪽에서 프로시저랑 함수로 구현하는 경우가 많아. PostgreSQL을 쓸 때는 함수와 프로시저의 차이(특히 11버전부터 프로시저가 생긴 이후!)랑, 이게 트랜잭션이랑 어떻게 상호작용하는지 잘 이해해야 해.
아래에서 PostgreSQL 17 기준 공식 문서랑 최신 제한사항에 따라 트랜잭션 메커니즘, 중첩 호출, 프로시저/함수에서 부분 롤백하는 방법의 핵심만 정리해줄게.
핵심 개념: 함수 vs 프로시저
함수 (CREATE FUNCTION) — 하나의 외부 트랜잭션 안에서만 실행돼. 함수 안에서는 명시적인 트랜잭션 명령(BEGIN, COMMIT, ROLLBACK, SAVEPOINT)을 쓸 수 없어.
- 모든 변경사항은 외부 트랜잭션 레벨에서만 커밋되거나 롤백돼.
- 함수 안에서 "부분 롤백" 하려면
BEGIN ... EXCEPTION ... END를 쓰는데, 이건 함수 안에서 커밋하는 건 아니야.
프로시저 (CREATE PROCEDURE) — 서버에서 트랜잭션을 직접 제어하려고 생긴 거야(예: 부분 커밋, 단계별 롤백 등).
- 프로시저(PL/pgSQL)에서는
COMMIT,ROLLBACK,SAVEPOINT,RELEASE SAVEPOINT를 쓸 수 있어. - 중요: PL/pgSQL 프로시저에서는
ROLLBACK TO SAVEPOINT를 쓸 수 없어(문법 에러남). - 프로시저는
CALL ...SQL 명령으로만 호출할 수 있고,SELECT나 다른 함수 안에서는 못 불러.
한 함수/프로시저에서 다른 함수/프로시저 호출하는 방법?
함수는 그냥 이름으로 다른 함수를 "투명하게" 호출할 수 있어:
-- 예시: 할인 계산 함수
CREATE OR REPLACE FUNCTION calculate_discount(order_total NUMERIC)
RETURNS NUMERIC AS $$
BEGIN
IF order_total >= 100 THEN
RETURN order_total * 0.1;
ELSE
RETURN 0;
END IF;
END;
$$ LANGUAGE plpgsql;
-- 주문 처리 함수가 다른 함수를 호출함
CREATE OR REPLACE FUNCTION process_order(order_id INT, order_total NUMERIC)
RETURNS VOID AS $$
DECLARE
discount NUMERIC;
BEGIN
discount := calculate_discount(order_total);
RAISE NOTICE '할인: %', discount;
INSERT INTO orders_log (order_id, order_total, discount)
VALUES (order_id, order_total, discount);
END;
$$ LANGUAGE plpgsql;
모든 게 하나의 외부 트랜잭션 안에서 실행돼! 함수 중 하나라도 에러 나면 모든 변경사항이 롤백돼.
프로시저 호출과 중첩 트랜잭션
프로시저는 다른 프로시저 안에서 CALL ...로 호출할 수 있어(PostgreSQL 17에서는 CALL proc1() -> CALL proc2() 이런 호출 스택이 가능함). 하지만 트랜잭션 규칙은 그대로야:
- 트랜잭션 명령(
COMMIT,ROLLBACK,SAVEPOINT,RELEASE SAVEPOINT)은 프로시저의 최상위 레벨에서만 쓸 수 있어. - 트랜잭션 제어가 들어간 프로시저를 이미 활성화된 명시적 트랜잭션(예: 클라이언트에서 autocommit 없이) 안에서 호출하면
COMMIT/SAVEPOINT시도할 때 에러남.
프로시저는 함수나 익명 블록(DO ...) 안에서 실행할 수 없어. 오직 CALL 명령으로만!
트랜잭션 제어가 들어간 프로시저 예시
-- 단계별 커밋이 들어간 프로시저(autocommit 연결에서만 동작함)
CREATE PROCEDURE process_batch_orders()
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT order_id, order_total FROM incoming_orders LOOP
BEGIN
-- 각 데이터 묶음을 따로 저장
INSERT INTO orders (order_id, total) VALUES (rec.order_id, rec.order_total);
EXCEPTION WHEN OTHERS THEN
INSERT INTO order_errors(order_id, err_text) VALUES (rec.order_id, SQLERRM);
END;
COMMIT;
END LOOP;
END;
$$;
-- 프로시저 호출
CALL process_batch_orders();
각 COMMIT 이후에 자동으로 새 트랜잭션이 시작돼.
PL/pgSQL에서 부분 롤백(savepoint 비슷한 동작)
PL/pgSQL(함수, 프로시저 모두)에서는 ROLLBACK TO SAVEPOINT 명령을 지원하지 않아.
코드 일부만 롤백하려면 BEGIN ... EXCEPTION ... END 블록만 써야 해:
BEGIN
-- 어떤 작업들
BEGIN
-- 에러 날 수 있는 작업
EXCEPTION WHEN OTHERS THEN
-- 이 블록의 변경사항만 롤백됨
RAISE NOTICE '블록 안에서 롤백!';
END;
END;
프로시저에서는 SAVEPOINT랑 RELEASE SAVEPOINT도 쓸 수 있지만, ROLLBACK TO SAVEPOINT는 안 돼. 이건 단계 구분용이고, 실제 제어는 예외 처리로만 가능해.
제한사항과 베스트 프랙티스
- 함수는 무조건 원자적(atomic) 작업만: all or nothing. 뭔가 잘못되면 모든 변경사항이 롤백돼.
- 프로시저는 CALL로만: 오직 별도의 SQL 명령으로만, SELECT/함수 안에서는 안 돼. 중첩 트랜잭션 제어는 PL/pgSQL 제한을 꼭 지켜야 해.
- 부분 롤백은 EXCEPTION만 사용: 공식적으로 권장되고 지원되는 부분 롤백 방식(SAVEPOINT 비슷하게).
- 중첩 프로시저는 CALL로 호출할 때만 트랜잭션 제어 가능: 아니면 에러남.
로직과 트랜잭션 상호작용 관련 질문
함수 안에서 "중첩" 트랜잭션 만들 수 있어?
아니. 전부 하나의 트랜잭션에서 실행돼. 부분 롤백은 EXCEPTION 블록만 가능.
함수나 익명 블록에서 COMMIT/ROLLBACK 할 수 있어?
아니, 문법 에러야. 프로시저를 써야 해.
함수에서 프로시저 호출할 수 있어?
아니, 오직 CALL 명령으로만 가능. 함수/SELECT에서는 안 돼.
프로시저에서 ROLLBACK TO SAVEPOINT 할 수 있어?
안 돼! PL/pgSQL에서는 금지야. EXCEPTION 블록을 써.
GO TO FULL VERSION