Power of 10: 안전성이 중요한 코드 개발을 위한 규칙
` 정적 분석과 코딩 규칙을 이용한 임베디드 소프트웨어 버그 줄이기 ‘라는 제목의 기사를 읽다가 ` Power of 10 ’ 규칙이 언급되었길래 다시 읽어볼 기회가 생겼습니다. 기사 내용은 필자가 개발한 정적 분석 도구 제품을 홍보하는 느낌을 받았지만, 어느 정도의 가이드라인을 지킨 코드는 정적 분석 도구의 도움을 받아 미리 오류를 발견하고 수정하기가 더 쉽기 때문에 상호 보완하는 것이 좋다는 점에는 동의합니다. 아무튼 기사와는 별도로, 십제곱(?) 규칙은 다시 읽어도 나름대로 곱씹을 가치가 있는 것 같아 우리말로 다시 정리해 보았습니다.
대부분 소프트웨어 프로젝트는 나름의 코딩 가이드라인을 사용합니다. 가이드라인은 프로그래머가 소프트웨어를 만들때 어떻게 구성할지, 언어의 어떤 특징을 사용하고 사용하면 안되는지 등을 규정합니다. 하지만 수많은 코딩 가이드라인이 대부분 비슷하고, 너무 규칙이 많거나 모호한 경우도 많습니다. 때로는 공백문자 사용법 등과 같은 개인적인 취향을 반영하기도 합니다. 결과적으로 이러한 코딩 가이드라인은 개발자가 코드를 작성할때 별로 영향을 끼치지 못하곤 합니다. 더 나아가 도구를 사용하여 검사하는 작업과 병행하기 어렵게 하기도 합니다. 도구를 사용하여 검사하는 과정은 중요한데, 수십만 라인의 코드를 직접 검토하는 작업은 불가능하기 때문입니다.
따라서 좋은 코딩 가이드라인은 분량이 적고, 사람들이 쉽게 이해하고 기억할 수 있도록 명료해야 합니다. 그래서 저자는 10개의 효율적이고 규칙을 제안합니다. 이 가이드라인은 임베디드 소프트웨어 개발에 오랫동안 널리 사용해 온 C 언어를 대상으로 합니다. (참고로 저자는 NASA의 JPL(Jet Propulsion Lab.)에 근무하면서 고신뢰 소프트웨어 개발을 연구하는 분입니다)
규칙 1: 단순하게 제어 흐름(control flow)을 구성하고 goto, setjmp(), longjump(), 재귀(recursion) 사용 안하기
제어 흐름이 단순할수록 더 튼튼하고 분석이 용이하며 코드를 명료하게 합니다. 재귀를 없애면 순환하는 호출 그래프를 없앨 수 있고, 그로 인해 스택 오버플로우 등을 걱정할 필요도 없습니다. 그렇다고 이 규칙이 모든 함수가 단일점에서 복귀(return)해야 한다는 건 아닙니다.
규칙 2: 루프에서 상한값을 고정하기
검사 도구가 쉽게 분석할 수 있을 뿐 아니라, 재귀를 피하는 규칙과 더불어 이 규칙을 따르면 무한루프처럼 폭주하는 코드를 걱정할 필요가 없습니다.
규칙 3: 초기화 이후 동적 메모리 할당 사용 한하기
많은 가이드라인에 포함되어 있는 규칙인데, 이유는 명료합니다. 동적 메모리 할당 함수는 성능에 심각한 영향을 끼칠 뿐 아니라 실수로 인한 메모리 누수는 시스템을 심각한 상태에 빠뜨릴 수 있기 때문입니다. 필요하다면 alloca() 등과 같은 스택 기반 동적 할당은 사용할 수 있습니다.
규칙 4: 함수 하나가 출력시 한 페이지를 넘어가지 않도록 제한하기
함수를 더 쉽게 이해하고 검증할 수 있는 단위로 나누기 위해 필요합니다. 함수가 길어질수록 논리적으로 잘 구조화된 코드를 작성하기 어렵습니다.
규칙 5: 함수에 최소 2개 이상의 단언문(assert) 사용하기
최종 빌드시 비활성화될 수 있는 단언문은 개발 도중 많이 사용할 수록 좋습니다.
규칙 6: 자료 객체는 가능한 가장 작은 범위(scope)에서 선언하기
정보 은닉(information hiding) 원칙에 따라, 불필요하게 변수의 범위를 확장하지 않으면 잘못 참조해서 발생하는 오류를 줄일 수 있습니다. 또한 변수 재사용을 막아서, 코드를 더 정확하게 분석하고 구조화할 수 있습니다.
규칙 7: 결과값을 돌려주는 함수의 결과값을 반드시 확인하고, 함수에 전달된 모든 인수가 유효한지 확인하기
가장 지켜지지 않는 규칙 중 하나입니다. printf(), scanf(), close() 등의 결과값을 검사하지 않는 사람도 대부분이지만, 검사하는 것이 맞습니다. 결과값이 맞던 틀리던 상관없더라도 반드시 각 조건에 해당하는 처리 코드가 있어야 하며, 분명히 인지하고 있다면 명시적으로 함수 결과값을 (void) 문을 이용해 형변환해서 무시해야 합니다. 하지만 에러값을 돌려주는 함수는 반드시 무조건 검사해야 합니다.
규칙 8: 매크로는 파일을 포함하거나(include) 단순하게 정의할 때만 사용하기
C 전처리기는 강력하기 때문에, 그만큼 코드를 복잡하게 만듭니다. 그래서 정적 분석 도구는 물론 사람 역시 코드를 분석하고 이해하려면 매우 많은 노력이 필요하고, 이는 결과적으로 불안정하고 불확실한 코드를 생성하는 주범이 될 수 있습니다. 특히 조건 컴파일을 사용하면 코드 복잡도가 사용하는 회수만큼 높아지기 때문에 가능하면 피해야 합니다.
규칙 9: 포인터 사용 안하기, 필요하더라도 1단계 이상 참조하는 포인터는 절대 사용 안하기
typedef 선언을 이용해 2단계 이상 포인터 참조를 숨겨서도 안됩니다. 포인터는 경험많은 프로그래머라도 오용하기 쉽고, 프로그램에서 데이터 흐름을 따라가기 어렵게 합니다. 함수 포인터 역시 가능하면 사용하지 않는 것이 좋은데, 분석 도구를 사용하더라도, 포인터 유효성, 재귀 호출의 위험 등을 미리 알 수 있는 방법이 어렵기 때문입니다.
규칙 10: 컴파일시 모든 경고 메시지를 켜고, 모든 코드가 경고 없이 컴파일되도록 하기
컴파일러 역시 하나의 분석 도구라고 간주할 수 있습니다. 컴파일러가 혼란을 일으키는 코드라면 반드시 실행 중에 문제를 일으킬 수 있습니다.
물론 위 규칙을 모든 소프트웨어 개발에 적용할 수는 없겠지만, 항상 염두에 두고 있다면, 비단 임베디드 시스템 개발 뿐 아니라 모든 소프트웨어 개발 과정에서 오류를 미리 예방하는데 도움될 것이라는 점은 분명한 것 같습니다.