본문 바로가기

뇌세포덩어리""/알고리즘

The Power of Ten - Rules for Developing Safety Critical Code

 

 

The Power of Ten – Rules for Developing Safety Critical Code

 

 

대부분의 소프트웨어 개발 프로젝트는 코딩 가이드라인을 사용합니다. 이러한 가이드라인은 소프트웨어가 어떻게 구성되어야 하는지, 어떤 언어 기능을 사용해야 하고 사용하지 말아야 하는지 등 소프트웨어 작성에 대한 기본 규칙을 명시하기 위한 것입니다. 흥미롭게도 좋은 코딩 표준이 무엇인지에 대한 합의가 거의 없습니다. 지금까지 작성된 많은 문서들 중에서 식별할 수 있는 패턴은 거의 없지만, 새로운 문서가 이전 문서보다 길어지는 경향이 있다는 점을 제외하면 현저히 적습니다. 그 결과 대부분의 기존 가이드라인에는 100개가 훨씬 넘는 규칙이 포함되어 있으며, 때로는 정당성이 의심스러운 규칙도 있습니다. 특히 프로그램에서 공백 사용을 규정하려는 일부 규칙은 개인적인 선호에 의해 도입되었을 수도 있고, 같은 조직 내에서 이전 코딩 작업에서 발생할 가능성이 매우 낮고 구체적인 유형의 오류를 방지하기 위한 규칙도 있습니다. 당연히 기존의 코딩 가이드라인은 개발자가 실제로 코드를 작성할 때 거의 영향을 미치지 않는 경향이 있습니다. 많은 가이드라인의 가장 치명적인 측면은 포괄적인 도구 기반 규정 준수 점검을 거의 허용하지 않는다는 것입니다. 대규모 애플리케이션을 위해 작성된 수십만 줄의 코드를 수동으로 검토하는 것은 종종 불가능하기 때문에 도구 기반 점검이 중요합니다.

잘 선택된 검증 가능한 코딩 규칙 집합을 사용하면 규칙 집합 자체의 준수 여부를 넘어 중요한 소프트웨어 구성 요소를 보다 철저하게 분석할 수 있습니다. 그러나 효과적이려면 규칙은 작아야 하며, 쉽게 이해하고 기억할 수 있을 정도로 명확해야 합니다. 규칙은 기계적으로 확인할 수 있을 만큼 구체적이어야 합니다. 효과적인 가이드라인을 위한 규칙의 수에 대한 쉬운 상한선을 제시하자면, 10개 이하의 규칙으로 제한하면 상당한 이점을 얻을 수 있다고 주장할 수 있습니다. 물론 이러한 작은 세트가 모든 것을 포괄할 수는 없지만 소프트웨어 신뢰성과 검증 가능성에 대해 측정 가능한 효과를 얻을 수 있는 발판을 마련할 수 있습니다. 강력한 검증을 지원하기 위해 규칙은 다소 엄격하다고 할 수 있을 정도로 엄격합니다. 

 

특히 안전에 중요한 코드를 개발할 때와 같이 정말 중요한 경우에는 더 많은 노력을 기울이고 바람직한 것보다 더 엄격한 한계를 지킬 가치가 있을 수 있습니다. 그 대가로 중요한 소프트웨어가 의도한 대로 작동한다는 것을 더 설득력 있게 보여줄 수 있어야 합니다.

 

 

안전에 중요한 코딩을 위한 10가지 규칙

안전에 중요한 코드의 언어 선택은 그 자체로 중요한 고려 사항이지만 여기서는 이에 대해 자세히 다루지 않겠습니다. JPL을 포함한 많은 조직에서 중요 코드는 C로 작성되며, 오랜 역사를 자랑하는 이 언어에는 강력한 소스 코드 분석기, 논리 모델 추출기, 메트릭 도구, 디버거, 테스트 지원 도구, 성숙하고 안정적인 컴파일러 선택 등 광범위한 도구 지원이 있습니다. 이러한 이유로 C는 개발된 대부분의 코딩 가이드라인의 대상이 되기도 합니다. 따라서 상당히 실용적인 이유로, 코딩 규칙은 주로 C를 대상으로 하며, C로 작성된 중요한 애플리케이션의 안정성을 보다 철저하게 검사하는 기능을 최적화하기 위해 노력합니다.

 


 

