오늘은 PL/pgSQL의 이 긴 여정을 마무리하면서, 함수와 프로시저를 디버깅하고 최적화할 때 너희를 기다리고 있을 수 있는 가장 흔한 실수들을 같이 살펴볼 거야. 이런 실수들을 알면 앞으로 문제를 피할 수 있을 뿐만 아니라, 만약 버그가 생겨도 훨씬 빠르게 원인을 찾을 수 있어.
디버깅과 최적화에서 자주 나오는 실수들
1. 변수 잘못 사용하기
PL/pgSQL 함수 작성이나 디버깅할 때 제일 많이 하는 실수 중 하나가 변수 선언이나 사용을 잘못하는 거야. 예를 들어, 변수 타입을 명확하게 지정 안 하거나, 파라미터로 받은 값이 헷갈릴 때가 있어. 실제로 어떻게 되는지 예시를 볼까:
CREATE OR REPLACE FUNCTION calculate_discount(order_total NUMERIC)
RETURNS NUMERIC AS $$
DECLARE
discount_rate NUMERIC;
BEGIN
-- 어이쿠! discount_rate 변수 초기화 안 했음
RETURN order_total * discount_rate;
END;
$$ LANGUAGE plpgsql;
이 함수를 호출하면 NULL을 계산에 사용해서 에러가 나올 거야. 왜냐면 discount_rate 변수가 처음에 초기화가 안 되어 있거든.
어떻게 피할까:
- 변수 선언할 때 항상 기본값을 할당해줘:
DECLARE
discount_rate NUMERIC := 0.1; -- 기본값
RAISE NOTICE로 변수 값이 기대한 대로 들어갔는지 꼭 확인해봐:
RAISE NOTICE 'discount_rate 값: %', discount_rate;
2. 에러 로깅 안 하기
또 많이 하는 실수는 로깅을 안 하는 거야. 뭔가 잘못됐는데 함수 실행 과정을 로그로 남기지 않으면, 진짜 어두운 방에서 검은 고양이 찾는 거랑 똑같아. 특히 그 방에 고양이가 있는지도 모를 때 말이지.
로깅 없는 함수 예시:
CREATE OR REPLACE FUNCTION process_order(order_id INT)
RETURNS VOID AS $$
BEGIN
-- 주문 처리 복잡한 로직
UPDATE orders SET status = 'processed' WHERE id = order_id;
END;
$$ LANGUAGE plpgsql;
만약 order_id가 잘못 들어오면? orders 테이블에 해당 레코드가 없으면?
어떻게 피할까: RAISE NOTICE나 RAISE EXCEPTION을 써서 중요한 단계마다 로그를 남겨봐:
CREATE OR REPLACE FUNCTION process_order(order_id INT)
RETURNS VOID AS $$
BEGIN
-- 입력값 로깅
RAISE NOTICE '주문 처리 시작, ID %', order_id;
-- 복잡한 처리 로직
UPDATE orders SET status = 'processed' WHERE id = order_id;
-- 결과 로깅
RAISE NOTICE '주문 상태 업데이트 완료, ID %', order_id;
END;
$$ LANGUAGE plpgsql;
이제 로그 메시지 덕분에 어디서 에러가 나는지 쉽게 찾을 수 있어.
3. 쿼리 성능 무시하기
이건 모든 데이터베이스 개발자의 숙적이지. 함수가 보기엔 멀쩡한데, 실행 속도가 너무 느릴 때가 있어. 느린 쿼리의 주범은 인덱스가 없거나, 쿼리 실행 계획이 비효율적일 때야.
느린 쿼리 예시:
CREATE OR REPLACE FUNCTION get_large_orders()
RETURNS TABLE(order_id INT, total NUMERIC) AS $$
BEGIN
RETURN QUERY
SELECT id, total FROM orders WHERE total > 1000;
END;
$$ LANGUAGE plpgsql;
orders 테이블의 total 컬럼에 인덱스가 없으면, 테이블 전체를 다 뒤져야 해서 엄청 비효율적이야.
어떻게 피할까:
EXPLAIN ANALYZE로 쿼리 효율을 꼭 체크해봐:
EXPLAIN ANALYZE SELECT id, total FROM orders WHERE total > 1000;
- 자주 쓰는 컬럼엔 인덱스를 만들어줘:
CREATE INDEX idx_orders_total ON orders(total);
4. 트랜잭션 격리 수준 잘못 쓰기
복잡한 프로시저를 실행할 때 트랜잭션 격리 수준을 잘못 이해해서 에러가 날 수 있어. 예를 들어, 두 트랜잭션이 동시에 같은 레코드를 업데이트하려고 하면 deadlock이 생길 수 있지.
잠재적 deadlock 예시:
BEGIN;
UPDATE orders SET status = 'processed' WHERE id = 1;
-- 다른 트랜잭션의 락을 기다림
UPDATE inventory SET stock = stock - 1 WHERE product_id = 100;
COMMIT;
다른 트랜잭션이 이 작업을 반대 순서로 하면, 서로 락을 기다리다가 교착 상태가 돼버려.
어떻게 피할까:
- 작업 순서를 미리 정하고 항상 그 순서대로 실행해.
- 필요하다면
SERIALIZABLE격리 수준을 써.
5. 에러 처리 안 하기
에러 처리는 좋은 습관일 뿐만 아니라, 코드의 안정성을 보장하는 필수 도구야. 아래 코드는 에러 처리가 전혀 없어:
CREATE OR REPLACE FUNCTION add_order(order_id INT)
RETURNS VOID AS $$
BEGIN
INSERT INTO orders (id, status) VALUES (order_id, 'new');
END;
$$ LANGUAGE plpgsql;
만약 order_id가 이미 있으면 duplicate key value violates unique constraint 에러가 나와.
어떻게 피할까: 예외 처리 블록을 써봐:
CREATE OR REPLACE FUNCTION add_order(order_id INT)
RETURNS VOID AS $$
BEGIN
INSERT INTO orders (id, status) VALUES (order_id, 'new');
EXCEPTION WHEN unique_violation THEN
RAISE NOTICE 'ID % 주문이 이미 있어!', order_id;
END;
$$ LANGUAGE plpgsql;
실수 예시와 수정 방법
실수 1: 인덱스가 없어서 쿼리가 느림
상황: 특정 컬럼으로 테이블을 필터링하는데, 그 컬럼에 인덱스가 없어.
수정: 해당 컬럼에 인덱스를 만들어줘.
실수 2: 함수 로직이 너무 복잡해서 디버깅이 어려움
상황: 함수에 로직이 너무 많고, 서브함수로 나누지 않았어.
수정: 복잡한 함수는 더 작은 서브함수로 쪼개. 그러면 읽기도 쉽고, 디버깅도 쉬워져.
실수 3: RAISE EXCEPTION을 잘못 사용함
상황: 모든 에러에 RAISE EXCEPTION을 써서, 사소한 것도 다 예외로 처리해.
수정: 정보성 메시지는 RAISE NOTICE로, 진짜 심각한 경우만 RAISE EXCEPTION을 써.
RAISE NOTICE '다 괜찮아 — 함수의 현재 단계 끝났어.';
RAISE EXCEPTION '뭔가 망가졌어! 입력 파라미터 확인해봐.';
실수 예방을 위한 팁
- 로깅 추가하기: 함수의 중요한 단계마다
RAISE NOTICE로 실행 과정을 추적해. - 함수 테스트하기: 테스트 데이터를 자주 써서 함수와 프로시저를 점검해.
- 코드 가독성 유지하기: 복잡한 함수는 더 작은 서브함수나 프로시저로 나눠.
- 성능 분석하기:
EXPLAIN ANALYZE로 쿼리 효율을 꼭 확인해. - 예상치 못한 상황 대비하기: 항상 예외 처리 블록을 추가해서 에러에 대비해.
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION '예상치 못한 에러 발생: %', SQLERRM;
이렇게 하면 에러를 자신 있게 처리할 수 있고, 앞으로도 버그를 미리 막을 수 있어.
GO TO FULL VERSION