사이드바 열기


요 몇일 사이 정신적으로 매우 힘든 시기인것 같다.
앞으로 내가 어떻게 해야 이 전쟁터에서 살아남을까? 라는 생각을 하며 여기저기 돌아다니다 접하게 된 글이다.
출처: http://blog.naver.com/erfile?Redirect=Log&logNo=100019111041



여러분이 초보 프로그래머라면 대부분 문제가 주어지면 바로 에디터를 띄우고 코딩을 시작할 것입니다. 물론 경험이 많은 프로그래머도 바로 코딩부터 시작하기도 합니다. 그러나 프로젝트의 규모가 커지게 되면 가능할까요. 좋은 소프트웨어는 좋은 코딩 과정에서 나오게 됩니다. 이제 우리는 주먹구구식의 코딩을 벗어나 프로젝트를 수행하려 합니다. 필자가 거창하게 프로젝트라 제목을 붙인 이유도 여기에 있습니다. 이번 달에는 소프트웨어의 구현 절차를 구현과정을 중심으로 살펴보고 좋은 코드를 만들어 내기 위한 실질적인 코딩 기법들에 대해서 살펴보겠습니다.

 

소프트웨어 개발 방법


과거의 수많은 사람들이 정의한 소프트웨어 개발 방법론은 일반적으로 문제 정의, 요구사항 분석, 개발 계획, 상위 레벨 디자인, 상세 디자인, 구현, 통합, 단위별 테스트, 시스템 테스트, 수정 보완, 기능 향상의 순서로 요약될 수 있습니다. 이러한 절차는 프로젝트의 크기와 특징에 따라 적절하게 몇 개를 뭉쳐서 동시에 진행할 수도 있습니다.
<그림 1>은 일반적인 소프트웨어 라이프 사이클의 모델을 보여주고 있습니다.
소프트웨어 라이프 사이클의 절차를 이용하여 프로젝트를 설계/구현/검증하는 과정에서 실제로 볼 수도 없고 만져보지도 못하는 것에 대한 추상적인 상상은 그리 쉬운 일이 아닙니다. 그러나 새로 사온 예쁜 강아지의 집을 만든다면 필요한 것이 무엇이고 어떤 모양으로 얼마나 크게 만들지 쉽게 상상할 수 있을 것입니다. 우리는 진행하는 프로젝트를 직관적으로 이해하기 쉽도록 하기 위해서 소프트웨어 은유(metaphor)라는 것을 사용합니다. 소프트웨어 개발에 은유는 중요한 역할을 할 경우가 많습니다.
소프트웨어 은유는 지도라기보다는 서치라이트와 같아서 답을 어디서 찾을지보다는 어떻게 찾을지를 알려주지요. 그래서 은유를 알고리즘이라기보다는 휴리스틱(heuristic)이라고 말을 합니다. 알고리즘은 문제의 직접적인 해결책을 주지만 휴리스틱은 해결책을 찾을 수 있는 방법을 얘기해주죠. 모든 문제를 해결할 수 있는 해결책을 찾는 것은 불가능하나 문제에 대한 개념적인 접근은 일반적인 접근 방법의 제시가 가능합니다. 즉, 소프트웨어 은유는 소프트웨어 개발 절차의 조명을 위하여 은유를 사용해서 구조나 절차를 더 잘 이해하도록 하고, 이를 통해 더 빠른 개발과 오류의 최소화를 가능하게 합니다.
‘Computer Science Has Some of The Most Colorful Language of any field’라고 얘기합니다. 바이러스, 트로이 목마, 웜, 버그, 백신 등 컴퓨터와 관련된 수많은 은유들을 여러분은 이미 들어본 경험이 있을 겁니다. 이러한 은유는 컴퓨터 과학의 발전에 상당한 도움을 주었다는 사실을 간과할 수 없습니다. 자! 이제 프로젝트의 개발 절차를 하나씩 살펴보도록 하지요.

 

문제의 기술