1. 모든 코드를 매우 단순한 제어 흐름 구조로 제한하세요.

goto문, setjmp / longjmp, 직접 또는 간접 재귀를 사용하지 마세요.

 

제어 흐름이 단순할수록 검증 기능이 강화되고 코드의 명확성이 향상되는 경우가 많습니다. 재귀를 쓰지 말자는 부분은 아마도 이 글에서  가장 놀라운 부분일 것입니다. 재귀가 없으면 비순환 함수 호출 그래프가 보장되어 코드 분석기가 이를 활용할 수 있으며, 바인딩되어야 하는 모든 실행이 실제로 바인딩되어 있음을 증명하는 데 직접적으로 도움이 될 수 있습니다.

 

(이 규칙은 모든 함수가 단일 반환점을 가질 필요는 없지만 제어 흐름을 단순화하기도 합니다. 하지만 조기 에러 반환이 더 간단한 해결책인 경우도 충분히 있습니다.)

 

 

 

2. 모든 루프에는 고정된 상한이 있어야 합니다. 검사 도구가 루프의 반복 횟수에 대해 미리 설정된 상한을 초과할 수 없음을 정적으로 증명할 수 있어야 합니다. 루프 상한을 정적으로 증명할 수 없는 경우 규칙을 위반한 것으로 간주합니다.

 

재귀가 없고 루프 바운드가 있으면 코드의 런어웨이를 방지할 수 있습니다. 물론 이 규칙은 종료되지 않는 반복(예: 프로세스 스케줄러)에는 적용되지 않습니다. 이러한 특수한 경우에는 정반대의 규칙이 적용됩니다. 즉, 반복이 종료될 수 없음을 정적으로 증명할 수 있어야 합니다.

 

이 규칙을 지원하는 한 가지 방법은 가변 반복 횟수가 있는 모든 루프(예: 연결된 목록을 순회하는 코드)에 명시적 상한을 추가하는 것입니다. 상한을 초과하면 assertion failure가 트리거되고 실패한 반복을 포함하는 함수는 오류를 반환합니다. 

 

 

 

3. 초기화 후에는 동적 메모리 할당을 사용하지 마세요.

이 규칙은 안전에 중요한 소프트웨어에 일반적으로 적용되며 대부분의 코딩 가이드라인에 나와 있습니다. malloc과 같은 메모리 할당자와 가비지 컬렉터는 종종 예측할 수 없는 동작으로 성능에 큰 영향을 미칠 수 있기 때문입니다. 메모리 할당 및 해제 루틴을 잘못 처리하여 메모리를 해제하는 것을 잊거나 메모리가 해제된 후에도 계속 사용하는 경우, 물리적으로 사용 가능한 메모리보다 많은 메모리를 할당하려고 시도하는 경우, 할당된 메모리의 한계를 초과하는 경우 등 코딩 오류의 주요 유형도 메모리 할당 및 해제 루틴을 잘못 처리하는 곳에서 발생합니다. 모든 애플리케이션이 미리 할당된 고정된 메모리 영역 내에서만 사용하도록 강제하면 이러한 문제를 상당수 해결할 수 있고 메모리 사용량을 더 쉽게 확인할 수 있습니다. 힙에서 메모리가 할당되지 않은 상태에서 메모리를 동적으로 요청하는 유일한 방법은 스택 메모리를 사용하는 것입니다. 재귀가 없는 경우(규칙 1) 스택 메모리 사용에 대한 상한선을 정적으로 도출할 수 있으므로 애플리케이션이 항상 사전 할당된 메모리 범위 내에서 사용된다는 것을 증명할 수 있습니다.

 

 

 

4. 함수는 문당 한 줄, 선언당 한 줄로 표준 참조 형식으로 한 장의 종이에 인쇄할 수 있는 길이보다 길어서는 안 됩니다. 일반적으로 이는 함수당 코드가 약 60줄을 넘지 않음을 의미합니다.

 각 함수는 코드에서 하나의 단위로 이해하고 검증할 수 있는 논리적 단위여야 합니다. 컴퓨터 디스플레이의 여러 화면에 걸쳐 있거나 인쇄 시 여러 페이지에 걸쳐 있는 논리 단위는 이해하기 훨씬 더 어렵습니다. 지나치게 긴 함수는 종종 코드 구조가 잘못되었다는 신호입니다.

 

 

 

