PostgreSQL에서 에러는 여러 이유로 발생할 수 있어: 제약조건 위반(NOT NULL, UNIQUE, CHECK), 문법 에러, 중복 값 등등. 이런 에러를 잡아서 처리하지 않으면, 바깥 트랜잭션 전체가 롤백될 수 있어. 비즈니스 작업을 안정적으로 하려면 에러를 제대로 처리하는 게 진짜 중요해.
PL/pgSQL은 BEGIN ... EXCEPTION ... END 블록으로 함수나 프로시저에서 에러를 잡고 처리할 수 있는 강력한 메커니즘을 제공해. 이건 Python이나 Java의 try-catch랑 비슷하지만, PostgreSQL 트랜잭션 컨텍스트에서만 동작하는 특징이 있어.
중요한 포인트:
각 BEGIN ... EXCEPTION ... END 블록은 “가상 savepoint”처럼 동작해. 예외가 발생하면, 이 블록 안의 모든 변경사항이 자동으로 롤백돼. 이게 함수나 프로시저에서 부분 롤백을 할 수 있는 유일하게 올바른 방법이야.
EXCEPTION으로 에러 처리하는 문법
BEGIN
-- 메인 코드
EXCEPTION
WHEN 에러_타입 THEN
-- 특정 에러 처리
WHEN 다른_에러_타입 THEN
-- 다른 에러 핸들러
WHEN OTHERS THEN
-- 나머지 모든 에러 처리
END;
함수/프로시저에서의 예시
DO $$
BEGIN
RAISE NOTICE '지금 에러가 발생할 거야...';
PERFORM 1 / 0;
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE '0으로 나누기 에러 잡았다!';
END;
$$;
예시: 에러 처리와 롤백이 포함된 업데이트
주문 테이블이 있다고 해보자:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
amount NUMERIC NOT NULL,
status TEXT NOT NULL
);
주문 상태를 업데이트하고, 에러가 나면 아무것도 바꾸지 않는 함수를 만들어보자:
CREATE OR REPLACE FUNCTION update_order_status(order_id INT, new_status TEXT)
RETURNS VOID AS $$
BEGIN
BEGIN
UPDATE orders
SET status = new_status
WHERE id = order_id;
-- 에러 시뮬레이션
IF new_status = 'FAIL' THEN
RAISE EXCEPTION '에러를 시뮬레이션한다!';
END IF;
RAISE NOTICE '주문 상태 % 업데이트됨', order_id;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '주문 % 업데이트 중 에러: %', order_id, SQLERRM;
-- 이 블록 안의 모든 변경사항은 자동으로 롤백돼!
-- 필요하면 바깥에서 처리할 수 있게 에러를 다시 던져
RAISE;
END;
END;
$$ LANGUAGE plpgsql;
트랜잭션을 직접 다루는 프로시저에서 어떻게 동작하는지
프로시저(CREATE PROCEDURE)에서는 COMMIT, ROLLBACK, SAVEPOINT를 쓸 수 있지만, 절대 ROLLBACK TO SAVEPOINT는 못 써. 프로시저 안에서 일부 작업만 롤백하려면, 역시 BEGIN ... EXCEPTION ... END 블록을 써야 해:
CREATE OR REPLACE PROCEDURE pay_order(order_id INT, amount NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
-- 프로시저 전체에서 COMMIT/ROLLBACK을 쓸 수 있지만, 일부 단계만 롤백하려면:
BEGIN
UPDATE accounts
SET balance = balance - amount
WHERE id = (SELECT account_id FROM orders WHERE id = order_id);
-- 에러
IF amount < 0 THEN
RAISE EXCEPTION '금액은 음수일 수 없어!';
END IF;
UPDATE orders SET status = 'PAID' WHERE id = order_id;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '주문 % 결제 처리 중 에러: %', order_id, SQLERRM;
-- 이 블록 안의 변경사항은 자동으로 롤백돼
END;
COMMIT; -- 프로시저에서만 트랜잭션을 명시적으로 끝낼 수 있어!
END;
$$;
에러 로그 남기기
에러를 잡는 것만이 아니라, 나중에 분석할 수 있게 저장하는 것도 중요해.
CREATE TABLE error_log (
id SERIAL PRIMARY KEY,
order_id INT,
error_message TEXT,
error_time TIMESTAMP DEFAULT now()
);
함수나 프로시저에서:
EXCEPTION
WHEN OTHERS THEN
INSERT INTO error_log (order_id, error_message)
VALUES (order_id, SQLERRM);
RAISE NOTICE '에러가 로그에 기록됨: %', SQLERRM;
RAISE;
PostgreSQL 17에서의 중요한 제약사항과 팁
함수(CREATE FUNCTION ... ) 안에서는 트랜잭션 제어 명령(BEGIN, COMMIT, ROLLBACK, SAVEPOINT, ROLLBACK TO SAVEPOINT, RELEASE SAVEPOINT)을 쓸 수 없어! 모든 함수는 전체가 바깥 트랜잭션 안에서 실행돼.
프로시저(CREATE PROCEDURE ... ) 안에서는 SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK을 명시적으로 쓸 수 있어. 하지만 ROLLBACK TO SAVEPOINT — 금지! (PL/pgSQL 프로시저에서 ROLLBACK TO SAVEPOINT를 쓰면 문법 에러가 나.)
함수나 프로시저 안에서 "코드 일부만 롤백"하려면 BEGIN ... EXCEPTION ... END 블록을 써야 해. 에러가 발생하면 블록 안의 모든 게 자동으로 롤백되고, 실행이 계속될 수 있어.
프로시저(CREATE PROCEDURE)는 함수 안이나 SELECT로 실행할 수 없고, CALL ... 명령으로만 따로 실행해야 해.
PL/pgSQL에서 "부분 롤백"을 실제로 구현하려면?
진짜로 동작하는 유일한 방법은 BEGIN ... EXCEPTION ... END 블록으로 에러 처리를 하는 거야. 이 블록은 자동으로 savepoint를 만들고, 에러가 나면 블록 안의 변경사항만 롤백하고 나머지는 건드리지 않아.
EXCEPTION 예시(추천 방식):
CREATE OR REPLACE PROCEDURE demo_savepoint()
LANGUAGE plpgsql
AS $$
BEGIN
-- 어떤 코드
BEGIN
-- 여기서 에러가 나도 전체 프로시저가 아니라,
-- 이 블록만 롤백돼!
INSERT INTO demo VALUES ('잘못된 데이터'); -- 에러가 날 수도 있음
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '에러 처리됨, 블록 안의 변경사항은 취소됨';
END;
-- 여기서부터 계속 실행돼!
END;
$$;
예시: 전체 프로세스 롤백을 막는 데이터 배치 로딩
CREATE OR REPLACE PROCEDURE load_big_batch()
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT * FROM import_table LOOP
BEGIN
INSERT INTO target_table (col1, col2)
VALUES (rec.col1, rec.col2);
EXCEPTION WHEN OTHERS THEN
INSERT INTO import_errors (err_msg)
VALUES ('레코드 에러: ' || rec.col1 || ': ' || SQLERRM);
-- 이 블록 안의 변경사항은 취소됨!
END;
END LOOP;
COMMIT; -- 프로시저가 바깥에서 autocommit 모드로 실행될 때만 가능!
END;
$$;
-- 프로시저 호출
CALL load_big_batch();
참고: 만약 이미 트랜잭션이 시작된 클라이언트(예: Python에서 autocommit=False)에서 이런 프로시저를 호출하면, 프로시저 안의 COMMIT이나 SAVEPOINT는 에러를 일으킬 수 있어.
중첩 savepoint와 EXCEPTION을 쓸 때 팁
- PL/pgSQL에서 ROLLBACK TO SAVEPOINT 쓰지 마! 이건 문법 에러야.
- 부분 롤백이 필요하면 항상 중첩된 BEGIN ... EXCEPTION ... END 블록을 써.
- 프로시저 안에서 COMMIT이나 ROLLBACK은 트랜잭션을 재시작하니까, 프로시저가 autocommit 모드에서 실행될 때만 써야 해!
- 에러는 따로 테이블에 로그로 남겨서 잘못된 행 정보를 잃지 않게 해.
- 비즈니스 작업이 진짜 원자적(올-오어-낫싱)이어야 하면 함수로 만들고, COMMIT/ROLLBACK은 쓰지 마. 단계별 처리가 필요하면 프로시저로 구현해.
예시: 부분 처리로 배치 임포트하기
CREATE OR REPLACE PROCEDURE import_batch()
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT * FROM staging_table LOOP
BEGIN
INSERT INTO data_table (data)
VALUES (rec.data);
EXCEPTION
WHEN unique_violation THEN
INSERT INTO import_log (msg)
VALUES ('중복: ' || rec.data);
WHEN OTHERS THEN
INSERT INTO import_log (msg)
VALUES ('에러: ' || rec.data || ' — ' || SQLERRM);
END;
END LOOP;
END;
$$;
PostgreSQL 17에서 꼭 기억해야 할 것:
PL/pgSQL 프로시저에서는 SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK이 허용되지만, ROLLBACK TO SAVEPOINT는 안 돼.
함수나 프로시저 안에서 "부분 롤백"은 오직 중첩된 BEGIN ... EXCEPTION ... END 블록으로만 구현할 수 있어.
트랜잭션 관리는 바깥(연결 파라미터나 autocommit 등)에서 하는 게 더 좋아. 프로시저 안에서는 위에서 설명한 에러 처리 메커니즘을 써.
GO TO FULL VERSION