우리는 프로젝트를 구현하기 전에 해결하고자 하는 시스템에 대한 명확한 문제를 기술해야 합니다. 이러한 문제의 기술은 해결에 대한 내용을 언급할 필요 없이 단지 문제들을 기술만 하면 됩니다. 중요한 것은 프로그래머의 입장이 아닌 사용자의 입장에서 문제 정의가 되어야 한다는 것이죠. 사용자 입장이 아닌 프로그래머의 입장에서 문제를 정의한다면 실제적인 구현 과정이 쉬워질 수도 있지만 시스템이 원하는 것들을 정확히 기술하기가 어렵게 됩니다. 예전에 필자도 문제를 정의하는데 프로그램으로 해결하기에 적당한 문제가 아니라는 생각에 문제를 다르게 바꿔서 해결한 적이 있었습니다. 하지만 결국은 중간에 다시 처음부터 시작을 해야 하는 결과를 초래했지요. 문제에 대한 정의의 실패는 잘못된 문제를 해결하기 위해서 수많은 시간을 투자하는 치명적인 오류를 범하게 됩니다. 그러나 한 가지 문제는 여러분이 너무나 프로그래머의 마인드를 가지고 있다는 것이죠. 문제를 정의하는 순간 만큼은 프로그래머라는 생각을 버리길 바랍니다.

 

요구사항 분석


문제를 정의한 후 세부적인 문제에 대한 분석을 통해 요구사항(requirements) 분석을 해야 합니다. 우리는 이것을 명세서(specification)라고도 합니다. 명확한 요구사항 분석이 되지 않으면 프로그래머에게 사용자가 무엇을 원하는지 추측하게 만들며 잘못된 추측의 오류를 범할 수 있게 합니다. 이러한 요구사항은 시스템의 범위(scope)를 결정하며 코딩과 테스트의 기준이 됩니다. 대형 프로젝트를 기준으로 요구사항 정의 에러는 요구사항을 결정하는 단계에서 수정하는데 비해 설계 단계에서는 5배, 코딩 단계에서는 10배, 시스템 테스트 단계에서는 20배, 최종 테스트 단계에서는 50배, 그리고 유지 보수단계에서는 100배의 노력 및 비용이 필요하다는 연구 결과가 있습니다. 그만큼 요구사항의 정의가 중요하다는 것이겠죠. 실제로 새롭게 제안된 대규모 시스템의 요구사항 분석 작업은 수많은 전문가들이 많은 시간들을 들여서 하고 있습니다.

 

구조 설계


요구사항에 대한 정의가 끝나면 실질적인 구조 설계 과정으로 들어가야 합니다. 소프트웨어 구조 설계는 소프트웨어 디자인의 상위 레벨에 위치합니다. 따라서 구조 설계를 일반적으로 상위 레벨 디자인(high-level design, top-level design)이라고 합니다. 일부 프로그래머들은 상위 레벨 디자인의 중요성에 대해 문제를 제기하기도 합니다. 물론 조그만 프로그램에서는 별 필요성이 없을 수도 있겠죠. 하지만 프로젝트가 커지게 되면 이러한 상위 레벨 디자인은 반드시 필요하게 됩니다. 큰 프로젝트의 디자인은 실질적인 코딩이 진행되기 전에 디자인 단계를 거치게 되는 것이 보통입니다. 하지만 조그만 프로젝트에서는 프로그래머가 키보드 앞에서 직접 루틴을 정의하고 각각의 루틴에 대해서 코딩전에 PDL(Program Design Language)이나 몇몇 중요한 루틴에 대한 다이어그램을 그리는 방법을 이용하기도 합니다.