5. 코드의 assertion는 함수당 평균 최소 두 개의 assertion이어야 합니다. assertion은 실제 실행에서 절대 발생해서는 안 되는 비정상적인 조건을 확인하는 데 사용됩니다. assertion은 항상 부작용이 없어야 하며 부울 테스트로 정의해야 합니다. assertion이 실패하면 실패한 assertion을 실행하는 함수의 호출자에게 오류 조건을 반환하는 등의 명시적인 복구 조치를 취해야 합니다. 정적 검사 도구로 절대 실패하거나 유지되지 않음을 증명할 수 있는 모든 assertion은 이 규칙을 위반합니다. (즉, 도움이 되지 않는 “assert(true)” 문을 추가하여 규칙을 충족할 수 없습니다.)

통계에 따르면 단위 테스트는 종종 10~100줄의 코드를 작성할 때마다 적어도 하나의 결함을 발견합니다. 결함을 발견할 확률은 assertion 밀도에 따라 증가합니다. 강력한 방어 코딩 전략의 일환으로 어설션을 사용하는 것이 권장되는 경우도 많습니다. assertion은 함수의 pre- 및 post- 조건, 매개변수 값, 함수의 반환 값, 루프 불변성을 검증하는 데 사용할 수 있습니다. assertion은 부작용이 없기 때문에 성능에 중요한 코드에서 테스트한 후 선택적으로 비활성화할 수 있습니다.

 

일반적인 사용 예는 다음과 같습니다

if (!c_assert(p >= 0) == true) {
	return ERROR;
}

를 다음과 같이 정의합니다:

