예외처리 원칙

이전에 작성했던 예외 관련 글이 있었지만, 시간이 지나고 경험을 통해 얻은 생각들을 다시 한번 명확하게 정리하고자 한다.

예외 처리는 크게 두 가지 방식으로 나눌 수 있다.

  1. if 문을 통한 조건 분기
  2. try-catch를 이용한 예외 처리

결론부터 말하자면, 필요에 따라 두 가지 모두 적절하게 사용해야 한다.


기본 원칙

  1. 내부 데이터는 항상 유효하게 유지한다.
    • null 값의 사용을 최대한 지양한다.
  2. 의도적인 크래시가 목적이 아니라면, 예외(throw)를 발생시키지 않는다.

  3. 라이브 환경에서 발생 가능한 케이스는 if문으로 분기 처리한다.
    • 예측 가능한 오류는 예외가 아닌 흐름 제어로 처리한다.
  4. 작업 중 가정한 “절대 발생해서는 안 되는” 케이스에는 ASSERT를 사용한다.
    • ASSERT가 실패하면, 로직에 버그가 있다는 신호이며 의도적으로 크래시를 발생시켜 문제를 즉시 인지하도록 한다.
  5. 기본적인 로직은 항상 Happy Path를 기준으로 작성한다.

  6. 입력값에 따라 성공 또는 실패할 수 있는 함수는 Try 접두사를 붙여 명확하게 표현한다.
    • 예: bool TryGetValue(Key key, out Value value)
  7. 로직적으로 발생 가능한 케이스는 if문으로 처리한다.
    • 예: 사용자가 자료구조에 없는 데이터를 요청하는 경우
  8. try-catch는 예외가 발생할 수 있는 최소한의 범위에만 사용한다.
    • 외부 라이브러리 호출 등 제어할 수 없는 코드에서 발생하는 예외를 처리하는 데 집중한다.

대원칙

  • 개발자는 발생 가능한 모든 케이스를 최대한 고려하고, 각 상황에 맞는 처리 방식을 결정해야 한다.
  • 때로는 “아무것도 처리하지 않는 것”도 하나의 유효한 처리 방식일 수 있다.

예시 1: 복구 불가능한 시스템 오류

동적 메모리 할당 실패와 같이 프로세스 실행을 더 이상 신뢰할 수 없는 상황이 발생했다고 가정해보자.

  • iftry-catch로 이 상황을 처리하더라도, 근본적인 문제가 해결되지 않았기 때문에 결국 다른 곳에서 크래시가 발생하게 된다.
  • 이런 경우, 오류를 처리하지 않고 즉시 크래시를 발생시키는 것이 오히려 가장 빠른 원인 파악 방법일 수 있다. ASSERT조차 필요 없는 명백한 실패 상황이다.

예시 2: 서버 초기화 실패

서버가 시작될 때 데이터를 로드하는 과정에서 실패했다고 가정해보자.

  • try-catch로 예외를 잡아 정상적으로 return 처리를 하면, 서버는 일단 실행될 수 있다. 하지만 데이터가 없기 때문에 아무 기능도 수행하지 못한다.
  • 또한, 서버가 조용히 종료되면 구체적인 원인 파악을 위해 원격 디버깅과 같은 복잡한 과정을 거쳐야 할 수 있다.
  • 차라리 크래시를 발생시켜 콜스택(Call Stack)을 확보하는 것이 원인을 가장 빠르고 명확하게 파악하는 방법이다.

ASSERT를 적극적으로 사용해야 하는 이유

코드 작성 시점에 “나의 가정이 맞다”는 것을 ASSERT로 명시하면 다음과 같은 이점을 얻을 수 있다.

  • 버그 조기 발견: 로컬 테스트 단계에서 잘못된 가정을 즉시 발견하고 수정할 수 있다.
  • 코드 가독성 향상: 코드를 읽는 사람에게 “이 변수는 절대 null이 아니어야 한다” 또는 “이 함수는 특정 상태에서만 호출되어야 한다”와 같은 개발자의 의도를 명확하게 전달한다.
  • 문제 확산 방지: 문제가 있는 로직이 억지로 실행되는 것을 막아, 발견이 늦어지고 영향 범위가 눈덩이처럼 불어나는 것을 방지한다.
  • 빠른 이슈 대응: ASSERT로 인한 크래시는 발생 즉시 인지되고 수정 또한 빠르게 이루어질 수 있다.
    • 만약 크래시가 너무 치명적이라면, ASSERT 발생 시 콜스택과 덤프를 남기고, 이를 즉시 인지할 수 있는 시스템을 갖춘 후 예외 처리를 통해 다른 로직으로 우회하는 것도 고려해볼 수 있다.

try-catch는 언제 사용해야 하는가?

  • 스스로 작성한 코드 로직에서는 예외가 발생하지 않도록 설계해야 한다. 성공/실패 여부가 필요한 함수는 int.TryParse처럼 결과를 반환하도록 하고, 호출부에서 if문으로 처리하는 것이 바람직하다.
  • 이렇게 설계하면 try-catch는 대부분 제어할 수 없는 외부 코드(표준 라이브러리, 서드파티 라이브러리 등)를 호출하는 경우로 사용이 제한된다.

예시: 파일 읽기

  • 파일을 읽기 전, 파일이 존재하는지 if문으로 확인하는 것은 당연한 기본 처리다.
  • 하지만 그럼에도 불구하고 파일을 읽는 과정에서 디스크 오류 등 예측하지 못한 예외가 발생할 수 있다.
  • 따라서 if문으로 기본적인 체크를 하고, try-catch로 예상치 못한 예외를 감싸는 두 가지 처리가 모두 필요하다.

정리

작업시 순서는 이렇게 된다.

  1. Debug.Assert를 고려
  2. -> Assert가 아닌 라이브 중 발생가능한 케이스 if문으로 처리 고려
  3. –> throw 발생 가능한 로직은 try catch로 감싼다

모든 코드 로직에는 작성자의 의도가 명확히 담겨야 하며, “왜 이렇게 코드를 작성했는가”에 대해 스스로 설명할 수 있어야 한다.

Posted 2025-10-01