CodeGym /행동 /SQL SELF /에러 처리와 원래 상태로 되돌리기: EXCEPTION, RAISE

에러 처리와 원래 상태로 되돌리기: EXCEPTION, RAISE

SQL SELF
레벨 53, 레슨 3
사용 가능

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을 쓸 때 팁

  1. PL/pgSQL에서 ROLLBACK TO SAVEPOINT 쓰지 마! 이건 문법 에러야.
  2. 부분 롤백이 필요하면 항상 중첩된 BEGIN ... EXCEPTION ... END 블록을 써.
  3. 프로시저 안에서 COMMIT이나 ROLLBACK은 트랜잭션을 재시작하니까, 프로시저가 autocommit 모드에서 실행될 때만 써야 해!
  4. 에러는 따로 테이블에 로그로 남겨서 잘못된 행 정보를 잃지 않게 해.
  5. 비즈니스 작업이 진짜 원자적(올-오어-낫싱)이어야 하면 함수로 만들고, 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 등)에서 하는 게 더 좋아. 프로시저 안에서는 위에서 설명한 에러 처리 메커니즘을 써.

2
과제
SQL SELF, 레벨 53, 레슨 3
잠금
EXCEPTION과 반환 사용하기
EXCEPTION과 반환 사용하기
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION