PostgreSQL 오류 처리

PostgreSQLBeginner
지금 연습하기

소개

이 랩에서는 PostgreSQL 의 절차 언어인 PL/pgSQL을 사용하여 오류 처리 기법을 탐색합니다. 오류가 발생하여 중단되는 대신 잠재적인 오류를 우아하게 처리할 수 있는 함수를 만드는 방법을 배우게 됩니다. 이 랩에서는 정보 메시지를 발생시키고, "0 으로 나누기"와 같은 특정 예외를 잡고, 나중에 검토할 수 있도록 데이터베이스 테이블에 오류 세부 정보를 기록하는 방법을 다룹니다. 이를 통해 더 강력하고 안정적인 데이터베이스 함수를 구축하는 방법을 이해하게 될 것입니다.

이것은 학습 및 연습을 돕기 위한 단계별 지침을 제공하는 Guided Lab 입니다. 각 단계를 완료하고 실습 경험을 얻으려면 지침을 주의 깊게 따르십시오. 과거 데이터에 따르면 이 랩은 초급 수준이며 90%의 완료율을 보입니다. 학습자로부터 100%의 긍정적인 평가율을 받았습니다.

알림 (Notice) 및 경고 (Warning) 발생시키기

이 단계에서는 PL/pgSQL 함수에서 RAISE 문을 사용하는 방법을 배웁니다. RAISE 문은 함수의 실행을 중단하지 않고 정보 메시지, 경고 또는 디버깅 출력을 제공하는 데 유용합니다.

먼저 postgres 사용자로 PostgreSQL 대화형 터미널인 psql에 연결합니다. 이 랩의 모든 후속 SQL 명령은 이 터미널 내에서 실행됩니다.

sudo -u postgres psql

이제 postgres=#와 같이 보이는 psql 프롬프트가 표시됩니다.

이름을 입력으로 받고 NOTICE를 발생시키는 greet라는 함수를 만들어 보겠습니다. 알림은 우선순위가 낮은 메시지입니다.

psql 셸에서 다음 SQL 명령을 실행합니다.

CREATE OR REPLACE FUNCTION greet(name TEXT)
RETURNS TEXT AS $$
BEGIN
  RAISE NOTICE 'Greeting function called with name: %', name;
  RETURN 'Hello, ' || name;
END;
$$ LANGUAGE plpgsql;

이 함수는 name 인수를 받습니다. RAISE NOTICE '...', name; 줄은 알림 메시지를 생성합니다. %name 변수의 값으로 대체되는 자리 표시자입니다.

이제 함수를 호출하여 작동 방식을 확인합니다.

SELECT greet('LabEx');

출력에는 알림 메시지와 함수의 반환 값이 모두 표시됩니다.

NOTICE:  Greeting function called with name: LabEx
   greet
-----------
 Hello, LabEx
(1 row)

더 높은 심각도 수준인 WARNING으로 메시지를 발생시킬 수도 있습니다. 입력 이름이 비어 있으면 경고를 발생시키도록 함수를 수정해 보겠습니다.

CREATE OR REPLACE FUNCTION greet(name TEXT)
RETURNS TEXT AS $$
BEGIN
  IF name IS NULL OR name = '' THEN
    RAISE WARNING 'Input name is empty or NULL.';
    RETURN 'Hello, stranger';
  ELSE
    RAISE NOTICE 'Greeting function called with name: %', name;
    RETURN 'Hello, ' || name;
  END IF;
END;
$$ LANGUAGE plpgsql;

이제 빈 문자열로 함수를 호출하여 경고를 트리거합니다.

SELECT greet('');

이번에는 NOTICE 대신 WARNING 메시지가 표시됩니다.

WARNING:  Input name is empty or NULL.
      greet
------------------
 Hello, stranger
(1 row)

함수 내에서 다양한 유형의 메시지를 생성하기 위해 RAISE를 성공적으로 사용했습니다.

예외 (Exception) 발생 및 처리

이 단계에서는 런타임 오류, 즉 예외를 처리하는 방법을 배웁니다. 처리되지 않은 예외는 함수를 즉시 종료시킵니다. 먼저 실패하는 함수를 생성한 다음, 예외를 우아하게 처리하도록 수정합니다.