구조 설계는 시스템의 개념적인 통합으로 궁극적으로 시스템의 질을 결정짓게 되죠. 또한 좋은 설계는 구현을 쉽게 만듭니다. 구조 설계에서는 프로그램에서 중요한 모듈들을 정의하게 됩니다. 이러한 모듈은 루틴들의 집합으로 정형화된 출력 기능을 하는 모듈, 명령을 분석하는 모듈, 메인 자료구조를 접근하는 모듈 등을 예로 들 수 있습니다. 잘 정의된 모듈은 하나의 태스크만을 수행해야 하며 모듈간의 상호작용을 최소화해야 하고 모듈간에 서로 알아야 할 필요성을 최소화해야 합니다. 여기서 모듈의 설계도 중요하지만 모듈간의 인터페이스 또한 매우 중요합니다. 모듈간에 직접적인 호출이 가능한 것과 간접적인 호출 및 호출이 불가능한 것에 대한 기술이 필요하며 모듈간에 주고받는 데이터에 대하여 명확히 정의해야 합니다.


소프트웨어 개발의 가장 기본적인 해결책은 구현 대신에 구입하는 것이죠. 구조 설계시에 이미 존재하는 모듈은 가능하면 구입하여 사용할 수 있도록 하는 것이 좋습니다. 기존에 존재하는 모듈의 재사용은 상당한 개발비용과 개발시간의 단축을 가능하게 합니다. 특히 이미 검증된 코드이기 때문에 검증 단계를 쉽게 만들어 주죠.


구조 설계시에는 사용될 주요 자료구조도 묘사돼야 합니다. 자료구조를 선택할 때에는 깊은 숙고를 통해 선택해야 하며, 선택된 이유를 설계 단계에서 명시해야 구현 단계나 유지관리할 때 이해를 높일 수 있습니다. 또한 구조 설계시에 특별한 알고리즘을 이용할 경우 그 알고리즘에 대한 설명이나 참고에 대한 인덱스가 필요하며 알고리즘의 선택시에 고려된 내용들에 대한 명시가 필요합니다. 이외에 설계시에 일반적으로 고려해야 하는 것들은 사용자 인터페이스, Input/Output 구조, 메모리 관리 등이 있습니다. 특히 에러/예외 처리에 대한 고려는 매우 중요하게 인식되고 있습니다. 실제로 코드의 90%가 예외처리를 위한 코드이며 단지 10%만이 정상적인 경우를 위한 코드라는 분석 결과도 있습니다.

 

구현


앞으로 세부 디자인과 코딩 그리고 각 유닛에 대한 테스트까지의 일련의 과정을 구현이라고 하겠습니다. 구현 단계는 일반적으로 소프트웨어 총 개발 시간의 30 ~80% 부분을 차지하며 소프트웨어 개발의 중심에 위치하고 핵심이 되는 과정입니다. 이 단계는 프로그래머의 능력에 따라 많은 부분이 좌우되고 프로그래머가 아니면 할 수 있는 일이 아니죠. 구현 단계를 성공적으로 수행하기 위해서는 기초 작업의 검토 및 루틴과 모듈의 디자인, 데이터 타입과 변수 이름의 결정, 세부 코드 블럭의 제어구조와 구성을 선택, 디버깅, 세부 디자인과 코드의 재검토, 코드 튜닝, 코드의 주석 및 포맷팅 등의 일이 필요하게 됩니다.

 

세부 디자인


구조 설계가 끝나고 나서 실제 코딩 전에 설계된 모듈에 대한 세부 디자인(Detailed Design, Low-level Design)이 이뤄져야 합니다. 세부 디자인의 일반적인 순서는 루틴을 디자인하고 이를 검토하며 디자인된 루틴을 코딩한 후 검토하는 일련의 사이클로 이뤄집니다.
<그림 3>은 루틴 디자인을 할 때 필요한 일들을 나타내고 있습니다. 그림에 나타난 요소들은 진행 순서에 대한 특별한 제약없이 진행될 수 있습니다.

 

코딩


루틴의 코딩은 PDL을 기반으로 시작하게 됩니다. 가장 먼저 루틴에 대한 선언을 기술해야 합니다. 즉, 인터페이스와 헤더를 기술하는 것입니다. 여기서 PDL로 기술한 헤더에 대한 설명은 바로 코드의 주석으로 사용합니다. 이렇게 인터페이스를 기술한 후에 루틴의 PDL을 라인별로 주석으로 바꾸면서 주석 바로 밑에 코드를 채워 나갑니다. 코딩을 하며 필요한 주석을 추가할 수 있겠죠.

 

