Skip to content

NR: 규칙이 아닌 것들과 근거없는 이야기들

이 절은 어디선가 널리 알려졌지만, 핵심 가이드라인에서 추천하지 않는 규칙과 가이드라인들을 포함한다.

이런 규칙들이 타당하던 시간과 장소가 있었다는 것을 우리는 충분히 잘 알고, 사용해왔다. 하지만, 우리가 가이드라인을 통해 권장하고 지지하는 프로그래밍의 스타일에 미루어 봤을때, 이 "비-규칙"들은 해로울(harm) 수 있다.

오늘날에도, 이 규칙들이 들어맞을 수 있다. 적절한 지원 도구가 없어 정해진 시간 안에 응답해야 하는 시스템에서 예외를 쓰지 못하는 경우를 예로 들 수 있다. 하지만 "알려진 지혜"를 맹신해선 안된다 ("효율성"에 대한 근거 없는 문구들이라던가); 그런 "지혜"는 십년은 더 된 정보에 근거하거나 (C나 Java 처럼) C++와는 매우 다른 속성을 가진 언어경험에 근거한 것일 수 있다.

이런 비-규칙들을 대신하기 위한 긍정적인 논쟁(positive arguments)들은 "대안들"에서 기술한다.

비-규칙 요약:

NR.1: 금지: 모든 선언이 함수의 상단에 오게 하라

Reason (not to follow this rule)

이 규칙은 구문이 실행된 이후의 변수와 상수의 초기화를 허용하지 않던 오래된 프로그래밍 언어들의 잔재다. 이 규칙은 프로그램 코드를 길게 만들고 초기화가 생략되거나 잘못된 값으로 초기화되는 것으로 인한 오류를 발생시킨다.

Example, bad
    int use(int x)
    {
        int i;
        char c;
        double d;

        // ... some stuff ...

        if (x < i) {
            // ...
            i = f(x, d);
        }
        if (i < x) {
            // ...
            i = g(x, c);
        }
        return i;
    }

초기화되지 않은 변수와 실제 사용 코드의 간격이 멀어질 수록, 버그가 발생할 기회가 많아진다. 다행히도, 컴파일러가 많은 "값을 결정하기 전에 사용하는" 오류를 잡아낼 수 있다. 불행하게도, 컴파일러가 모든 오류를 잡아낼 수는 없고 이 짧은 예제와 같이 쉽게 찾아낼만한 경우만 있는 것도 아니다.

Alternative

NR.2: 금지: 함수에서 오직 하나의 return 구문을 사용하라

Reason (not to follow this rule)

함수에 반환 지점이 하나만 있으면 불필요하게 뒤얽힌 코드를 만들 수 있고 추가적인 상태 변수들을 만들게 된다. 특히, 단일 반환 규칙은 함수 상단에서 오류 검사에 집중할 수 없게 만든다.

Example
    template<class T>
    //  requires Number<T>
    string sign(T x)
    {
        if (x < 0)
            return "negative";
        else if (x > 0)
            return "positive";
        return "zero";
    }

반환 지점을 하나로 만들기 위해선 다음과 같은 일을 해야한다

    template<class T>
    //  requires Number<T>
    string sign(T x)        // bad
    {
        string res;
        if (x < 0)
            res = "negative";
        else if (x > 0)
            res = "positive";
        else
            res = "zero";
        return res;
    }

이는 더 긴 코드일 뿐만 아니라 비효율적일 수 있다. 함수가 커지고 복잡해질수록, 더 작성하거나 사용하기에 고통스러운 함수가 된다. 물론 많은 단순한 함수들은 그 내용에 담겨있는 단순한 논리 덕분에 자연스럽게 하나의 return을 가지게 될 것이다.

Example
    int index(const char* p)
    {
        if (!p) return -1;  // error indicator: alternatively "throw nullptr_error{}"
        // ... do a lookup to find the index for p
        return i;
    }

단일 반환 규칙을 적용하면 이렇게 변한다

    int index2(const char* p)
    {
        int i;
        if (!p)
            i = -1;  // error indicator
        else {
            // ... do a lookup to find the index for p
        }
        return i;
    }

(의도적으로) 규칙을 위반했다는 점에 주목하라. 이런 코드 스타일은 보통 초기화되지 않은 변수가 있는 코드를 작성하게 된다. 또한, 이 스타일은 goto exit과 같은 방식을 적용하고 싶은 유혹을 불러일으킨다.

Alternative
  • 함수는 짧고 단순하게 유지하라
  • return 구문을 여러번 사용해도 좋다 (예외를 던져도 괜찮다)

NR.3: 금지: 예외를 사용하지 마라

Reason (not to follow this rule)

이 비-규칙에는 3가지 이유가 따라온다.

  • 예외는 비효율적이다(inefficient)
  • 예외는 누수와 오류로 이어진다(leaks and errors)
  • 예외의 성능은 예측할 수 없다(not predictable)

이 쟁점에 대해서 모두가 만족하도록 타협할 방법은 없다. 무엇보다, 예외에 대한 토론은 40년 넘게 이어지고 있다. 일부 언어들은 예외 없이는 쓸수가 없고, 다른 일부는 예외를 지원하지 않기도 한다. 이 때문에 예외를 쓰거나 쓰지 않는 강한 전통과 열띤 논쟁이 발생한다.

그렇지만 핵심 가이드라인을 작성하는 우리가 범용 프로그래밍에서는 예외가 최선의 방안이라고 생각하는 이유는
단순한 찬반 양론은 종종 결론을 내릴 수 없기 때문이다.