정수 나눗셈을 수행하는 simple_divide 함수를 만들어 보겠습니다. 이 함수에는 잠재적인 결함이 있습니다. 0 으로 나누려고 하면 충돌합니다.

CREATE OR REPLACE FUNCTION simple_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
END;
$$ LANGUAGE plpgsql;

먼저 유효한 입력으로 함수를 호출하여 올바르게 작동하는지 확인합니다.

SELECT simple_divide(10, 2);

출력은 나눗셈 결과입니다.

 simple_divide
---------------
             5
(1 row)

이제 0 으로 나누는 인수로 함수를 호출하여 예외를 발생시킵니다.

SELECT simple_divide(10, 0);

이 명령은 실패하고 오류 메시지를 반환합니다. 함수 실행이 중단됩니다.

ERROR:  division by zero
CONTEXT:  PL/pgSQL function simple_divide(integer,integer) line 3 at RETURN

이것은 처리되지 않은 예외입니다. 함수가 충돌하는 것을 방지하려면 EXCEPTION 블록을 사용할 수 있습니다. 오류 처리를 포함하는 새 함수 safe_divide를 만들어 보겠습니다.

CREATE OR REPLACE FUNCTION safe_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
EXCEPTION
  WHEN division_by_zero THEN
    RAISE NOTICE 'Error: Cannot divide by zero.';
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

EXCEPTION 블록은 다음과 같이 작동합니다.

  • BEGIN...END;: 코드 블록을 정의합니다.
  • EXCEPTION: 이 키워드는 예외 처리 섹션을 시작합니다.
  • WHEN division_by_zero THEN: division_by_zero 오류가 발생할 때만 다음 코드가 실행되도록 지정합니다.
  • 핸들러 안의 코드는 알림을 발생시키고 충돌하는 대신 NULL을 반환합니다.

이제 새 safe_divide 함수를 0 으로 나누는 인수로 호출합니다.

SELECT safe_divide(10, 0);

이번에는 함수가 충돌하지 않습니다. 예외를 처리하고 사용자 지정 알림을 표시하며 NULL을 반환합니다.

NOTICE:  Error: Cannot divide by zero.
 safe_divide
-------------

(1 row)

특정 예외를 성공적으로 처리하고 함수의 출력을 제어했습니다.

테이블에 오류 로깅하기

예외를 처리하는 것은 좋지만, 감사 및 디버깅을 위해서는 오류를 테이블과 같이 영구적인 위치에 로깅하는 것이 더 나은 경우가 많습니다. 이 단계에서는 랩 설정 중에 생성된 error_log 테이블에 오류 세부 정보를 작성하도록 safe_divide 함수를 수정합니다.

먼저 psql에서 \d 명령을 사용하여 error_log 테이블의 구조를 살펴보겠습니다.

\d error_log

테이블의 열을 볼 수 있으며, 여기에는 자동 증가하는 id, timestamp, 오류 message, 그리고 function_name이 포함됩니다.

                                              Table "public.error_log"
    Column     |            Type             | Collation | Nullable |                     Default
---------------+-----------------------------+-----------+----------+----------------------------------------------------
 id            | integer                     |           | not null | nextval('error_log_id_seq'::regclass)
 timestamp     | timestamp without time zone |           |          | (now() AT TIME ZONE 'utc'::text)
 message       | text                        |           |          |
 function_name | text                        |           |          |
Indexes:
    "error_log_pkey" PRIMARY KEY, btree (id)

이제 safe_divide 함수를 수정해 보겠습니다. EXCEPTION 블록에서 반환하기 전에 오류를 로깅하기 위해 INSERT 문을 추가합니다.

CREATE OR REPLACE FUNCTION safe_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
EXCEPTION
  WHEN division_by_zero THEN
    INSERT INTO error_log (message, function_name)
    VALUES ('Division by zero occurred!', 'safe_divide');

    RAISE NOTICE 'Error: Cannot divide by zero. Details logged.';
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

이제 오류와 새로운 로깅 로직을 트리거하기 위해 함수를 다시 호출합니다.

SELECT safe_divide(10, 0);

이번에도 함수는 이전과 같이 NULL을 반환하지만, 데이터베이스 쓰기도 수행했습니다. 이를 확인하기 위해 error_log 테이블을 쿼리하여 새 항목을 확인합니다.

