좋은 코드, 나쁜 코드 4장

4장

  • 시스템이 복구할 수 있는 오류와 복구할 수 없는 오류를 구분한다.
  • 신속하게 실패하고 분명하게 실패한다
  • 오류를 전달하기 위한 다양한 기법과 선택을 위한 고려 사항

복구 가능성

  • 복구 가능한 오류
    • 네트워크 오류
      • 자신의 코드가 의존하는 서비스에 연결하지 못한 경우 사용자가 몇 초 기다렸다가 다시 시도하거나, 네트워크 연결을 확인하도록 요청할 수 있다.
    • 중요하지 않은 작업 오류
      • 서비스 사용에 대한 어떤 통계를 기록하는 부분에서 오류가 발생한다면 실행을 계속해도 무방하다.
  • 복구할 수없는 오류
    • 오류가 발생하고 시스템이 오류를 복구할 수 있는 합리적인 방법이 없는 경우
      • 코드와 함께 추가되어야 하는 리소스가 없다.
      • 다음 예와 같이 어떤 코드가 다른 코드를 잘못 사용한다.
        • 잘못된 입력 인수로 호출
        • 일부 필요한 상태를 사전에 초기화하지 않음
      • 신속한 & 요란한 실패의 개념을 적용해야 한다
  • 호출하는 쪽에서만 오류 복구 가능 여부를 알 때
    • 오류로부터 복구하기를 호출하는 쪽에서 원하는가?
    • 만약 그렇다면 오류를 처리할 필요가 있다는 것을 호출하는 쪽에서 어떻게 알 수 있을까?
    • 함수가 어디서 호출될지 그리고 호출 시 제공되는 값이 어디서 올지 정확한 지식이 없다.
    • 코드가 미래에 재사용될 가능성은 아주 희박하지만, 재사용이 된다면 코드가 어디에서 호출되고 값이 어디서 오는지에 대한 가정이 의미가 없어질 수 있음을 의미한다.
  • 호출하는 쪽에서 복구하고자 하는 오류에 대해 인지하도록 하라
    • 함수에서 오류가 발생할 수 있다는 가능성을 호출하는 쪽에서 확실하게 인지하도록 한다.
    •  
class PhoneNumber{
	fun getPhoneNumber(number: String){
    	if (!isValidPhoneNumber(number)){
        	// some code to handle the error 
        } 
     }
 }

견고성과 실패

  • 오류가 발생할 때 아래 두 가지 동작을 할 수 있다.
    • 실패 → 더 높은 코드 계층이 처리하도록 하거나, 프로그램 작동을 멈춘다.
    • 오류를 처리하고 계속 진행한다.
  • 처리하고 진행하는 것이 더 견고한 코드라고 볼 수 있지만, 오류가 감지되지 않고 이상한 일이 발생하기 시작한다는 의미가 될 수 있다.
  • 신속하게 실패하라
    • failing test
    • 가능한 문제의 실제 발생 지점으로부터 가까운 곳에서 오류를 나타내는 것이다.
    • 안전하게 복구할 수 있는 기회를 최대한 제공한다.
    • 인수로 함수를 호출하는 경우가 대표적인 예시이다.
      • 즉시 오류를 발생시킴
    • 오류 발생 지점에서 신속하게 실패하지 않으면, 문제를 추적하고 해결하기 위한 시간이 필요해질 것이다.
  • 요란하게 실패하라
    • 프로그램이 복구할 수 없는 오류가 발생하면 프로그래밍 오류나 개발자의 실수로 인한 버그일 가능성이 크다.
    • 오류가 발생하였음에도 아무도 모르는 상황을 막고자 하는 것이다.
    • 예외를 발생해 프로그램을 중단되게 하는 것이다.
    • 이는 개발 도중이나 테스트하는 동안에 버그가 발견될 가능성이 크다.
  • 복구 가능성의 범위
    • 클라이언트의 요청을 처리하는 서버 내에서 실행되는 코드를 작성하는 경우 개별 요청을 처리하는 도중 버그가 있는 코드 경로를 실행해 오류가 발생할 수 있다.
      • 이 경우 범위 내에서 복구할 수 있는 합리적인 방법은 없지만, 시스템 전체가 작동을 멈추는 것은 막을 수 있다.
    • 일반적으로 소프트웨어를 견고하게 작성하는 것이 좋지만, 한 번의 잘못된 요청으로 전체 서버 동작이 멈추는 것은 바람직하지 않다.
    • 프로그래밍 오류가 발견되면 개발자가 이를 알아차릴 수 있도록 프로그래밍 오류를 기록하고 모니터링 하는 것이 좋다.
      • 모든 유형의 오류를 기록하는 것도 조심해야 한다.
        • 오류가 숨겨져 문제가 발생할 수 있다.
  • 오류를 숨기지 않음
    • 오류가 적절히 기록되거나 보고되지 않으면 개발팀이 문제를 인식하지 못할 수 있다.
    • 오류를 숨기는 방법은 아래와 같다.
      • 기본값 반환
      • 널 객체 패턴
      • 아무것도 하지 않음
      • 예외 억제 ( 예외 무시)
    • 기록되는 내용에 주의해야 한다
    • 오류를 숨기면 실제적인 심각한 결과가 발생할 수 있다.