예외가 실제로 부적합한 특별한 응용 프로그램들도 존재한다. (예외 처리의 비용을 정확히 평가하기 위한 지원이 없으면서 시간 내 응답성을 보장해야 하는(hard-real-time) 시스템 처럼)

예외를 반대하는 주장으로 돌아와서 생각해보면

  • 예외는 비효율적이다:
    무엇에 비해서 비효율적인가? 비교를 한다면 같은 오류 집합을 처리하고 각각을 동등하게 처리할 때를 비교해야 한다.
    특히, 즉시 비정상 종료하는 프로그램과 오류를 기록하기 전에 주의깊게 자원들을 정리하는 프로그램을 비교해서는 안된다. 실제로, 일부 시스템들은 형편없는 예외 처리 구현을 가지고 있다; 때로는 그런 구현이 다른 오류 처리 접근법들을 취할 수 밖에 없도록 만든다.
    하지만 그게 오류의 근본적인 문제는 아니다. 효율성을 따진다면 - 어떤 경우에든 - 문제에 대한 통찰을 줄 수 있는 좋은 데이터를 가졌는지 주의 깊게 따져보라.
  • 예외는 누수와 오류로 이어진다:
    그렇지 않다. 만약 당신의 프로그램이 자원 관리를 위한 전략 없이 완전히 꼬여있는 포인터들이라면, 무엇을 하더라도 문제가 될 것이다.
    그런 코드가 백만줄이 있다면 예외를 쓰는 것이 불가능할 것이다, 하지만 그것은 무분별하고 과용된 포인터 사용으로 인한 문제다.
    우리의 의견은, 예외 기반 오류 처리를 단순하고 안전하게 하기 위해서는 RAII가 필요하다 -- 이는 예외를 쓰지 않을때보다 단순하고 안전하다.
  • 예외의 성능은 예측할 수 없다:
    주어진 시간 안에 작업을 마친다는 것을 보장해야 하는 시스템을 만든다면, 그런 보장을 뒷받침할 도구들이 필요하다.
    우리가 아는 한 그런 도구는 (최소한 대부분의 프로그래머들에게는) 없다

주로, 보통 대부분, 예외로 인한 문제는 오래된 지저분한 코드와 함께 동작해야하는 요구에 기인한다.

예외를 사용하자는 주장들 중 핵심은

  • 예외는 잘못된 반환과 정상적인 반환을 완전히 다르게 취급한다
  • 예외는 잊어버리거나 무시할 수 없다
  • 예외는 체계적으로 사용할 수 있다

이 점들을 기억하라

  • 예외는 오류를 알리기 위한 것이다 (C++에서는 그렇다; 다른 언어들은 다른 목적으로 사용할 수도 있다).
  • 예외는 지역적으로 처리할 수 없는 오류를 위한 것이다.
  • 모든 함수에서 모든 예외를 잡으려(catch) 하지 말아라 (지루하고, 보기 흉하며, 느린 코드가 생성된다).
  • 예외는 복구할 수 없는 오류 이후 모듈/시스템을 종료하기 위한 처리방식이 아니다.
Example
    ???
Alternative
  • RAII
  • Contracts/assertions: GSL(가이드라인 지원 라이브러리) 의 ExpectsEnsures를 사용하라. (언어에서 contract를 지원하기 전까지)

NR.4: 금지: 클래스 선언을 각각의 파일에 배치하라

Reason (not to follow this rule)

이 규칙을 따르면서 생성되는 파일들은 관리하기 힘들고 컴파일 속도를 저하시킨다. 각각의 클래스들이 유지보수와 배포를 위한 매우 좋은 논리적인 단위인 경우는 드물다.

Example
    ???
Alternative
  • 네임스페이스로 서로 결합된(cohesive) 클래스들과 함수들을 포함시켜라

NR.5: 금지: 생성자에서 많은 일을 하지마라; 대신 2 단계 초기화를 사용하라

Reason (not to follow this rule)

이 규칙을 따르면 불변조건이 약화되고, 더 복잡한 코드(절반만 생성된 개체들을 다뤄야 한다)와 오류(그 개체들을 일관적이고 정확한 방법으로 다루지 않았을 경우)가 발생한다.

Example
    ???
Alternative
  • 생성자에서 클래스 불변조건을 확실히하라(establish)
  • 필요해지기 전까지는 개체를 정의하지 마라

NR.6: 금지: 모든 정리 동작을 함수 끝에 배치하고 goto exit을 사용하라

Reason (not to follow this rule)

goto는 오류에 취약하다. 이 기술은 예외가 없었던 때 RAII 같은 자원과 오류 처리하던 방법이다.

Example, bad
    void do_something(int n)
    {
        if (n < 100) goto exit;
        // ...
        int* p = (int*) malloc(n);
        // ...
        if (some_error) goto_exit;
        // ...
    exit:
        free(p);
    }

버그를 찾아보라.

Alternative
  • RAII와 예외를 사용하라
  • RAII를 따르지 않는 자원에는 finally를 사용하라

NR.7: 금지: 모든 데이터 멤버를 protected로 하라

Reason (not to follow this rule)

protected 데이터는 오류의 원인이 된다.
protected 데이터는 계측할 수 없는 양의 코드에 의해서, 다양한 위치에서 변경될 수 있다.
protected 데이터는 클래스 계층구조에서 전역변수와 동일하다.

Example
    ???
Alternative