SELECT function_name, message FROM error_log;

출력에는 로깅된 오류가 표시되어야 합니다.

 function_name |          message
---------------+----------------------------
 safe_divide   | Division by zero occurred!
(1 row)

이를 통해 오류 처리 로직이 나중에 분석할 수 있도록 오류 정보를 영구적으로 저장할 수 있게 되었음을 확인할 수 있습니다.

OTHERS 를 사용한 일반 예외 처리

지금까지 division_by_zero라는 매우 구체적인 오류를 처리했습니다. 하지만 다른 예상치 못한 오류는 어떻게 처리해야 할까요? PL/pgSQL은 더 구체적인 WHEN 절에서 처리되지 않은 모든 오류를 잡기 위한 범용 OTHERS 예외 핸들러를 제공합니다.

이 단계에서는 safe_divide 함수를 개선하여 발생 가능한 모든 예외를 처리합니다. 또한 SQLSTATE (SQL 오류 코드) 와 SQLERRM (SQL 오류 메시지) 이라는 두 개의 특수 변수를 사용하여 더 자세한 정보를 로깅합니다.

safe_divide 함수를 마지막으로 수정해 보겠습니다.

CREATE OR REPLACE FUNCTION safe_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
EXCEPTION
  WHEN division_by_zero THEN
    INSERT INTO error_log (message, function_name)
    VALUES ('Division by zero occurred!', 'safe_divide');

    RAISE NOTICE 'Error: Cannot divide by zero. Details logged.';
    RETURN NULL;

  WHEN OTHERS THEN
    INSERT INTO error_log (message, function_name)
    VALUES ('An unexpected error occurred: ' || SQLSTATE || ' - ' || SQLERRM, 'safe_divide');

    RAISE NOTICE 'An unexpected error occurred. Details logged.';
    RETURN -1;
END;
$$ LANGUAGE plpgsql;

새로운 WHEN OTHERS 블록은 다른 모든 오류를 처리합니다. SQLSTATESQLERRM 변수를 사용하여 자세한 메시지를 구성하고 로그에 삽입합니다. 그런 다음 일반 오류가 발생했음을 나타내기 위해 -1을 반환합니다.

현재 함수는 division_by_zero 오류만 발생시킬 수 있지만, 이 구조는 향후 수정이나 예상치 못한 입력에 대해 훨씬 더 강력합니다. 예를 들어, 입력 유형이 오버플로우될 수 있는 유형으로 변경되면 OTHERS 블록은 numeric_value_out_of_range 오류를 처리합니다.

깨끗한 테스트를 위해 로그 테이블을 먼저 비우고 division_by_zero 경로를 다시 테스트해 보겠습니다.

TRUNCATE error_log;

이제 함수를 호출합니다.

SELECT safe_divide(10, 0);

마지막으로 로그를 확인합니다. 첫 번째 WHEN 블록이 올바르게 선택되었음을 확인하는 특정 "Division by zero" 메시지가 포함되어야 합니다.

SELECT message FROM error_log;
          message
----------------------------
 Division by zero occurred!
(1 row)

이제 특정하고 예상되는 오류와 일반적이고 예상치 못한 오류를 모두 처리하는 탄력적인 함수를 구축했습니다.

요약

이 랩에서는 PostgreSQL 의 PL/pgSQL에서 오류 처리의 기본 사항을 배웠습니다. 디버깅 및 정보 제공 목적으로 NOTICEWARNING 메시지를 발행하기 위해 RAISE 문을 사용하는 것으로 시작했습니다. 그런 다음 실패할 수 있는 함수를 만들고 특정 division_by_zero 오류를 잡기 위해 BEGIN...EXCEPTION...END 블록을 구현하여 런타임 예외를 처리했습니다. 이를 바탕으로 오류 세부 정보를 데이터베이스 테이블에 로깅하도록 함수를 개선하여 감사 기록을 영구적으로 보존했습니다. 마지막으로 일반 WHEN OTHERS 핸들러를 추가하여 예상치 못한 오류를 처리하도록 함수를 더욱 강력하게 만들어 다양한 조건에서 함수가 우아하게 실패할 수 있도록 보장했습니다.