#define c_assert(e) ((e) ? (true) : \
	tst_debugging(”%s,%d: assertion ’%s’ failed\n”, \
	__FILE__, __LINE__, #e), false)

 

이 정의에서 __FILE__ 및 __LINE__은 매크로 전처리기에 의해 미리 정의되어 실패한 어설션의 파일 이름과 줄 번호를 생성합니다. 구문 #e는 assertion 조건 e를 오류 메시지의 일부로 인쇄되는 문자열로 변환합니다. 임베디드 프로세서를 대상으로 하는 코드에서는 당연히 오류 메시지 자체를 인쇄할 곳이 없습니다. 이 경우 tst_debugging에 대한 호출은 no-op으로 바뀌고 assertion은  비정상적인 동작에서 오류를 복구할 수 있는 순수 boolean 테스트로 바뀝니다.

 

 

 

6. 데이터 객체는 가능한 가장 작은 범위로 선언해야 합니다.

이 규칙은 데이터 숨김의 기본 원칙을 지원합니다. 분명히 개체가 범위 내에 있지 않으면 그 값을 참조하거나 손상시킬 수 없습니다. 마찬가지로, 객체의 잘못된 값을 진단해야 하는 경우 해당 값이 할당될 수 있는 문이 적을수록 문제를 진단하기가 더 쉬워집니다. 이 규칙은 호환되지 않는 여러 용도로 변수를 재사용하지 못하도록 하여 오류 진단을 복잡하게 만들 수 있습니다.

 

 

 

7. non-void 함수가 아닌 함수의 반환 값은 각 호출 함수에서 확인해야 하며, 매개 변수의 유효성은 각 함수 내부에서 확인해야 합니다.

이 규칙은 가장 자주 위반되는 규칙이므로 일반적인 규칙으로서는 다소 의심스러운 규칙입니다. 이 규칙을 가장 엄격하게 적용하면 printf 문과 파일 닫기 문의 반환 값도 확인해야 한다는 뜻입니다. 하지만 오류에 대한 응답이 성공에 대한 응답과 다를 바 없다면 반환값을 명시적으로 확인할 필요가 없다고 주장할 수도 있습니다.

 

이는 종종 printf 및 close 호출의 경우에 해당합니다. 이런 경우에는 함수 반환값을 (void)로 명시적으로 캐스팅하여 프로그래머가 실수가 아니라 명시적으로 반환값을 무시하기로 결정했음을 표시하는 것이 허용될 수 있습니다.

 

좀 더 모호한 경우에는 반환값이 무의미한 이유를 설명하는 주석이 있어야 합니다. 하지만 대부분의 경우, 특히 오류 반환값이 함수 호출 체인을 따라 전파되어야 하는 경우에는 함수의 반환값을 무시해서는 안 됩니다. 표준 라이브러리는 이 규칙을 위반하여 잠재적으로 심각한 결과를 초래하는 것으로 유명합니다. 예를 들어, 표준 C 문자열 라이브러리로 strlen(0) 또는 strcat(s1, s2, -1)을 실수로 실행하면 어떤 일이 벌어지는지 보세요. - crash가 나거나 / 버퍼 오버플로우로 다운됩니다.

 

일반적인 규칙을 유지하면서 예외는 반드시 정당한 사유가 있어야 하며, 기계적 검사기를 통해 위반 사항을 표시합니다. 규칙을 준수하지 않아도 되는 이유를 설명하는 것보다 규칙을 준수하는 것이 더 쉬운 경우가 많습니다.

 

 

 

8. 전처리기의 사용은 헤더 파일과 간단한 매크로 정의의 포함으로 제한되어야 합니다. 토큰 붙여넣기, 가변 인수 목록(줄임표), 재귀적 매크로 호출은 허용되지 않습니다. 모든 매크로는 완전한 구문 단위로 확장되어야 합니다. 조건부 컴파일 지시어를 사용하는 것도 종종 모호하지만 항상 피할 수는 없습니다. 즉, 대규모 소프트웨어 개발 작업에서도 동일한 헤더 파일을 여러 번 포함하지 않도록 하는 표준 상용구 외에 한두 개 이상의 조건부 컴파일 지시문을 사용하는 경우는 거의 없습니다. 이러한 각 사용은 도구 기반 검사기로 플래그를 지정하고 코드에서 정당화해야 합니다.

 

C 전처리기는 강력한 난독화 도구로 코드 명확성을 파괴하고 많은 텍스트 기반 검사기를 당황하게 만들 수 있습니다. 제한되지 않은 전처리기 코드의 구조체 효과는 공식적인 언어 정의가 있더라도 해독하기가 매우 어려울 수 있습니다. C 전처리기의 새로운 구현에서 개발자는 C 표준의 복잡한 정의 언어를 해석하기 위해 이전 구현을 참조로 사용해야 하는 경우가 많습니다. 조건부 컴파일에 대한 주의의 근거도 마찬가지로 중요합니다. 조건부 컴파일 지시어가 10개만 있으면 코드의 가능한 버전이 최대 210개가 될 수 있으며, 각 버전을 테스트해야 하므로 필요한 테스트 노력이 크게 증가한다는 점에 유의하세요.

 

 

9. 포인터의 사용은 제한되어야 합니다. 특히, 한 수준 이상의 역참조는 허용되지 않습니다. 포인터 역참조 연산을 매크로 정의나 typedef 선언 안에 숨길 수 없습니다. 함수 포인터는 허용되지 않습니다.

포인터는 숙련된 프로그래머도 쉽게 오용할 수 있습니다. 특히 도구 기반의 정적 분석기가 프로그램의 데이터 흐름을 추적하거나 분석하는 것을 어렵게 만들 수 있습니다. 마찬가지로 함수 포인터는 정적 분석기가 수행할 수 있는 검사 유형을 심각하게 제한할 수 있으므로 사용에 대한 강력한 정당성이 있는 경우에만 사용해야 하며, 도구 기반 검사기가 제어 흐름과 함수 호출 계층 구조를 결정하는 데 도움이 되는 대체 수단이 제공되는 것이 이상적입니다. 예를 들어 함수 포인터를 사용하면 도구가 재귀가 없음을 증명하는 것이 불가능해질 수 있으므로 분석 기능의 손실을 보완할 수 있는 대체 보증이 제공되어야 합니다.

 

 

10. 모든 코드는 개발 첫날부터 컴파일러의 가장 현학적 설정에서 모든 컴파일러 경고를 활성화한 상태로 컴파일해야 합니다. 모든 코드는 경고 없이 이 설정으로 컴파일되어야 합니다. 모든 코드는 최소한 하나 이상의 최신 정적 소스 코드 분석기로 매일 검사해야 하며, 가급적이면 두 개 이상의 최신 정적 소스 코드 분석기를 사용하여 경고 없이 분석을 통과해야 합니다.

현재 시중에는 매우 효과적인 정적 소스 코드 분석기가 여러 가지 있으며, 프리웨어 도구도 상당수 있습니다. 소프트웨어 개발에 있어 쉽게 사용할 수 있는 기술을 활용하지 않을 이유가 없습니다. 중요하지 않은 코드 개발의 경우에도 일상적인 관행으로 간주해야 합니다. 컴파일러나 정적 분석기가 잘못된 경고를 제공하는 경우에도 경고 제로 규칙이 적용됩니다. 컴파일러나 정적 분석기가 혼동을 일으키는 경우, 혼동을 일으키는 코드를 다시 작성하여 보다 사소하게 유효하도록 만들어야 합니다. 많은 개발자들이 경고가 틀림없이 유효하지 않다는 가정에 사로잡혀 있다가 나중에 그 메시지가 실제로는 덜 분명한 이유로 유효하다는 사실을 깨닫는 경우가 많습니다. 정적 분석기는 대부분 유효하지 않은 메시지를 생성하는 린트와 같은 초기 제품으로 인해 평판이 다소 좋지 않았지만, 이제는 더 이상 그렇지 않습니다. 오늘날 최고의 정적 분석기는 속도가 빠르며 선택적이고 정확한 메시지를 생성합니다. 심각한 소프트웨어 프로젝트에서 정적 분석기의 사용을 타협해서는 안 됩니다.

 


 

처음 두 가지 규칙은 빌드, 테스트 및 분석이 더 쉬운 명확하고 투명한 제어 흐름 구조를 생성하도록 보장합니다. 세 번째 규칙에 명시된 동적 메모리 할당이 없으면 메모리 할당 및 해제, 스트레이 포인터 사용 등과 관련된 여러 가지 문제가 제거됩니다. 다음 몇 가지 규칙(4~7번)은 좋은 코딩 스타일의 표준으로 상당히 광범위하게 받아들여지고 있습니다. 안전에 중요한 시스템을 위해 발전된 다른 코딩 스타일(예: “계약에 의한 설계”의 규율)의 일부 이점은 규칙 5~7에서 부분적으로 찾을 수 있습니다.

 

이 10가지 규칙은 JPL에서 미션 크리티컬 소프트웨어를 작성하는 데 실험적으로 사용되고 있으며, 고무적인 결과를 얻고 있습니다. 개발자들은 처음에는 엄격한 규칙에 대한 거부감을 극복하고 나면 규칙을 준수하는 것이 코드 명확성, 분석 가능성 및 코드 안전성에 도움이 된다는 것을 알게 되는 경우가 많습니다. 규칙은 개발자와 테스터가 다른 방법으로 코드의 주요 속성(예: 종료 또는 경계, 메모리 및 스택의 안전한 사용 등)을 설정해야 하는 부담을 덜어줍니다. 이 규칙이 처음에는 엄격해 보이지만, 여러분이 타고 있는 비행기, 거주지에서 몇 마일 떨어진 원자력 발전소, 우주비행사를 궤도에 올리는 우주선을 제어하는 데 사용되는 코드 등 말 그대로 여러분의 생명이 달려 있는 코드를 점검할 수 있도록 하기 위한 것임을 명심하세요. 이 규칙은 자동차의 안전벨트와 같은 역할을 합니다. 처음에는 조금 불편할 수 있지만 시간이 지나면 자연스럽게 사용하게 되고 사용하지 않는 것은 상상할 수 없게 됩니다.

'뇌세포덩어리"" > 알고리즘' 카테고리의 다른 글

백준 플래티넘5 기념샷  (1) 2024.09.17
BM25(Okapi BM25)  (0) 2023.03.07
[baekjoon] dfs / bfs  (0) 2023.02.20
[baekjoon] union-find  (0) 2023.02.17
[baekjoon] dijkstra 문제들  (0) 2023.02.17