오류 전달 방법

  • 명시적 방법
    • 코드를 직접 호출한 쪽에서 오류가 발생할 수 있음을 인지할 수밖에 없도록 한다.
    • 오류가 발생할 가능성이 코드 계약의 명확한 부분에 나타나 있기 때문에 오류를 넘어갈 . 수있는 방법이 거의 없다.
    • 검사 예외, 널 반환, 옵셔널 반환, 아웃컴 반환(반환값 확인이 필수)
  • 암시적 방법
    • 코드를 호출하는 쪽에 오류를 알리지만, 호출하는 쪽에서 그 오류를 신경 쓰지 않아도 된다.
    • 문서에 오류가 언급되어 있다면 코드 계약의 숨겨진 세부 조항이다.
    • 비검사 예외, 어서션, 체크, 패닉
  • 요약: 예외
    • 예외는 일반적으로 충분한 기능을 가진 클래스로 구현된다.
    • 개발자는 자신의 요구 사항에 따른 맞춤형 예외 처리를 위해 오류에 대한 정보를 자유롭게 정의하고 캡슐화할 수 있다.
    • 검사 예외비검사 예외를 제공한다.
  • [명시적]검사 예외
    • 호출하는 쪽에서 예외를 인지하도록 강제적으로 조치한다.
    • 오류를 전달하기 위한 명시적인 방법이다.
  • [명시적]비검사 예외
    • 다른 개발자들이 코드가 이 예외를 발생시킬 수 있다는 사실을 전혀 모를 수 있다.
    • 개발자가 문서화하는 것을 잊어버릴 때가 있으며, 문서화를 하더라도 코드 계약의 세부 조항이 된다.
    • 오류가 발생할 수 있다는 것을 호출하는 쪽에서 인지하리라는 보장이 없기 때문에 오류를 암시적으로 알리는 방법이다.
  • [명시적]널값이 가능한 반환 유형
    • 널 값이 가능한 반환 유형을 사용하는 것은 오류를 전달하기 위한 명시적인 방법이다.
    • 널 안전성을 지원하지 않는 언어를 사용하는 경우 옵셔널 반환 유형을 활용하는 것이 좋다.
    • 오류를 발생한 이유에 대한 정보가 없으므로 주석문이나 문서 추가가 필요하다.
  • [명시적] 리절트 반환 유형
    • 널,옵셔널 반환의 오류 정보를 전달할 수 없는 문제를 해결하였다.
    • 언어 자체에서 지원하지 않는 경우 자신만의 리절트 유형을 만들어야 하는데, 이것이 제대로 사용될지 여부는 다른 개발자가 리절트 유형을 사용하는 방법에 얼마나 익숙하느냐에 달려있다.
class NagetiveNumberError extends Error { }

