본문 바로가기

SQL Server/SQL Server 형상 관리

[Sequel Safe] 에러 핸들링 (上)

Stored Procedure에서의 에러 핸들링은 매우 중요한 주제입니다.

따라서, 에러를 핸들링하는 방법은  그 규칙을 정해 모든 개발자가 지키도록 하는 것이 바람직하다고 생각합니다.

에러 핸들링을 아예 하지 않는 것도 좋지 않고, 개발자마다 각자의 방식을 사용하는 것도 좋지 않습니다.

누군가가 이 역할을 맡아 주어야 하는데, DBA가 해주면 좋지만 여건이 되지 않는다면 이 글을 읽고 계신 여러분이 직접 해 보는 건 어떨까요?


에러에는 어떤 종류가 있을까?

간단히 분류해 본다면, 논리적 에러시스템 에러가 있습니다.

여기서 시스템 에러를 다시 둘로 나누면, 에러는 다음과 같이 분류할 수 있습니다.

1. 논리적 에러
   로직을 처리하는 과정에서 "예외 사항"로 분류하여 실행을 중단하거나 롤백 해야 하는 상황
   예)
      - 글 제목을 수정하려는데 글이 존재하지 않는다.
      - 상품을 구입하려는데 한정 상품이고 이미 한정 수량을 초과하였다.

2. 미리 예상 가능한 시스템 에러
   제약 조건 (Foreign Key, Primary Key, Unique, Check 등)을 위반하여 실행을 중단하거나 롤백해야 하는 상황
   예)
      - 글을 삭제하려는데 댓글이 존재하여 참조키 위반 에러가 발생하였다.
      - 회원 가입하려는데 이미 존재하는 닉네임이라 고유키 위반 에러가 발생하였다.

3. 예상하지 못한 시스템 에러


예상했던 에러가 발생했군. 이 사실을 어떻게 전달하지?

제가 권하는 방법은 이렇습니다.

"논리적 에러""미리 예상 가능한 시스템 에러"는 아래와 같이 해당 에러 상황을 미리 정의합니다.
(아래 정의를 xml파일로 만들어 어플리케이션이나 어플리케이션 서버에 배포하여 사용합니다.)

에러 번호 에러 정의
101 수정할 글이 존재하지 않습니다.
102 이미 한정 판매 수량을 초과하였습니다.
103 댓글이 존재하는 글입니다. 삭제할 수 없습니다.

에러가 발생하면 정의된 에러 번호를 리턴하도록 SP를 작성하면 됩니다.

저는 SP의 리턴 값을 에러 번호로 사용하며, 아래와 같이 정의합니다.

0 : 에러 없음
1 : Orphaned Transaction이 감지되어 롤백
2 ~ 99 : 예약
100 : 시스템 에러 발생
101 ~ : 논리적 에러 발생


여기서 잠깐...

혹시 에러 번호를 리턴할 때 Record Set 또는 Output Parameter 를 사용하시나요?

이건 좋지 못한 습관이라고 감히 말씀드리고 싶습니다.

모든 SP는 의도하던 지, 의도하지 않던 지에 상관 없이 4byte 정수를 리턴하는데... 이 자원을 사용하지 않을 이유가 없습니다.
 
게다가 SP의 Return 값은 SP안에서 SP를 호출할 때 값의 전달이라는 면에서 특히 유용합니다.


혹시 SP의 Return 값을 받아오는 방법을 모르시나요?

방법은 항상 있답니다. 방법을 찾는 게 답이지, 피하는 게 답은 아니지 않을까요? ^^


예상하지 못한 시스템 에러가 발생했어요.

이제 마지막으로 "예상하지 못한 시스템 에러"는 어떻게 처리해야 하는지 생각해 볼 차례입니다.

이런 특징이 있겠네요.

- 거의 100% 디버깅의 대상이 된다.
- 에러 내용을 미리 정의할 수 없다. (말 그대로 예상하지 못한 에러이기 때문에~)


디버깅의 대상... 즉, 빨리 찾아 고쳐야합니다.
그리고, 에러 내용을 미리 정의하지 못합니다. 그렇다면 시스템이 반환하는 에러 메세지를 반환 받아야겠군요.

그래서 이 종류의 에러는 에러 메세지를 DB에 기록하고, 에러를 Rethrow합니다.

하나씩 보겠습니다.


1. 에러 메시지를 DB에 기록한다.

SP를 작성함에 있어 기본 원칙은... "시스템 에러가 발생하지 않도록 SP를 작성한다." 입니다.

하지만, 사람이 완벽할 수는 없으므로... 아무리 잘 작성한 SP라도 언젠가 예상하지 못한 에러를 일으킬 수 있습니다.

SP 실행 중 이런 종류의 에러가 발생 했을 때 에러 내용을 DB에 기록하는 것도 나쁘지 않습니다.

사실 어플리케이션 서버에서 로그로 남기기 때문에 굳이 따로 DB에 기록을 남기지 않기도 합니다.

그럼에도 불구하고 DB에 기록하는 이유는, DBA가 SP 실행 오류에 좀 더 적극적으로 대처할 수 있기 때문입니다.

아무래도 로그 파일보다는 DB 조회가 접근성 면에서 유리하니까요.

저는 미리 ErrorLogs라는 테이블을 모든 DB에 생성하고, P_AddErrorLog라는 SP도 모든 DB에 만들어 두었습니다.
(오른쪽 카테고리 메뉴에 있는 Sequel Safe를 설치하셨다면... 여러분의 DB에도 이미 이 개체들이 존재하고 있습니다.)


2. 에러 Rethorw

에러는 던지고(throw) 잡는다고(catch) 표현합니다.

Rethrow란... 말 그대로 에러를 "다시 던진다"라는 뜻입니다.

TRY블럭에 작성된 SQL문이... 실행 도중 에러를 던지면 CATCH블럭이 에러를 잡아 처리합니다.

문제는 CATCH블럭이 에러를 잡으면, 그 에러는 어플리케이션 서버에 전달되지 않는다는 접입니다.

이건 어플리케이션 서버 개발자 입장에선 좀 곤란할 수 있습니다. DB에러가 발생했는데 에러에 대한 자세한 내용을 던져주지 않으니 말입니다.

이런 이유로 에러를 다시 던져야할 필요가 생깁니다.


SP에서 SP를 호출합니다. 에러 핸들링과 관련해 신경 쓸 사항은?

SP에서 SP를 호출할 수 있습니다. 이런 경우 "SP가 중첩되었다" 라고 말합니다.

그런데 우리는 중첩된 SP에서의 에러 핸들링에도 신경을 써야합니다.

예를 들어보겠습니다.

1. A라는 SP에서 B라는 SP를 호출하도록 되어있습니다.
2. A가 호출됩니다.
3. A가 실행되는 과정에서 B가 호출됩니다.
4. B가 777을 리턴합니다.
5. A는 B가 0외의 값을 리턴했으므로 777을 리턴하고 강제 종료합니다.

바로 우리가 원하는 프로세스입니다. 그리고 이런 프로세스는 SP의 중첩 레벨에 상관없이 잘 동작해야합니다.



지금까지 에러 핸들링이 어떤 문제를 어떻게 해결해 주어야 하는가에 대해 얘기해 봤습니다.

다음 포스트에서는 실제 코드를 살펴보도록 하겠습니다.