2026년 07월 16일 | DBMS Error 가이드
이 글에서 다루는 내용
54000 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
54000 program limit exceeded 는?
PostgreSQL 에러 코드 54000 program_limit_exceeded는 쿼리나 데이터베이스 작업이 PostgreSQL 내부적으로 정의된 프로그램 한계값을 초과했을 때 발생하는 에러입니다. 이 에러는 단순한 리소스 부족이 아니라, SQL 표준 또는 PostgreSQL 엔진이 허용하는 구조적·논리적 한계를 넘어섰을 때 트리거됩니다. 대표적인 상황으로는 스택 깊이(stack depth) 초과, 함수의 재귀 호출 한계 초과, 쿼리 복잡도 한계 초과 등이 있으며, 운영 환경에서 예기치 않게 서비스 장애로 이어질 수 있어 반드시 원인을 파악하고 사전에 대비해야 합니다.
주요 발생 원인
1. 스택 깊이(Stack Depth) 초과
가장 빈번하게 발생하는 원인으로, PL/pgSQL 또는 SQL 함수에서 재귀 호출(recursive call)이 너무 깊어질 때 발생합니다. PostgreSQL은 max_stack_depth 파라미터로 허용 스택 깊이를 제한하는데, 기본값은 보통 2MB이며 이를 초과하면 즉시 에러가 발생하여 세션이 중단됩니다. 잘못 설계된 재귀 함수나 무한 루프에 가까운 로직이 주된 원인이며, 특히 트리 구조나 계층형 데이터를 다루는 복잡한 함수에서 자주 나타납니다.
실제 에러 메시지 예시:
ERROR: 54000: stack depth limit exceeded
HINT: Increase the configuration parameter "max_stack_depth" (currently 2MB),
after ensuring the platform's stack depth limit is adequate.
2. 과도하게 복잡한 쿼리 구조 (너무 많은 중첩 서브쿼리 또는 조인)
하나의 쿼리 안에 수십 개의 서브쿼리, CTE, 또는 조인이 중첩될 경우 PostgreSQL 플래너(planner)와 실행 엔진이 처리할 수 있는 복잡도 한계를 초과할 수 있습니다. 특히 자동 생성된 ORM 쿼리나 BI 도구에서 생성된 복잡한 리포트 쿼리에서 이 문제가 발생하는 사례가 많습니다. 이 경우 쿼리를 분리하거나 임시 테이블을 활용해 단계별로 처리하는 방식으로 해결해야 합니다.
3. WITH RECURSIVE(재귀 CTE)의 무한 루프 또는 과도한 깊이
WITH RECURSIVE 구문을 사용할 때 종료 조건(termination condition)이 올바르지 않거나, 데이터 특성상 재귀 깊이가 지나치게 깊어지는 경우 이 에러가 발생합니다. 계층형 조직도, 카테고리 트리, 그래프 탐색 등의 쿼리에서 흔히 나타나며, 사이클(cycle)이 존재하는 데이터에서는 무한 루프로 인해 반드시 이 에러가 발생합니다. PostgreSQL 14 이상에서는 CYCLE 절을 활용하여 사이클을 감지할 수 있습니다.
해결 방법
원인 1 해결: 스택 깊이 초과 대응
먼저 현재 설정값을 확인합니다.
-- 현재 max_stack_depth 확인
SHOW max_stack_depth;
-- 세션 레벨에서 임시로 조정 (OS 스택 한계 내에서만 가능)
SET max_stack_depth = '4MB';
-- postgresql.conf에서 영구적으로 변경
-- max_stack_depth = '4MB'
-- 변경 후 reload 필요
SELECT pg_reload_conf();
재귀 함수 자체를 수정하여 깊이를 제한하는 방법도 권장됩니다.
-- 재귀 깊이를 명시적으로 제한하는 안전한 함수 예제
CREATE OR REPLACE FUNCTION safe_recursive_func(
p_id INTEGER,
p_depth INTEGER DEFAULT 0
)
RETURNS TEXT AS $$
BEGIN
-- 최대 깊이 초과 시 강제 종료
IF p_depth > 100 THEN
RAISE EXCEPTION '재귀 깊이가 한계를 초과했습니다: depth=%', p_depth;
END IF;
-- 기저 조건 (base case)
IF p_id IS NULL THEN
RETURN 'END';
END IF;
RETURN p_id::TEXT || ' -> ' || safe_recursive_func(p_id - 1, p_depth + 1);
END;
$$ LANGUAGE plpgsql;
원인 2 해결: 복잡한 쿼리 분리
복잡한 쿼리는 임시 테이블이나 CTE로 단계적으로 분리합니다.
-- 문제가 될 수 있는 과도하게 중첩된 쿼리 (나쁜 예)
SELECT *
FROM (
SELECT * FROM (
SELECT * FROM (
SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.id
-- ... 수십 개의 추가 서브쿼리 중첩
) sub1
) sub2
) sub3;
-- 권장: 임시 테이블을 활용한 단계별 처리 (좋은 예)
CREATE TEMP TABLE temp_step1 AS
SELECT o.id, o.customer_id, o.total_amount, c.name AS customer_name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at >= CURRENT_DATE - INTERVAL '30 days';
CREATE TEMP TABLE temp_step2 AS
SELECT customer_name, SUM(total_amount) AS monthly_total
FROM temp_step1
GROUP BY customer_name;
-- 최종 결과
SELECT *
FROM temp_step2
ORDER BY monthly_total DESC;
-- 사용 후 정리
DROP TABLE IF EXISTS temp_step1, temp_step2;
원인 3 해결: WITH RECURSIVE 안전하게 사용하기
-- 문제가 있는 재귀 CTE (사이클 감지 없음)
WITH RECURSIVE category_tree AS (
SELECT id, parent_id, name, 1 AS depth
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.parent_id, c.name, ct.depth + 1
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.id
-- 종료 조건 없으면 사이클 발생 시 무한 루프!
)
SELECT * FROM category_tree;
-- 안전한 재귀 CTE: 깊이 제한 + 사이클 방지
WITH RECURSIVE category_tree AS (
SELECT
id,
parent_id,
name,
1 AS depth,
ARRAY[id] AS visited_ids -- 방문한 노드 추적
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT
c.id,
c.parent_id,
c.name,
ct.depth + 1,
ct.visited_ids || c.id
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.id
WHERE
ct.depth < 50 -- 최대 깊이 제한
AND NOT (c.id = ANY(ct.visited_ids)) -- 사이클 방지
)
SELECT id, name, depth
FROM category_tree
ORDER BY depth, id;
-- PostgreSQL 14+ 에서는 CYCLE 절 사용 가능
WITH RECURSIVE category_tree AS (
SELECT id, parent_id, name
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.parent_id, c.name
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.id
)
CYCLE id SET is_cycle USING path -- 사이클 자동 감지
SELECT * FROM category_tree WHERE NOT is_cycle;
예방 방법
1. 재귀 로직에 대한 코드 리뷰 및 깊이 제한 정책 수립
모든 재귀 함수와 WITH RECURSIVE 쿼리에는 반드시 최대 깊이 제한과 사이클 감지 로직을 포함하는 것을 팀 내 코딩 표준으로 정립해야 합니다. 특히 계층형 데이터를 다루는 쿼리는 배포 전에 실제 데이터를 기반으로 최대 깊이를 측정하고, EXPLAIN ANALYZE를 통해 실행 계획을 사전에 검증하는 절차를 의무화하세요.
-- 배포 전 재귀 깊이 측정 쿼리 예시
WITH RECURSIVE depth_check AS (
SELECT id, parent_id, 1 AS depth
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.parent_id, dc.depth + 1
FROM categories c
JOIN depth_check dc ON c.parent_id = dc.id
WHERE dc.depth < 1000
)
SELECT MAX(depth) AS max_depth, COUNT(*) AS total_nodes
FROM depth_check;
2. 모니터링 및 알림 체계 구축
pg_stat_activity 뷰를 주기적으로 모니터링하여 장시간 실행 중인 쿼리를 조기에 감지하고, log_min_duration_statement 파라미터를 통해 느린 쿼리를 로그로 기록하는 체계를 구축하세요. 54000 에러 발생 시 즉시 알림이 오도록 로그 기반 알림 시스템(예: pgBadger, Datadog, Prometheus + alertmanager)을 연동하면 장애 대응 시간을 크게 단축할 수 있습니다.
-- 현재 실행 중인 장기 실행 쿼리 모니터링
SELECT
pid,
now() - pg_stat_activity.query_start AS duration,
query,
state
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) > INTERVAL '5 minutes'
AND state != 'idle'
ORDER BY duration DESC;
관련 에러
- 53000
insufficient_resources: 메모리, 디스크 등 시스템 리소스가 부족할 때 발생하며 54000과 혼동하기 쉽습니다. 54000은 구조적 한계 초과, 53000은 물리적 리소스 부족이라는 차이가 있습니다. - 53200
out_of_memory: 쿼리 실행 중 메모리 할당에 실패했을 때 발생하며,work_mem설정과 밀접한 관련이 있습니다. - 53100
disk_full: 디스크 공간 부족으로 임시 파일 생성에 실패했을 때 발생합니다. - 42P17
invalid_object_definition: 잘못된 함수 또는 뷰 정의로 인해 발생하며, 재귀 함수 작성 오류 시 함께 나타날 수 있습니다. - 57014
query_canceled: 장시간 실행되는 재귀 쿼리가statement_timeout또는 수동 취소로 중단될 때 발생하며, 54000 에러의 전조 증상으로 나타나기도 합니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.