Result<Double, NegativeNuimberError> getSquareRoot(Double value) {
		if (value < 0.0) { 
				return ,...
  • [명시적] 아웃컴 반환 유형
    • 오류가 발생하여 그것을 호출한 쪽에 알리고자 한다면, 함수가 수행한 동작의 결과를 나타내는 값을 반환하도록 함수를 수정하는 것이 한 가지 방법이 될 수 있다.
    • 아웃컴 반환 유형을 반환할 때 호출하는 쪽에서 반환값을 강제적으로 확인해야 한다면 이것은 오류를 알리는 명백한 방법이다.
Boolean sendMessage(Channel channel, String message) {
	if (channel.isOpen()){
			channel.send(messaeg);
			return true;
	}
	return false;
}
  • 단순한 불리언보다 더 정교한 아웃컴 유형을 사용하는 것이 더 적절할 수 있다.
  • 열거형을 사용하는 것이 유용하며, 더 자세한 정보가 필요하다면 이것을 캡슐화하기 위해 전체 클래스를 정의하는 방법도 좋다.
  • 아웃컴의 문제점
    • 호출하는 쪽에서 반환값을 무시하거나, 함수가 반환값을 가지는 것을 인식하지 못하는 경우가 발생할 수 있다.
    • 일부 언어에서는 호출하는 쪽에서 아웃풋을 무시하면 컴파일러가 경고를 생성하도록 할 수 있다.
  • [암시적 방법] 프로미스 또는 퓨처
    • 비동기적인 코드를 작성할 때 사용할 수 있다.
    • 오류 처리를 강제로 해야 하는 것은 아니고 해당 함수에 대한 코드 계약의 세부 조항을 잘 알지 못하면 오류 처리 코드를 추가로 작성해야 한다는 것을 모를 수 있다.
    • 호출하는 쪽에서 잠재적인 오류 시나리오를 완전히 알지 못하기 때문에 오류를 알리는 암시적인 방법이다.
  • [암시적 방법] 매직값 반환
    • 매직값 : 함수의 정상적인 반환 유형에 적합하지만 특별한 의미를 부여하는 값이다.
    • -1을 반환하는 방법을 활용한다.

호출하는 쪽에서 복구하기를 원할 수도 있는 오류의 전달

  • 비검사 예외와 명시적 오류 전달 기법 중 어느 것을 사용해야 하는지에 대한 논쟁이 존재한다.
  • 비검사 예외를 사용해야 한다
    • 코드 구조 개선
      • 대부분 오류 처리가 상위 계층에서 이루어질 수 있기 때문에, 코드 구조를 개선할 수 있다.
      • 그 사이의 코드는 오류를 처리할 필요가 없으며, 상위 계층이 오류를 처리한다.
      • 코드의 계층을 올라가면서 오류를 반복적으로 전달하고 이를 처리하는 작업이 필요하다.
        • → 이는 개발자의 편의를 도모하고 잘못된 작업을 하게 만든다.
      • 비검사 예외를 찬성하는 주장 중 하나가 이 문제에 대해 실용적으로 접근해야 한다는 주장이다.
  • 명시적 기법을 사용해야 한다
    • 매끄러운 오류 처리
      • 비검사 예외를 사용하면 모든 오류를 매끄럽게 처리할 수 있는 단일 계층을 갖기가 어렵다.
      • 사용자 친화적이지 않은 오류 메시지를 UI에 표시하는 경우가 생길 수 있다.
      • 이에 따라 잠재적 오류를 좀 더 강제적으로 인식하도록 하면 이러한 오류를 매끄럽게 처리할 가능성이 커진다.
    • 실수로 오류를 무시할 수 없다.
    • 좀 더 명확한 오류 전달 방식을 활용하면 잘못된 일이 기본적으로 혹은 실수로 인해 일어나지 않는다.

컴파일러 경고를 무시하지 말라

  • 컴파일러 경고는 어떤 식으로든 코드가 의심스러우면 표시를 하는데, 이것은 버그에 대한 조기 경고일 수 있다.
  • 코드베이스에 병합되기 훨씬 전에 코드로부터 프로그래밍 오류를 발견하고 제거할 수 있다.

요약

  • 오류에는 크게 두가지 종류가 있다.
    • 시스템이 복구할 수 있는 오류
    • 시스템이 복구할 수 없는 오류
  • 해당 코드에 의해 생성된 오류로부터 복구할 수 있는지 여부를 해당 코드를 호출하는 쪽에서만 알 수 있는 경우가 많다.
  • 에러가 발생하면 신속하게 실패하는 것이 좋고, 에러를 복구할 수 없는 경우에는 요란하게 실패하는 것이 바람직하다.
  • 오류를 숨기는 것은 바람직하지 않을 때가 많으며, 오류가 발생했다는 신호를 보내는 것이 바람직하다.
  • 오류 전달 기법은 두 가지 범주로 나눌 수 있다.
    • 명시적 방법: 코드 계약의 명확한 부분, 호출하는 쪽에서 오류가 발생할 수 있음을 인지한다.
    • 암시적 방법: 코드 계약의 세부 조항을 통해 오류에 대한 설명이 제공되거나 전혀 설명이 없을 수도 있다.
  • 복구할 수없는 오류에 대해서는 암시적 오류 전달 기법을 사용해야 한다.
  • 잠재적으로 복구할 수 있는 오류에 대해서는 명시적 혹은 암시적 기법 중 어느 것을 사용할지에 대해서는 개발자들 사이에서 일치되는 의견이 없다.
  • 컴파일러 오류는 주의를 기울이는 것이 바람직하다