코드의 검증


코딩된 소스는 결코 완벽하지 못하므로 여러 가지 오류를 포함하게 됩니다. 이러한 오류는 통합된 후 찾기란 결코 쉽지가 않습니다. 따라서 루틴의 검토 단계에서 반드시 오류들을 해결해야만 합니다. 루틴의 디버깅은 먼저 눈으로 각각의 패스를 따라가며 가상으로 실행 시켜 보며 에러를 잡습니다. 이때 일반적인 경우와 데이터의 경계 부분 그리고 예외상황에 대한 검토가 이뤄져야겠죠. 주로 에러는 데이터의 경계 부분에서 발생하게 됩니다. ‘0’이라던가 ‘MAX’ 값이라던가 ‘NULL’에 대한 검토가 반드시 필요하다는 얘기입니다. 만약 루틴이 방대하다면 이러한 가상 실행을 통한 검토가 힘들어지므로 루틴은 가능한 작게 만들어야 합니다.
이러한 검토를 통한 오류 정정 후 루틴 컴파일을 통한 컴파일 에러 디버깅을 거쳐야 합니다. 컴파일까지 완료된 소스는 디버거 툴 등을 통한 라인별 디버깅을 한 후 다양한 테스트 케이스를 이용하여 루틴에 대한 실행 검증을 해야 합니다. 이렇게 한다면 100%는 아닐 수 있겠지만 거의 완벽한 루틴을 만들어 낼 수 있게 됩니다. 루틴의 크기는 보통 한 두 페이지 정도로 약 66에서 132라인 정도가 적당하다고들 얘기를 합니다. 크기를 더 줄이기 위한 노력은 추가 비용이 들어갈 뿐만 아니라 인터페이스의 복잡성으로 인해 에러를 발생시킵니다. 그러나 200라인 이상의 코드는 에러를 증가시키는 것은 물론 디버깅의 비용을 상당히 증가시키므로 루틴의 특성에 적합한 너무 길지 않은 코드를 만들어야 합니다.

 

코딩의 질을 높이자


여기서는 실제적인 코딩 기법에 대해서 다루고자 합니다. 이러한 기법들에 대한 글들이 많지 않아 상당히 많은 프로그래머들은 실제 프로젝트를 하며 경험에 의존하여 이러한 기법들을 터득하게 됩니다. 물론 경험을 통해 배우는 것만큼 확실한 것은 없겠죠. 그러나 미리 이러한 것들을 몸에 익히고 코딩에 임한다면 더욱 빠른 시간 내에 양질의 코딩을 하는 프로그래머가 될 수 있을 것입니다. 그러나 이러한 내용 외에도 상당히 많은 부분은 여러분이 다른 코드를 분석하고 실제로 프로그래밍을 하며 배워야 합니다.


일반적인 주석 기법


