4. 오류
오류는 불가피하다. 모든 것이 잘못될 수 있고 잘못될 것이기 때문에 오류 사례를 신중하게 생각하지 않으면 견고하고 신뢰성 높은 코드를 작성할 수 없다. 오류에 대해 생각할 때, 소프트웨어가 작동을 계속할 수 있는 오류와 작동을 계속할 합리적인 방법이 없는 오류로 구분하는 것이 유용할 때가 많다.
복구 가능성
소프트웨어에 대해 생각할 때, 특정 오류가 발생한 경우 복구할 수 있는 현실적인 방법이 있는지 생각해야 하는 경우가 많다.
1) 복구 가능한 오류
많은 소프트웨어 오류는 치명적이지 않으며, 오류가 발생하더라도 사용자는 알아채지 못하도록 적절하게 처리한다면 작동을 계속할 수 있는 합리적인 방법이 있다. (네트워크 오류의 경우 다시 연결될 때까지 기다렸다가 다시 시도)
2) 복구할 수 없는 오류
오류가 발생하고 시스템이 오류를 복구할 수 있는 합리적인 방법이 없을 때가 있다. 이러한 현상은 프로그래밍 오류 때문에 발생할 때가 많다.
오류를 복구할 수 있는 방법이 없다면, 유일하게 코드가 할 수 있는 합리적인 방법은 피해를 최소화하고 개발자가 문제를 발견하고 해결할 가능성을 최대화하는 것이다.
3) 호출하는 쪽에서만 오류 복구 가능 여부를 알 때가 많다
대부분의 오류는 한 코드가 다른 코드를 호출할 때 발생한다. 따라서 다음과 같은 사항을 신중하게 고려해야 한다.
- 오류로부터 복구하기를 호출하는 쪽에서 원하는가?
- 만약 그렇다면 오류를 처리할 필요가 있다는 것을 호출하는 쪽에서는 어떻게 알 수 있을까?
코드는 여러 곳에서 호출되며, 간결한 추상화 계층을 만들고자 한다면 일반적으로 코드의 잠재적 호출자에 대한 가정을 가능한 한 하지 않는 것이 좋다. 호출하는 쪽에서 오류로부터 복구하기를 원할 것이라고 판단하는 것은 좋은 일이지만, 오류가 발생할 수 있다는 것조차 인식하지 못한다면 그것을 제대로 처리하지 못할 것이다.
4) 호출하는 쪽에서 복구하고자 하는 오류에 대해 인지하도록 하라
다른 코드가 자신의 코드를 호출할 경우, 호출 시 오류가 발생한다는 것을 사전에 알 수 있는 실질적인 방법이 없는 경우가 많다. 함수의 작성자는 오류가 발생할 수 있다는 가능성을 호출하는 쪽에서 확실하게 인지하도록 해야 한다. 그렇지 않으면 함수를 호출하는 개발자가 오류를 처리하는 코드를 작성하지 않은 상태에서 오류가 발생하는 경우 개발자의 예상과는 다른 결과를 초래할 수 있다.
견고성 vs 실패
오류가 발생할 때 다음 중 하나를 선택해야 한다.
- 실패. 더 높은 코드 계층이 오류를 처리하게 하거나 전체 프로그램의 작동을 멈춤
- 오류를 처리하고 계속 진행한다.
오류가 있더라도 처리하고 계속 진행하면 더 견고한 코드라고 볼 수 있지만, 오류가 감지되지 않고 이상한 일이 발생하기 시작한다는 의미도 될 수 있다.
1) 신속하게 실패하라
가능한 한 문제의 실제 발생 지점으로부터 가까운 곳에서 오류를 나타내야 한다.
복구 할 수 있는 오류의 경우 호출하는 쪽에서 오류로부터 훌륭하고 안전하게 복구할 수 있는 기회를 최대한으로 제공하고, 복구할 수 없는 오류의 경우 개발자가 문제를 신속하게 파악하고 해결할 수 있는 기회를 최대한 제공해야 한다.
2) 요란하게 실패하라
프로그램이 복구할 수 없는 오류가 발생하면 프로그래밍 오류나 개발자의 실수로 인한 버그일 가능성이 크다.
오류가 발생하는데도 아무도 모르는 상황을 막고자 오류를 요란하게 발생시키는 것이 필요하다.(프로그램 중단 등)
3) 복구 가능성의 범위
복구할 수 있는 범위는 달라질 수 있다.
한 번의 잘못된 요청으로 인해 서버의 전체 동작이 멈추는 것은 바람직하지 않다. 하지만 오류를 알아차리지 못한 채 시스템이 계속 동작하지 않도록 하는 것 또한 중요하기 때문에 코드가 요란하게 실패해야 한다. 이 두 목표는 양립하지 못할 때가 많다. 가장 요란스럽게 실패하는 것은 분명 소프트웨어를 견고하지 못하게 만든다.
해결책은 프로그래밍 오류가 발견되면 개발자가 이를 알아차릴 수 있도록 프로그래밍 오류를 기록하고 모니터링하는 것이다. 하지만 오류를 전달하는 대신 기록만 하면 오류가 숨겨져 문제가 발생할 수 있다.
4) 오류를 숨기지 않음
어떤 때는 실수를 숨기고 아무 일도 없었던 것처럼 동작하도록 코드를 작성하고 싶은 마음이 생길 수 있다. 오류를 숨기면 코드는 단순해지겠지만 다음과 같은 문제를 일으킨다.
- 호출하는 쪽에서 복구하고자 할 수도 있는 오류를 숨기면, 호출하는 쪽에서 오류로부터 복구할 수 있는 기회를 없애는 것이다.
- 복구할 수 없는 오류를 숨기면 프로그래밍 오류가 감춰진다.
- 이 두 경우 모두 에러가 발생하면 일반적으로 호출하는 쪽에서 예측한 대로 코드가 실행되지 않는다는 것을 의미한다.
오류가 발생했음을 숨기는 방법은 다음과 같다.
- 기본값 반환
- 널 객체 패턴
- 아무것도 하지 않음 (catch(e) { 작성 x })
오류 전달 방식
오류를 알리는 방법은 크게 두 가지 종류로 나뉩니다.
명시적 방식
명시적 방식은 코드를 직접 호출한 쪽에서 오류가 발생할 수 있음을 인지할 수 있도록 합니다. 이 방식은 검사 예외, 널 반환 유형(널 안정성의 경우), 옵셔널 반환 유형, 리절트 반환 유형, 아웃컴 반환 유형, 스위프트 오류 등을 예시로 들 수 있습니다.
암시적 방식
암시적 방식은 코드를 호출하는 쪽에 오류를 알리지만, 호출하는 쪽에서 그 오류를 신경 쓰지 않아도 된다는 특징을 갖습니다. 이 방식은 비검사 예외, 매직값 반환(피해야 함), 프로미스 또는 퓨쳐, 어서션, 체크, 패닉 등을 예시로 들 수 있습니다.
호출하는 쪽에서 복구하기를 원할 수도 있는 오류의 전달
비검사 예외와 명시적 오류 전달 기법 중 어느 것을 사용해야 하는지에 대한 논쟁이 있습니다.
비검사 예외를 사용해야 하는 주장
비검사 예외를 발생시키면 코드 구조를 개선할 수 있다는 주장이 있습니다. 오류가 높은 계층까지 거슬러 올라가면서 전달되고, 그 사이에 있는 코드는 오류 처리를 할 필요가 없습니다. 그러나 이 방식을 사용하면 오류를 숨길 수 있기 때문에 주의해야 합니다.
명시적 기법을 사용해야 하는 주장
명시적 기법을 사용하면 모든 오류를 매끄럽게 처리할 수 있는 단일 계층을 갖기가 어렵다는 주장이 있습니다. 또한, 비검사 예외를 사용하면 개발자가 특정 오류가 발생할 수 있다는 사실을 완전히 알지 못해 실수로 오류를 무시할 수 있습니다.
필자의 의견 : 명시적 방식을 사용하라
호출하는 쪽에서 복구를 원할 수도 있는 오류에 대해 비검사 예외를 사용하지 않는 것이 최상이라고 생각합니다. 경험상 비검사 예외의 사용은 코드 베이스 전반에 걸쳐 완전히 문서화되는 경우가 거의 없기 때문입니다.
컴파일러 경고를 무시하지 말라
컴파일러 경고는 종종 코드에 문제가 있을 때 이에 대해 표시해줍니다. 이 경고에 주의를 기울여 문제를 해결하는 것이 좋습니다.