이 글을 읽는 상당히 많은 분들은 주석을 별로 좋아하지 않을 것입니다. 주석을 단지 매우 귀찮은 존재로서 생각하고 있을 터입니다. 그러나 주석이 없는 코드는 마치 암호와 같아서 코딩을 한 본인도 시간이 지나면 분석하기가 매우 어려워지게 됩니다. 여러분이 앞서 본 것처럼 주석은 코딩을 한 후에 하는 문서 작업이 아니라 코딩을 하기 전 설계 과정에 하는 것입니다. 즉 주석은 코딩을 위해 필요합니다. 나중에 코드에 대한 이해를 돕는 것은 주석의 부수적인 기능이죠. 그렇다면 주석에도 기본적인 형식이 있을까요? 그렇습니다. 물론 어떻게 해야 한다는 강요는 아니지만 일반적으로 많이 사용하는 방법들이 있다는 얘기입니다. 여기서는 필자가 사용하는 주석 기법을 예로 들겠습니다. 본인만의 주석 기법을 정해두고 사용하기 바랍니다.
먼저 파일의 헤더에 대한 주석 방법을 알아보도록 하겠습니다. <리스트 1>은 파일의 일반적인 주석 방법을 보여주고 있습니다. 여기서 ‘REF’는 관련된 파일을 적어두고 나중에 유지보수를 편하게 하도록 하기 위하여 기술하며, ‘DESCRIPTION’은 파일의 구조와 기본 기능에 대한 설명, 중요한 함수 및 변수(특히 외부에 보여지는 함수 및 전역변수)들에 대해 기술을 합니다. ‘NOTES’는 파일 사용시에 주의해야 할 사항이나 특이사항 등에 대한 기술을 합니다. 그리고 ‘REVI SION HISTORIES’는 파일을 유지관리하기 위해 버전 정보, 수정 일자, 수정 내용을 기록해 둡니다.
다음은 함수의 헤더에 대한 주석 방법을 알아보도록 하겠습니다. 이것은 파일의 헤더 주석과 달리 INPUT, OUTPUT에 대한 정의를 반드시 해야 합니다. 함수로 입력되는 매개변수에 대한 설명과 전역변수에 대한 사용시에 이를 반드시 기술하기 바랍니다. 하지만 함수 버전에 대한 정보는 기술할 필요가 없겠죠.
<리스트 3>은 기타 여러 가지 주석에 대한 예입니다. 변수에 대한 설명 if, switch, while문 블럭의 끝을 알려주는 주석 그리고 전체적인 기능 단위를 묶어주는 주석 등에 대한 예를 보여주고 있습니다.
이외에도 다양한 주석문의 사용 방법이 있습니다. 다음 달에 우리가 수행할 프로젝트의 실제 코드를 보면서 더욱 자세한 것들을 살펴보기 바랍니다.

 

코드 레이아웃


코드 레이아웃은 매우 다양한 방법들이 사용되고 있습니다. <리스트 4>는 일반적인 경우의 예를 보여주고 있습니다.
예 1은 코드의 하나의 라인은 한 가지 기능만을 해야 한다는 것을 보여주고 있으며, 예 2는 블럭을 정의하는 일반적으로 많이 쓰이는 두 가지 방법을, 예 3은 수직 정렬의 예와 적절한 공백라인의 활용을 보여주며, 예 4는 코드의 한 문장이 라인을 넘을 때 처리하는 방법을 보여주고 있습니다. 예 5의 경우는 매개변수가 많을 경우 아래쪽에 보여진 방식이 유리하다는 것을 눈으로 확인할 수 있을 것입니다. 예 6은 실행코드뿐 아니라 프리프로세서도 인덴테이션(Endentation)이 필요하다는 것을 보여주고 있습니다.

 

에러 처리 기법


소프트웨어를 더욱 견고하게 만들기 위해서 에러에 대한 처리는 매우 중요합니다. 이를 위해 모든 루틴의 입출력은 반드시 우리가 원하는 제한된 값인지 검증돼야 하고 프로그램이 돌아가는 동안에 변수나 함수의 리턴 값 등에 대한 신뢰를 할 수 있어야 합니다. 우리는 이러한 경우에 ‘Assert’ 함수를 이용합니다. 이는 예상치 않은 조건에 대하여 대비하자는 목적입니다. Assert 함수는 개발 환경에 따라 기본적으로 제공하는 경우도 있습니다. Assert 함수는 조건이 참이 아닐 때 에러 메시지를 출력하고 소프트웨어를 중지하는 기능을 합니다. 실제 사용 방법은 다음과 같습니다.


Assert( FileOpen( InputFile ) != NULL, "Could not open input file" );

다음은 일반적으로 에러를 처리하는 방법으로 에러 코드에 의한 방법을 사용합니다. 에러가 발생했을 때 단지 프로그래머가 작성한 에러처리 루틴만을 호출하게 되면 프로그래머의 의도대로 발생한 에러에 대한 처리 및 발생된 에러의 정보를 출력해줄 수 있게 됩니다. <리스트 5>는 일반적으로 사용하는 에러 코드에 대한 정의 및 해당하는 에러 정보를 출력해주는 루틴입니다.


네이밍 기법


네이밍(Naming) 기법이란 변수명, 함수명 등에 대한 이름을 짓는 것입니다. 네이밍 방법은 매우 다양한 방법이 있기 때문에 프로그래머 자신이 선호하는 방법을 사용하면 됩니다. 그러나 중요한 것은 언제나 동일한 네이밍 방법을 사용해야 한다는 것입니다. 파일마다 다른 방법을 사용한다면 코드를 읽는 사람에게 엄청난 혼동을 주게 될 것입니다. 큰 프로젝트는 수많은 변수와 함수, 상수 등이 존재합니다. 따라서 단지 함수나 변수의 이름만 가지고 추측을 하게 되는 경우가 빈번히 나타나게 되므로 이런 상황에서 네이밍은 매우 중요합니다.
이제 예제를 통해 실제로 네이밍하는 방법을 살펴보도록 하겠습니다. 다음 예제는 필자가 사용하는 방법대로 표현했습니다. 변수의 시작은 소문자, 단어의 구분은 대문자를 이용하며 포인터 변수일 때는 변수 이름 앞에 p를 추가하여 네이밍을 합니다. 함수 이름은 변수와 달리 대문자로 시작하였으며 상수와 직접 정의한 데이터 타입 이름 모두 대문자로 표현되고 단어의 구분을 위해 “_”를 사용합니다.


int dataSize, *ptxData,
BOOL TxData();
BOOL RxData();
#define TIME_OUT 1
typedef struct
{
int xPosition;
int yPosition;
int width;
int height;
} RECT

 

여기서 필자가 강조하고 싶은 내용은 함수의 기능적인 그룹을 표현하는 네이밍 기법입니다. 함수 이름을 이용하여 함수들을 기능별로 그룹을 짓는 것을 얘기합니다.
예를 들어 데이터를 주고받는 통신 시스템에서 송신단과 관련된 함수를 TX_xxx TX_yyy, TX_zzz 형식으로, 수신단과 관련 함수를 RX_xxx, RX_yyy, RX_zzz 형식으로 이름 짓는 방법은 매우 유익한 기법입니다. 일반적으로 임베디드 시스템에서는 다양한 RT OS(Real Time Operating System)의 포팅을 쉽게 하기 위해 OS에 의존적인 함수를 OS_function Name의 형태로 네이밍을 합니다.

 

개발 과정의 습관화


지금까지 우리는 프로젝트의 문제 정의부터 코딩까지 실질적인 구현 절차를 살펴보았습니다. 또한 구현 단계에서 필요한 여러 가지 코딩 기법들을 설명했습니다. 이러한 내용은 단지 읽고 이해한다고 되는 것이 아닙니다. 많은 경험과 노력 속에서 몸에 익고 습관화가 되어야만 진정한 여러분 것으로 남게 되는 것입니다. 다음 달에는 지금까지 배운 절차를 이용해 숫자 퍼즐을 맞추는 지능을 가진 프로그램을 구현해보도록 하겠습니다.

정리 : 박은정 whoami@sbmedia.co.kr


PDL
PDL은 생각들의 집합을 간결한 글로서 진술해 놓은 것을 말합니다. 다음은 PDL의 간단한 예입니다. 이러한 PDL은 코딩 동안에 주석으로 바뀌게 됩니다. PDL의 간단한 예를 살펴보겠습니다.

 

/* 헤더에 대한 PDL */
This routine outputs an error message based on an error code supplied by the calling routine. The way it outputs the message depends on the current processing state, which it retrieves on its own. It returns a variable indicating success of failure.

/* 루틴에 대한 PDL */
Set the default status
look up the message based on the error code
if the error code is valid
determine the processing method
if doing interactive processing
print the error message interactively
and declare success
else doing batch processing
if the batch message file opens properly
log th error message to the batch file.
close the file, and declare success
else the message code is not valid
notify the user that an internal error has been defected


출처 : http://www.imaso.co.kr

Posted by ilwoongkang
이전페이지 1 2 3 4 5 ... 30 다음페이지
위로

사이드바 열기