[출처 : http://ryu2811.tistory.com/23]


__attribute__ ((packed)) : 구조체 내의 최대 사이즈 데이터 타입으로 alignment 하지 않도록한다.

************************************************************
#include <stdio.h>

typedef struct _test_t_
{
    unsigned char uc1;
    unsigned char uc2;
    unsigned long long ui1;
} test_t;

typedef struct _testp_t_
{
    unsigned char uc1;
    unsigned
    char uc2;
    unsigned long long ui1;
} __attribute__ ((packed)) testp_t;

int main (void)
{
    test_t t1;
    testp_t t2;

    printf ("test_t : %d\n", sizeof (t1));
    printf ("testp_t : %d\n", sizeof (t2));

    return 0;
}
************************************************************
[ryu2811@qwertyryu-home ~/work/src/test] ./kkk
test_t : 16
testp_t : 10

[출처 : http://studyfoss.egloos.com/5409933]


gcc: 4.5.0
arch: x86_64


정렬 제한이란 데이터가 메모리에 저장될 때 해당 메모리의 주소에 대한 제약 사항을 말하는 것이다.
구체적으로는 데이터가 저장될 위치가 특정한 단위로 정렬(배치?) 되어 있어야 한다는 것을 뜻한다.

예를 들어 CPU 아키텍처에서 메모리 접근 시에 32비트 단위로 정렬된 주소에만 접근할 수 있다고 하자.


위의 데이터 A는 주소값이 4의 배수 형태이므로 정렬되어 있지만, B의 경우는 그렇지 않다.
B의 경우 해당 데이터에 접근 시 실행 환경에 따라 다음과 같은 여러 가지 결과를 일으킬 수 있다.
  • 비정상적인 메모리 접근을 인식하여 OS가 프로그램을 종료시킨다.
  • CPU 혹은 컴파일러가 B가 속한 두 메모리 영역에 접근한 뒤 적절한 비트 연산을 거쳐 원하는 값을 만들어준다.
  • 주소를 적당히 4의 배수로 변경하여 메모리에 접근한다.
  • 별 문제없이 실행된다.

어떤 경우이든 정렬되지 않은 메모리에 대한 접근은 성능에 도움이 되지 않는다.
최악(?)의 경우 B가 걸쳐있는 두 메모리 워드가 각자 다른 캐시 라인에 속할 수도 있음을 생각해 보자.
따라서 컴파일러는 컴파일 시에 이러한 정렬 제한을 고려하여 데이터를 메모리 상에 배치한다.
(물론 최종적으로는 링커가 이러한 작업을 수행한다.)

C 언어의 기본 데이터 타입은 모두 자신의 크기와 동일한 정렬 제한을 가진다.
gcc에서 이러한 정보는 sizeof와 __alignof__를 통해서 얻을 수 있다.

/* align-basic.c */

#include <stdio.h>

char c;
short s;
int i;
long l;
float f;
double d;

int main(void)
{
#define print_align(type, var)                        \
  printf(#type "\t%zd\t%zd\t%p\n", sizeof(type), __alignof__(type), &var)

  printf("type\tsize\talign\taddress\n");
  print_align(char, c);
  print_align(short, s);
  print_align(int, i);
  print_align(long, l);
  print_align(float, f);
  print_align(double, d);

  return 0;
}

위의 예제 프로그램을 빌드한 후 실행해보면 다음과 같은 결과를 얻을 수 있다.

$ gcc align-basic.c
$ ./a.out
type     size    align    address
char     1       1        0x40209a
short    2       2        0x402098
int      4       4        0x402094
long     8       8        0x402088
float    4       4        0x402090
double   8       8        0x402080

데이터 타입의 크기와 정렬 제한은 모두 동일한 크기를 가지며
실제 변수가 생성된 주소도 이러한 정렬 제한에 맞추어 졌음을 볼 수 있다.
좀 더 자세히 살펴보면 동일한 크기/정렬 제한을 가지는 데이터끼리 묶어서 배치하였으며
공간을 효율적으로 사용하기 위해 큰 크기/정렬 제한을 가지는 데이터부터 배치한 것도 볼 수 있다.

정렬 제한은 포인터 변환 시에도 반드시 고려해야 할 부분인데
cast 연산을 통해 강제로 변환한 주소값이 변환된 포인터가 가리키는 데이터 타입의
정렬 제한을 어길 수 있기 때문이다. 예를 들어 위의 예제에서
변수 i의 주소를 long * 타입으로 변환한다면 long 타입이 요구하는 8 바이트의 정렬 제한을
만족시키지 못하므로 해당 포인터를 통해 메모리에 접근 시 위에서 언급한 결과 중의 하나를 얻게 될 것이다.

이제 본격적으로 구조체에 대해서 살펴보기로 하자.
구조체는 여러 데이터 타입을 모아둔 집합체의 개념이므로 구조체를 이루는 각 멤버에 대해서도
위에서 언급한 정렬 제한을 모두 만족시켜야 한다. 또한 구조체는 위의 예제에서와 같이
메모리를 효율적으로 사용하기 위해 구조체 멤버의 순서를 임의로 조정할 수 없으며
반드시 구조체 선언 시에 명시된 순서대로 멤버를 메모리 상에 배치해야 한다.
따라서 이를 조정하기 위해 멤버 사이에 사용하지 않는 패딩 공간을 할당할 수 있다!!

구체적인 예를 통해 살펴보기로 하자.

/* align-struct.c */

#include <stdio.h>

struct sc {
  char c;
} sc;

struct sl {
  long l;
} sl;

struct scl {
  char c;
  long l;
} scl;

struct slc {
  long l;
  char c;
} slc;

int main(void)
{
#define print_align(type, var)                        \
  printf(#type "\t%zd\t%zd\t%p\n", sizeof(type), __alignof__(type), &var)

  printf("type\t\tsize\talign\taddress\n");
  print_align(struct sc, sc);
  print_align(struct sl, sl);
  print_align(struct scl, scl);
  print_align(struct slc, slc);

  return 0;
}

실행 결과를 살펴보기 전에 먼저 결과가 어떻게 나오게될 지 생각해 보자.
멤버로 오직 하나의 데이터 만을 가지고 있는 sc와 sl의 경우는
단순히 해당 멤버의 크기와 정렬 제한을 그대로 갖게될 것이다.
하지만 scl과 slc와 같이 서로 다른 데이터 타입으로 이루어진 경우라면 어떨까?

위의 예제를 실행해 보면 다음과 같은 결과를 얻을 수 있다.

$ gcc align-struct.c
$ ./a.out
type          size    align    address
struct sc     1       1        0x4020a8
struct sl     8       8        0x4020a0
struct scl    16      8        0x402080
struct slc    16      8        0x402090

앞서 말한대로 sc와 sl의 경우에는 특별한 사항이 없다.

scl의 경우에는 우선 정렬 제한이 8이라고 나오는데 이는 멤버 중의 가장 큰 정렬 제한을
가지는 long 타입의 정렬 제한을 그대로 물려받은 것이다.
또한 크기는 9가 아닌 16으로 나오는데 이는 8 바이트 단위로 정렬된 위치에 첫 멤버인
char 타입의 데이터가 저장되고 그 이후에 long 타입의 데이터가 다시 8바이트 단위로
정렬된 위치에 저장되기 때문이다. 즉 1~7번 바이트는 사용되지 않고 단순히 long 타입의
멤버의 정렬 제한을 보장하기 위한 용도로 채워진 패딩 바이트인 것이다.

구조체 내의 각 멤버의 위치는 offsetof 매크로를 통해 조사할 수 있는데
위의 경우 offsetof(struct scl, l)의 값은 8이 될 것이다.

slc의 경우에는 마찬가지로 정렬 제한이 8이고 크기가 16으로 나오는데
정렬 제한이야 그렇다 하겠지만 크기는 왜 9가 아닌 16으로 나오는 것일까?
만약 크기가 9이더라도 long 타입과 char 타입의 정렬 제한을 이미 모두 만족시키고 있는데
왜 굳이 구조체 뒤쪽에 불필요한 패딩 바이트를 추가하여 크기를 크게 만들었을까?

그 해답은 바로 배열 때문이다.
알고 있듯이 배열은 구조체와 같은 집합체이지만 동일한 데이터 타입으로 이루어진 것이며
중요한 사항은 배열을 이루는 각 원소들은 메모리 상에서 연속된 영역에 존재해야 하며
각 원소들 자체에 대해서도 정렬 제한을 만족시켜야 한다는 점이다.
만일 slc의 크기가 9가 된다면 배열의 다른 요소들은 정렬 제한을 만족하지 못할 것이므로
slc 내에 패딩을 포함시켜 slc의 크기 자체를 정렬 제한의 배수가 되도록 맞춘 것이다.

이렇게 구조체 내의 패딩 바이트가 추가되는 규칙을 알고 있다면
구조체 선언 시 각 멤버들의 위치를 잘 선택하여 효율적인 메모리 배치를 이루도록 할 수 있을 것이다.

하지만 상황에 따라 이러한 구조체의 크기/정렬 제한을 조정해야 하는 경우가 있을 수 있는데
(일반적으로는 그리 추천할 만 한 방법은 아닐 것이다)
이 때는 gcc에서 확장 기능으로 제공하는 aligned 혹은 packed 속성을 이용하면 된다.

역시 예제를 통해 살펴보기로 하자.

/* align-adjust.c */

#include <stdio.h>

struct asc {
  char c __attribute__((aligned(8)));
} sc;

struct __attribute__((aligned)) asl {
  long l;
} sl;

struct __attribute__((packed)) pscl {
  char c;
  long l;
} scl;

#pragma pack(push, 2)
struct pslc {
  long l;
  char c;
} slc;
#pragma pack(pop)

int main(void)
{
#define print_align(type, var)                        \
  printf(#type "\t%zd\t%zd\t%p\n", sizeof(type), __alignof__(type), &var)

  printf("type\t\tsize\talign\taddress\n");
  print_align(struct asc, sc);
  print_align(struct asl, sl);
  print_align(struct pscl, scl);
  print_align(struct pslc, slc);

  return 0;
}

앞서 말한대로 구조체의 정렬 제한은 멤버 중 가장 큰 값을 물려받는다.
따라서 구조체의 정렬 제한을 변경하는 대신 멤버의 정렬 제한을 변경한 경우에도
구조체에 영향을 미치게 된다. aligned 속성에는 2의 제곱수에 해당하는 숫자 만을
인자로 사용할 수 있으며 기본 정렬 제한보다 큰 값을 적용하는 경우에만 의미가 있다.
인자를 생략한 경우 실행 환경에 가장 적합한 정렬 제한을 가지도록 컴파일러가 결정한다.
반대로 packed 속성의 경우에는 정렬 제한을 가장 작은 수인 1로 줄이게 되며
1이 아닌 값을 적용하기 위해서는 #pragma를 이용할 수 있다.

위의 예제를 실행하면 다음과 같은 결과를 얻을 수 있다.

$ gcc align-adjust.c
$ ./a.out
type          size    align    address
struct asc    8       8        0x4020a8
struct asl    16      16       0x402080
struct pscl   9       1        0x40209a
struct pslc   10      2        0x402090

구조체에 비트 필드가 추가되는 경우 보다 복잡/미묘한 상황이 발생할 수 있는데
이에 대해서는 "더 이상의 자세한 설명은 생략한다".. ;;


=== 참고 문헌 ===
저자: 한동훈

[지난기사보기]
프로그래밍 스타일(6)
프로그래밍 스타일(5)
프로그래밍 스타일(4)
프로그래밍 스타일(3)
프로그래밍 스타일(2)
프로그래밍 스타일(1)

2.12 괄호 형식

괄호형식은 크게 다음과 같은 형태가 가장 많이 쓰인다.

for(int i = 0; i < 100; i++) {
   // some code
}

for(int i = 0; i < 100; i++)
{
   // some code
}

필자는 대부분의 언어에서 두번째와 같은 형식을 사용한다. 첫번째와 같은 형식은 구조가 복잡할수록 괄호의 위치를 찾기가 어려워지고, 로직보다는 괄호의 위치에 더 신경을 쓰게한다. 그러나 아직도 많은 C, Java 프로그래머들은 이러한 위치를 선호한다. 물론 첫번째와 같은 방법을 쓰면 코드의 라인수는 줄일 수 있지만, 수행 성능에는 영향을 미치지 않는다. 원하는 형식을 사용할 수 있으며, 개인적으로는 두번째와 같은 형식을 권한다.

VB, Pascal과 같은 언어에서는 위와 같은 중괄호를 사용하여 블록의 시작과 끝을 표현하지 않는다. Python과 같은 언어는 괄호 대신에 들여쓰기로 이를 대신하는 독특한 표기법을 사용한다.

2.12.1 if 문의 괄호형식

경우에 따라 다음과 같은 형태의 괄호 형식도 널리 사용된다.

if(A) {
} else if( B) {
} else {
}

일부 프로그래머는 함수 선언에는 함수 선언 다음 줄에 시작중괄호를 두고, if 문등에는 문장의 끝에 중괄호를 두는 것과 같이 두 가지 스타일을 혼용하기도 한다. 필자는 C/C++ 언어를 사용하는 경우에 이 두가지 스타일을 혼용하고 있다.

2.13 코드를 절과 단락으로 분리하기

대부분의 사람들이 작성한 코드를 살펴보면 코드들이 라인을 분리하지 않고 다닥다닥 붙여서 코딩하는 것을 쉽게 볼 수 있다.

' We have another branch. Solve the branch recursively
   If Not rsSubtopics.EOF then
      Response.Write("<TD><A onClick=""Toggle(this)""><IMG SRC=""minus.gif""> " + topic + "</A><DIV>")
      Do While Not rsSubtopics.eof
      subtopicID = rsSubtopics("topicID")
      subtopic = rsSubtopics("name")
      call SubTopicsInTree(dc, subtopicID, subtopic, depth + 1)
      rsSubtopics.movenext
      Loop
         ' We have a leaf. Simply print the leaf
   Else
Response.Write("<TD><IMG SRC=""leaf.gif""> " + topic + "<DIV>")
   End If

직관적인 코드라고 볼 수도 없으며, 단락으로 구분해야할 것을 다닥다닥 붙여놓아서 알아보기가 힘들다. 보다 읽기 쉬운 코드는 다음과 같을 것이다.

' We have another branch. Solve the branch recursively
If Not rsSubtopics.EOF Then

   Response.Write("<TD><A onClick=""Toggle(this)""><IMG SRC=""minus.gif""> " + topic + "</A><DIV>")

   Do While Not rsSubtopics.eof?

      subtopicID = rsSubtopics("topicID")
      subtopic = rsSubtopics("name")

      Call SubTopicsInTree(dc, subtopicID, subtopic, depth + 1)
      rsSubtopics.MoveNext
   Loop

' We have a leaf. Simply print the leaf
Else
   Response.Write("<TD><IMG SRC=""leaf.gif""> " + topic + "<DIV>")
End If

분명히 이와 같은 코드는 라인수는 더 길게 나온다. 하지만 각 코드가 단락에 따라서 구분되어 있기 때문에 코드를 보다 쉽게 읽을 수 있다. 코드는 결코 딱딱하게 머리 싸매면서 봐야하는 것이 아니다. 코드는 소설처럼 쉽게 읽을 수 있도록 절과 단락으로 적절히 구분되어 있어야한다.

2.14 주석은 코드의 내용을 기술하는 것이 아니다

먼저 2.13에서 살펴본 코드에서 if 문을 다시 한 번 보자.

If Not rsSubtopics.EOF then

이 주석에 대해서 여러분이 주석을 달게 된다면 어떻게 할까? 아마도 다음과 같이 주석에 쓰기 쉬울 것이다.

‘Subtopics 레코드셋에 레코드가 있다면

또 다른 코드를 살펴보자.

Employee = Employee + 1;

이 코드에 대한 주석은 보통 다음과 같이 할 것이다.

// Employee에 1을 더한다.

그러나 올바른 주석은 코드를 해석하는 것이 아니라, 코드가 행하는 내용이 무엇인지를 써야한다. 먼저, if 문에 대한 주석은 다음과 같을 것이다.

‘ 트리구조에서 노드를 갖고 있는 경우. 노드는 재귀적으로 해결한다.

두번째 Employee에 대한 주석은 다음과 같이 내용을 알 수 있게 바꿔야한다.

// 다음 종업원을 처리한다.

어떤 변수에 1을 더하거나 레코드셋에서 루프를 도는 것과 같은 것들은 굳이 주석으로 쓰지 않아도 알 수 있다. 그러나 그것이 어떤 의미인지 코드를 하나하나 이해하기는 어렵다. 주석에는 이 코드가 어떤 의미를 갖는지 그 내용을 쓰도록 해야한다.

심지어 필자는 단 30줄도 안되는 함수에 대한 주석을 3 페이지씩 썼던 적도 있다.

2.15 매크로 사용

예를 들어서, 엔진을 제어하는 코드를 작성한다고 할 때, 엔진의 최대속도가 80km/h이고, 엔진의 속도가 80km/h를 넘으면 엔진이 고장났다는 메시지를 출력한다고 하자.

$current_speed = 60;

if ( $current_speed < 80 )
{
   print "engine is workingn";
}

else
{
   print "engine is out of order!n";
}

그런데 엔진을 제조하는 기술이 발전해서 엔진의 최대속도가 120km/h가 되었다면 80이라는 숫자를 120으로 변경해야할 것이다. 만약, 이렇게 직접 숫자를 사용한 부분이 코드에서 여러곳이라면 모든 부분을 일일이 찾아서 수정해야한다. 이렇게 수정하다보면, 깜빡하고 수정하는 것을 잊어버린 코드도 생기게 되고, 버그의 원인이 된다. 이러한 꼉우에는 80과 같이 숫자를 직접 사용하는 대신에 전역 상수를 선언하도록 해야한다.

$SPEED_LIMIT = 100;

$current_speed = 60;

if ( $current_speed < $SPEED_LIMIT )
{
   print "engine is workingn";
}

else
{
   print "engine is out of order!n";
}

이와 같이 작성하면 나중에 엔진의 최고 속도를 수정할 필요가 생기더라도 한 곳만을 수정하면 된다.

이러한 방법은 대부분의 언어에서 모두 사용할 수 있다.(위 코드는 Perl로 작성했다)

자, 이제 다른 것을 논의해보자. 만약에 여러분의 응용 프로그램이 20명의 종업원을 다루도록 했다고 하자. 방금 배운 지식을 활용해서 다음과 같이 작성했다고 하자.

Private Const MAXEMPLOYEE = 20

Public Sub PrintAllEmployeeName()

   Dim LoopCtr As Integer
   Dim Employee(MAXEMPLOYEE) As New Employee

   For LoopCtr = 1 To UBound(Indx)
      Debug.Print Employee(LoopCtr).Name
   Next LoopCtr

End Sub

VB와 같이 대부분의 언어는 배열을 0에서 시작하기 때문에 이 경우에는 21개의 Employee 정보가 들어갈 수 있게 된다. 이와 같은 것이 혼동스럽다면 다음과 같이 조금 더 고쳐쓸 수 있다.

Private Const EMPLOYEENUMBER = 20
Private Const MAXEMPLOYEE = EMPLOYEENUMBER - 1

Public Sub PrintAllEmployeeName()

   Dim LoopCtr As Integer
   Dim Employee(MAXEMPLOYEE) As New Employee

   For LoopCtr = 1 To UBound(Indx)
      Debug.Print Employee(LoopCtr).Name
   Next LoopCtr

End Sub

위와 같이 간단한 팁을 사용하여 프로그래머가 이해하기 쉽도록 할 수 있다.

위 코드에서 한 가지 더 보아야할 것은 For 부분이다. 대부분의 프로그래머들은 이 부분을 다음과 같이 사용할 것이다.

For LoopCtr = 1 To MAXEMPLOYEE

와 같이 매크로 상수를 사용하거나 직접 숫자를 넣어서 사용할 것이다. 그러나 배열에 항상 정해진 수만큼 값이 들어있는 것은 아니다. 배열에 최대한 넣을 수 있는 값은 20개지만, 실제로는 14개만 들어가 있는 경우가 더 많을 것이다. 이러한 경우에 배열에서 가장 큰 범위값을 가져와서 이용하는 것이 보다 융통성있는 코드를 작성할 수 있도록 해준다.

2.15.1 for 루프

for( k = 0; k < 10; k++ )

for( k = 0; k != 10; k++ )

첫번째와 두번째의 차이점을 아는가? 첫번째는 k가 10번 이상만 수행되면 종료되는 루프이다. 두번째 루프는 루프가 끝난 시점에서 k = 10이 되기 때문에 k의 값이 루프를 수행한 횟수를 담고 있다는 것을 알 수 있다는 장점이 있다.

- for루프와 불변식에 대해서는 다음에 다룰 기회를 갖도록 하자

2.16 round-off 에러

최근에 VB는 부정확한 언어라고 하여 산수 계산도 제대로 못한다라는 기사를 본적이 있다. 그러한 기사가 인터넷을 통해서 전세계에 널리 퍼졌던 것을 보고 배꼽잡고 웃지 않을 수 없었던 일이 있다. 다음과 같은 코드를 실행시켜 보도록 하자.

Dim Result As Single
Dim LoopCtr As Integer
Result = 0

For LoopCtr = 1 To 10000

   Result = Result + 0.00001

   Next LoopCtr

Label1.Caption = Result

VB에서 레이블을 하나두고 다음과 같은 코드를 실행시켜보도록 한다. 0.00001을 1 만 번 반복해서 더하는 것이니 결과는 0.1이 나와야 할 것이다. 불행히도 결과는 0.0999915와 같이 된다. 이러한 오차가 생기는 것은 숫자를 이진수로 표현하는 컴퓨터의 표현 방식에 따른 것이다. 따라서 컴퓨터로는 0.1과 같은 간단한 수 조차도 제대로 표현할 수 없다. 우리가 0.1이라고 믿는 숫자는 컴퓨터에서는 실제로 0.100000000000000035와 같이 매우 작은 오차를 갖고 있을 것이다. 위와 같이 각각의 머신이 나타낼 수 최소한의 값을 머신 입실론(Machine Epsilon) 값이라 한다. 따라서 이러한 수치 오차를 줄이기 위해 Single 대신에 Double이나 Float와 같이 보다 정확한 수치 데이터 형을 사용해야하고, 위와 같은 코드의 오차를 줄이기 위한 방법을 써야한다.

C 언어에서 보자면 다음과 비슷한 코드를 사용할 수 있을 것이다. C 언어 프로그래머들에게는 꽤 익숙한 기교적인 방법일 것이다.

#include

int main(int argc, char* argv)
{
   int loopctr;
   loopctr = 0;

   while ( loopctr < 4 )
   {
      loopctr = loopctr + 4/3;
      printf("Loop : %fn", loopctr);
   }
}

이것은 C 언어 코드이다. 4/3을 통해서 루프를 4번만 돌도록 한 것이다. 이 코드에서 loopctr은 int 형으로 되어 있기 때문에 4/3의 결과에서 소수점 부분은 없어지고, 정수 부분만 남게 된다. 따라서 결과는 1, 2, 3, 4가 출력된다.

그러나 실제로 C 언어에서는 4/3의 결과는 무조건 1이 된다. 4/3의 결과인 1.3333…이 계산되도록 하려면 명시적인 타입 캐스팅을 사용해야한다.

다음은 고의적으로 round-off 문제가 일어나도록 재작성한 코드다.

#include

int main(int argc, char* argv)
{
   float loopctr;
   loopctr = 0.0;

   while ( loopctr < 4 )
   {
      loopctr = loopctr + (float)4/3;
      printf("Loop : %fn", loopctr);
   }
}

이 코드를 실행시키면 결과는 다음과 같다.

[traxacun@ns works]$ cc roundoff.c?
[traxacun@ns works]$ ./a.out
Loop : 1.333333
Loop : 2.666667
Loop : 4.000000

결과에서처럼 프로그래머가 의도한 대로 4번의 루프를 돌게되는 것이 아니라 루프는 3번만에 끝나게 된다. 또한 두번째 루프에서부터 round-off 에러에 의해서 2.666667로 되어 있는 것을 알 수 있다.(보다 자세히 확인하고 싶다면 %f대신에 %.24f를 사용하도록 한다. 32비트 데이터형은 24 비트의 mantissa 데이터를, 8 비트의 exponent 데이터를 저장한다)

이와 같이 여러분이 어떤 프로그래밍 언어를 사용하고 있더라도 round-off 문제를 일으킬 수 있다. 따라서 실수형의 데이터를 다루게 된다면 매우 조심해서 다뤄야한다. 또한, 이러한 round-off는 많은 방법으로 처리할 수 있으며, 이러한 에러 자체를 피해갈 수 있는 방법도 존재한다는 사실을 기억한다.( Machine Epsilon에 대해 더 자세히 알고 싶다면 C, C#, VC++에서의 Machine Epsilon에 대한 필자의 글을 참고하기 바란다)

재정 관련 데이터를 처리하는 프로그램을 작성하는 경우에 이와 같은 머신 입실론 값에 따른 오차 한계가 매우 중요한 문제가 된다.(적어도 돈 계산에는 오차가 용납되지 않는다. 공학에서처럼 10의 -5승과 같은 오차한계가 용납되지 않는다. ^^;)

따라서 여러분이 사용하는 시스템과 언어에 따라서 오차 한계를 정확히하고, 이러한 문제를 피하도록 한다. 이에 대한 논의는 이 글의 범위를 벗어나므로 생략하며, 관심있는 분들은 수치해석(Numerical Method) 관련서를 찾아보기 바란다.

2.17 대충 설계하지 마라

필자는 Y2K를 수정하는 작업을 담당한 적이 있으며, 하루 18시간을 UNIX 머신 앞에 앉아서 작업한 경험이 있다. 살도 빠지고, 몸무게도 줄고 있는데 배만 나오는 그 상황을 생각해 보라.

대충 설계하지 마라는 것은 대충 만들어 놓지 말라는 것과 같은 말이다. 어떤 것을 작성하는 데 있어서 나중에 해야지라는 생각은 곤란하다. 분명히 Y2K로 인해서 수정해야하는 소프트웨어들도 누군가 나중에 수정해야지라고 했을 것이다. 그리고 그것은 소프트웨어에 대재앙을 불렀을 것이다.

누군가는 종종 그런 얘기를 한다. 아무리 잘 만든 소프트웨어라 할지라도 곧 수정사항이 생기고 변경사항이 생기기 마련이므로 3년 이상 쓰는 소프트웨어란 없다고 말이다. 그러나 필자는 초기에 작성되어 계속해서 유지보수되어 20년 이상 사용되고 있는 소프트웨어들을 수도 없이 봐왔다. 그당시 최신기술들이 사용되고, 이 언어, 저 언어, 이 기술, 저 기술등이 사용되어 점점 눈덩이처럼 불어나서 아무도 시스템 전체를 이해하지 못하고, 무엇이 시스템에서 돌아가고 있는 것이며, 무엇이 돌아가는 않는 것인지 알 수 없는 상황에 직면한 적이 있다. 불행히도 그것을 정리해내고 바로 잡아서 시스템을 분석해내는 것은 아주 힘든 작업이며 시스템을 새로 개발하는 것에 버금가는 비용이 든다.

소프트웨어를 제대로 설계하고, 제대로 문서화를 해오면서 일관성을 유지한다면 유지보수에 그렇게 큰 어려움이 있지는 않을 것이다.

여러분이 어떤 소프트웨어를 작성하게 되더라도, 누군가 처음에 단지 임시로만 쓸 것이라고 얘기하더라도 여러분은 30년 동안 쓸 것을 만든다고 생각하고 만들어야한다.

2.18 대충 만들지 마라

여러분이 어떤 문제를 해결하는 데 있어서 쉽고 빠른 방법과 일반적이고 융통성있는 방법중에 한 가지를 선택하라고 한다면 일반적이고 융통성있는 방법을 선택한다.

모든 소프트웨어는 반드시 변경사항이 생겨나고 요구사항이 생긴다. 그리고 이러한 유지보수가 발생했을 때 쉽고 빠른 방법을 사용한 경우에 대개는 융통성이 없기 때문에 유지보수하기가 어려워진다. 따라서 조금 오래걸리더라도 보다 일반적이고 융통성있는 방법을 선택하도록 해야한다.

2.19 서드 파티 컴포넌트를 사용하지 않는다

서드 파티 컴포넌트를 사용함으로써 보다 나은 기능을 응용 프로그램에 구현할 수 있고, 그러한 기능을 자체적으로 개발하는 데 드는 비용을 절감할 수 있다.

그러나 응용 프로그램에 버그가 발생하여 수정하게 되었을 때, 서드 파티 컴포넌트에서 버그가 발생한 경우에 버그의 수정을 서드 파티 컴포넌트 개발사에 얘기해야하고, 최악의 경우에는 버전업된 컴포넌트를 구입해야하고, 이전 버전에 대해서는 지원을 중단하는 경우다. 이러한 경우에 여러분은 버그를 없애기 위해 같은 기능을 하는 컴포넌트를 개발해야하거나 새로운 컴포넌트를 구입해야할 것이다.

소스가 공개되어 있지 않은 상용 소프트웨어를 이용하는 경우에 문제가 있어도 여러분이 직접 수정할 수 없다. 따라서 개발하는 응용 프로그램에서 꼭 필요한 경우가 아니라면 서드 파티 컴포넌트의 사용을 자제하도록 한다.

필자의 경험에 따르면 같은 회사에서 제공한 컴포넌트의 버전에 따라서 작동하는 절차 같은 것들이 너무나 다르게 변경된 적이 있어서 고생한 경험이 있다.(믿거나 말거나 아직도 이 응용 프로그램을 유지보수하기 위해선 오래된 소프트웨어만을 사용하고 있다. 전국에 있는 각 지사 사용자에게 무엇을 받아서 설치하고 하는 등의 지시를 일일이 할 수도 없고, 또 이것만으로 다양한 환경에서 발생하는 문제를 해결하지 못한다는 것을 이미 경험을 통해서 너무나 잘 알고 있기 때문이다)

코드의 규모가 작고, 사소한 변화라면 여러분이 직접 코드를 찾아다니며 변경하고 테스트할 수 있을 것이다. 그러나 코드가 이미 한 개인이 수정하기에는 너무 커졌다면 이와 같은 변화 자체가 여러분의 프로젝트에 엄청난 재앙이 될 것이다. 따라서 서드 파티 제품의 사용은 최대한 자제하는 것이 좋다. 설령, 서드 파티 제품을 전혀 사용하지 않는다고 해도 기본 라이브러리 자체의 버그만으로도 여러분을 충분히 괴롭히고도 남을 것이다.

2.19.1 써드 파티 컴포넌트에 대한 변명

RUP가 개발자 세상을 휩쓸고 CBD가 세상을 휩쓸었던 때가 있다. 요즘은 다양한 프레임워크들이 흐름을 이끌어가는 세상이다. 개발자 한 사람이 모든 것을 개발 할 수 없고 다른 사람이 만든 솔루션을 사용할 수밖에 없다. 그렇지만 최대한 서드 파티 컴포넌트를 사용하지 않는 것이 좋다는 것도 사실이다. 쇼핑몰을 위한 결재 시스템을 직접 만드는 것은 분명 어리석다. 외부의 결재 시스템을 자신의 사이트에 덧붙이는 형태가 일반적이며, 이는 대형 쇼핑몰이나 포탈도 예외는 아니다.

3년간 사용하던 결재 시스템을 제공하던 회사가 부도가 나서 다른 곳으로 결재 시스템을 바꾼 경험이 있다. 시스템을 변경하는 동안 겪게되는 손실도 있지만 외부 솔루션을 사용한다는 것은 늘 어느 정도의 위험성을 갖고 있다는 것을 얘기한 것이다. 매우 저렴하고 훌륭한 차트 시스템을 제공하는 국내 중소기업의 컴포넌트를 이용하여 윈도우와 웹 응용 프로그램을 작성했는데 3년 후에 버전업된 컴포넌트를 구입하려하니 이미 회사가 부도가 났다. 차트 만이 아니라 버튼 컨트롤도 보다 편한 기능과 예쁜 모양을 제공하기에 세련된 인터페이스를 보여주기 위해 이러한 컴포넌트들을 자주 이용했으며, 세련된 인터페이스에 감탄을 보내곤 했다. 외국의 유명한 컴포넌트 제작 전문회사의 것을 이용했으나 이 회사는 다른 회사에 인수되어 버렸고, 이 사업 부문을 중단했다. 이로 인한 수정 작업이 그리 간단한 것이 아님은 말할 것도 없다.

또 다른 예로 동영상 스트리밍 서브시의 보안을 위해 도입한 보안 솔루션을 살펴보자. 닷넷이 나오기 전에 개발된 이 제품은 매우 저렴한 가격에 보안 기능을 제공했고, 여러 회사의 제품을 비교해서 가격대비 성능면에서 우수하다 생각해서 이 제품을 선택했다. 그러나 닷넷이 나오면서 이 솔루션은 시스템에 닷넷이 설치되어 있으면 동작하지 않는 현상을 보였다. 소스의 제어권은 우리에게 없으며 솔루션 제작 회사에 있다. 때문에 닷넷 소프트웨어 개발은 불가능하게 되었으며, 외부 솔루션 하나 때문에 전체의 발이 묶이는 현상을 경험했다. 닷넷이 나온지 횟수로 5년이 넘어도 닷넷 환경에서 수행되는 버전은 나오지 않았다. 윈도우 서버 2000에서만 수행되며 2003 서버에서는 수행되지 않는다.

때문에 윈도우 2003이 보다 나은 동영상 스트리밍 서비스를 제공한다고 해도 우리는 윈도우 2003으로 업그레이드를 할 수 없었다. 결국, 2005년도에 서버를 2003으로 변경하면서 솔루션 업체를 변경하고 말았고, 그에 따른 스트리밍 서비스를 전부 재작업해야 했다.

외부 솔루션을 사용하는 것에는 늘 위험이 따라다니며, 솔루션을 바꾼다는 것은 내가 사용하는 모든 소스 코드를 변경해야 한다는 것을 의미한다. 내가 하고 싶었던 이야기는 이러한 위험을 인지하고 꼭 필요한 만큼만 사용하는 것이 좋다는 것이다. 외부 솔루션의 변경에 대해서도 내가 작성한 프로그램의 변경사항과 코드 오염을 최소화하는 방편은 내가 만든 컴포넌트와 외부 컴포넌트 간에 하나의 레이어를 두는 방법이다. 이러한 레이어를 두는 방법으로 가장 잘 알려진 것으로는 GoF의 Adapter, Bridge, Decorator 패턴이 있다.

써드 파티 컴포넌트만 꼭 이런 문제가 있는 것은 아니다. 필자가 한 때 사용했던 PHP에서도 이런 문제가 있다. PHP에서는 Prepared statement라는 기능을 제공하지 않았는데 4.1 버전 이후부터 Prepared statement라는 것을 제공하기 시작한 것으로 기억한다. 다만, 이 기능을 사용하기 위해서는 PHP에서 사용했던 MySQL 관련 코드를 모두 수정해야했다. 예를 들어, mysql_connect, mysql_close, mysql_query와 같은 함수들은 mysqli_connect, mysqli_close, mysqli_query 함수로 바꿔야만 Prepared statement 기능을 사용할 수 있다. PHP로 개발한 솔루션을 보다 나은 성능 향상을 위해 Prepared statement 기능 하나를 사용하기 위해 전체 API를 변경하는 것은 비효율적이지 않을까?

필자가 쓴 PHP Data Access 2. 라이브러리 작성에서 처럼 하나의 레이어를 두고, 이 레이어를 이용해서 PHP 솔루션을 개발했기 때문에 필자는 간단하게 버전업을 할 수 있었지만 대부분의 개발자는 이런 상황에 모든 코드를 직접 수정해야 할 것이다.

ASP 업로드 컴포넌트를 이용하여 개발했을 때도 업로드 컴포넌트 1.5에서는 Open 메서드를 사용하지 않았는데 2.0부터는 Open 메서드를 호출한 다음에 각 폼 요소에 접근할 수 있게 변경되었다. 당연히 컴포넌트 버전에 따라 필자의 코드는 모두 깨지거나 정상적으로 동작하게 되는 이상한 프로그램이 되고 말았다. Open 메서드 하나 때문에 업로드 컴포넌트 버전에 따라 두가지 버전의 ASP 코드를 작성해야 하는가? 이 경우에도 소스의 제어권이 내가 아닌 써드 파티에 있기 때문에 참담한 기분이 되고 만다. 업로드 진행상황을 상태바로 보여주기 위한 기능을 추가하고 싶어도 추가할 수 없다. 결국, 이런 문제 때문에 직접 업로드 컴포넌트를 작성하게 된 경험이 있다.

써드 파티 컴포넌트나 프레임워크를 사용하지 않을 수는 없다. 그러나, 이러한 도구들을 사용할 때는 항상 그 위험성을 인지하고, 변경사항이 발생했을 때 그 변경사항을 최소화할 수 있는 방향으로 코드를 작성해야 한다는 것을 염두에 두어야 한다.

CBD 뿐만 아니라 객체 지향 프로그래밍에 대한 여러가지 장단점에 대해 알고 싶다면 『Component Software: Beyond Object-Oriented Programming』 (1999, Addison Wesley)를 살펴보기 바란다. - 2003년도에 2판이 출간되었다.

- 필자 기억에 1판에서 2.19 써드 파티 컴포넌트를 사용하지마라에 대한 많은 비난이 있었다는 것을 기억한다. 앞서 처음에도 밝혔듯이 이는 필자의 생각이지 이에 대한 옳다, 그르다를 따지는 것이 아니었다. 다양한 의견은 좋으나 비난만은 하지 않기를 바란다.

첫번째 판에서는 '서드 파티 컴포넌트를 사용하지 말라'고 얘기했다. 이에 대한 의견으로 올라온 것으로 '요즘은 CBD(Component Based Development: 컴포넌트 기반 개발)인데 시대에 역행하는 이야기를 하는가? 당신 무식해!' 라는 식이었다. 이를테면 같은 의견이라도 다음과 같이 완곡하게 의견을 올려달라는 것이다.
'오늘날에는 CBD 개발을 얘기하고 있기 때문에 저는 당신의 의견에 동의할 수 없습니다.' '오늘날에는 CBD가 대세인데 시대에 뒤쳐진 얘기가 아닐까요?' 와 같이 말이다.

2.20 빌드 도구를 사용한다

Makefile과 같은 빌드 파일을 사용하여 다른 프로그래머가 여러분의 소스 코드를 컴파일하는 방법을 알 수 있도록 해야한다. 이와 같이 하지 않으면, 아무도 여러분의 코드를 컴파일하지 못하게 될 수도 있다.

오늘날에는 다양한 환경에 대한 다양한 빌드 도구들이 존재한다. VS의 솔루션 파일이나 프로젝트 파일을 그대로 빌드해줄 수 있는 MSBuild, XML 기반 설정 파일을 이용한 빌드 솔루션인 Ant, Ant를 닷넷 환경에 포팅한 NAnt가 있다. .NET Framework 2.0에는 MSBuild가 기본 파일로 포함되어 있다.

빌드 도구를 이용하면 빌드를 자동화하는 것 외에 일일빌드를 통해 모든 코드가 제대로 동작하는지 검증할 수 있다는 장점이 있다. 또한, 모든 직원에 퇴근한 밤 사이에 하루동안 제출된 코드를 취합해서 오랜시간이 걸리는 빌드를 자동화하는 것도 가능하다. 이러한 목적에는 CruiseControl.NET과 같은 도구가 있다.

참고. Cruise Control .NET

2.21 주석을 철저히한다

프로젝트에서 필자는 변수는 한 줄에 하나씩 선언하고 주석을 달도록 했다. 뿐만 아니라 코드 전체에 있어서 모든 주석을 달도록 한 적이 있다. 그리고 프로젝트가 끝나고, 그 회사에 코드를 넘겨줬을 때, 그들은 일반적인 코드에 사용된 주석을 불필요한 것이라하여 모두 정리하고, 꼭 필요하다고 판단한 주석만을 남겨두었다.

결국에 시간이 흘러 코드를 이해할 수 없게 되어서, 모든 코드를 다시 작성해야만 했다.

이 예에서 알 수 있는 것처럼 주석은 지나쳐서 부족한 것이 아니다. 많으면 많을수록 좋다.

2.22 주석의 거짓말

주석의 내용과 코드의 내용이 일치하지 않을 때 주석의 거짓말이라고 부르기를 필자는 좋아한다. 코드에 문제가 생겨서 변경했는데 주석만 변경하지 않은 경우 이런 현상이 발생한다. 철저한 주석만큼 중요한 것은 변경사항이 있을 때 이를 주석에 제대로 반영하는 것이다.

3. 회사 문화

3판에 짧게 낙서처럼 글을 남겨본다.

앞서 얘기가 지극히 개인적인 것들이었다면 회사문화란 상당히 정치적인 것들이다. 항상 좋은 사람들만을 만나는 것이 아니라 기상천외한 사람들을 만나고 상대하며 살아가야하는 것이 사람의 인생살이다.

많은 분들은 프로젝트가 성공하기 위해 개발 환경, 언어, 개발방법론, 프로젝트 관리방법 같은 것을 떠올린다. 불행히도 어떤 언어, 어떤 개발 방법론에도 성공과 실패는 있다. 결국에 이런 것들은 성공을 위한 필수조건이 되지 못한다. 오직 충분조건밖에 되지 못한다.

"Agile Software Development"를 보면 이런 말이 나온다.

"뛰어난 사람은 필요없다. 프로젝트가 성공하기 위해서는 적절한 사람들이 제대로 움직이는 팀이며, 이들은 주어진 프로세스나 기술에 관계없이 프로젝트를 성공시킨다. - 프로세스나 기술은 그들의 업무를 도와주거나 방해할 수는 있겠지만 -

Object Technology International 창립자인 Dave A. Thomas의 성공 방정식은 "어떤 사람들은 소프트웨어를 완성해 내놓지만 어떤 사람은 그렇지 못하다. 나는 소프트웨어를 산출해 낼 수 있는 사람을 고용하였다"라고 말했다."

참고자료
저자: 한동훈

[지난기사보기]
프로그래밍 스타일(5)
프로그래밍 스타일(4)
프로그래밍 스타일(3)
프로그래밍 스타일(2)
프로그래밍 스타일(1)

2.7 명명 규칙

명명규칙에는 많은 것들이 있다. 지역 변수, 모듈 변수, 전역 변수와 같이 변수의 범위를 지정하는 헝가리안 표기법도 있고, 파스칼식의 변수 명명법도 있고, 낙타혹 표기법도 있고, _을 이용한 표기법, Perl의 세계에서만 쓰이는 표기법등 그 표기법도 셀 수 없이 많다. 어떤 표기법이 가장 좋다는 생각은 옮지않다. 어떤 프로그래머들은 헝가리안 표기법이야말로 써서는 안되는 그야말로 쓰레기 표기법이라고 혹평하고 있으며, 다른 프로그래들, 그들 대부분은 윈도우 프로그래머인,은 헝가리안 표기법이야말로 단순하면서 사용하기 쉬운 표기법이라고 한다.

그만큼 명명규칙에는 많은 논쟁이 있다. 필자는 어떤 것이 옳다고 얘기할 수 없다. 다만, 자신의 마음에 들면 쓰고, 마음에 들지 않으면 쓰지 마라. 그리고 이 부분은 자신의 프로그래밍 경험이 쌓여감에 따라서 다시 한 번 펼쳐보고 다시 한 번 생각해보는 것으로 자신의 방법을 세워나가길 바란다. (이점에 있어서는 필자도 마찬가지다.)


2.7.1 변수의 범위를 정하는 표기법을 사용하라

대부분의 언어에서 헝가리안 표기법을 사용하는 것은 바람직하다. 전역변수를 뜻하는 g, 멤버 변수, 즉 클래스 전체에서 쓰이는 변수를 뜻하는 m과 이러한 접두어가 붙지 않는 지역변수와 같이 변수의 범위에 따라 나누는 것도 적절하다.

헝가리안 표기법이 싫다면, 변수명 전체를 대문자로 쓰거나 변수의 첫번째 글자만을 대문자로 쓰는 것과 같은 표기법으로 전역 변수라는 것을 나타낼 수 있다. 코드로 표현하면 다음과 같을 것이다.

PAY_METHOD
Pay_Method
PAYMETHOD
PayMethod

이러한 변수 표기법을 써도 좋다. 전역 변수를 PayMethod와 같이 첫글자를 대문자로 쓰기로 했다면 지역 변수들은 payMethod와 같이 첫글자를 무조건 소문자로 시작하는 것도 좋은 방법이다.

헝가리안 표기법은 변수의 범위외에 변수의 타입을 접두어로 표기한다. 문자열에 대해서는 str을 쓰거나 객체에 대해서는 obj, long 타입에 대해서는 lng 등을 사용하고, 컨트롤에 대해서는 cmd, lst등의 여러가지 접두어를 사용한다.

헝가리안 표기법을 따르기로 했다면 이러한 표기를 따르도록 하고, 만약 다른 접두어를 사용하려 한다면 팀원간에 협의하에 다른 접두어를 사용하도록 한다.

예를 들어서 필자는 예전에는 레코드셋 객체에 대해서 objRsEmployee와 같이 사용했지만, 현재는 rsEmployee와 같은 접두어를 사용한다. rs와 같은 접두어를 사용한다면 이러한 표기법에 대해서 팀원간에 의견을 나누고 문서로 만들어서 팀원 모두가 공유하여야한다.

헝가리안 표기법을 따르는 것과 따르지 않는 것은 여러분의 자유다. 그러나 어떤 프로젝트에서 사용할 표기법을 만들고, 팀원간에 그 표기법을 공유해야한다. 또한, 프로젝트 진행중에 그 표기법에서 문제점이 나타나더라도 표기법을 변경해서는 안된다. 이미 진행된 프로젝트는 정한 표기법에 따라서 진행하고, 나타난 문제점은 프로젝트가 끝난 다음에 분석하여 다음 프로젝트를 위해서 보다 다듬어진 표기법을 만들도록 한다.

2.7.1.1 헝가리안 표기법의 쇠퇴

앞에서도 말했지만 기존의 코드에 자신의 코드를 추가해야하는 경우에는 자신의 스타일 보다는 기존 코드와의 조화가 중요하기 때문에 기존 코드의 스타일을 따라야한다고 했다. 때문에 MFC는 헝가리안 표기법을 사용하고 있고, Win32 API로 헝가리안 표기법을 따르기 때문에 대부분의 MFC 프로그래머는 헝가리안 표기법을 따르고 있다.

그러나 Java/C#을 비롯한 언어들은 타입에 있어 엄격한 언어이며, 개발툴에서는 이런 특성을 쉽게 이용할 수 있기 때문에 헝가리안 표기법을 사용하지 않아도 쉽게 변수의 타입을 알 수 있기 때문에 헝가리안 표기법을 사용하지 않는다. 또한, C# 클래스 라이브러리 개발자를 위한 가이드라인 문서를 보아도 헝가리안 표기법을 사용하지 않고 파스칼 명명법을 사용하라고 되어 있다.

C 언어에서도 typedef를 이용해서 특정 유형의 자료 구조에 대한 연산은 모두 typedef로 선언된 타입을 이용한다. 때문에, 헝가리안 표기법은 윈도우 프로그래밍인 경우에만 널리 쓰이고 있다.

2.7.2 변수의 이름은 최대한 알기 쉽게 해라

어떤 변수 이름이 알기 쉬운 것인가? 라는 것에도 의견이 다르다.
종업원 수에 대해서 변수명을 지어보라고 하면 사람들마다 다른 변수명을 내놓을 것이다.

iEmployee, employee, NumEmployee, NumberEmployee, NumberOfEmployee, EmployeeNumber, EmployeeNo와 같이 다양하게 될 것이다.

숫자라는 것을 뜻하는데 num, no, number, numberof가 쓰일 수도 있고, 여기에 헝가리안 표기법과 같은 접두어가 더해지면 문제는 더욱 더 다양해진다.

대부분의 경우에 num, no와 같은 축약형은 사람들마다 선호하는 축약형이 다르기 때문에 문제를 복잡하게 만들기 때문에 적절하지 않다. NumberOf와 같은 것들도 임의적인 것들이기에 적합한 것은 아니다. 대부분의 경우에 NumberEmployee나 EmployeeNumber와 같은 것들을 쓰는 것이 적당하다. 리눅스 커널을 작성한 라이너스(Linus Torvalds)는 숫자에 대해서 전역상수는 NR_로, 변수는 nr_로 시작하는 명명법을 사용하고 있다. 이 명명규칙은 리눅스 커널 전반에 걸쳐 일관되게 사용되고 있다.

축약형을 사용하기로 했다면 항상 축약형만을 사용해야 한다. 이에 대해서는 2.7.2.1을 살펴본다.

2.7.2.1 축약형에 대한 규칙

읽기 쉬운 변수명도 중요하지만 굉장히 긴 함수명을 입력하는 것도 괴로운 일일 것이다.
.NET Framework를 보면 다음과 같은 함수가 있다.

txtResult.Text = FormsAuthentication.
      HashPasswordForStoringInConfigFile(
                  txtValueToHash.Text, "sha1");

필자가 아는 범위에서는 닷넷에서 가장 긴 함수 이름중에 하나에 속할 것이다. HashPasswordForStoringInConfigFile() 이라는 함수는 굉장히 길다.

이런 극단적인 경우는 아니더라도 Password에 대해서는 Pwd로 사용한다던가, Document에 대해서는 Doc, Employee에 대해서는 Emp와 같은 축약형을 사용할 수 있을 것이다. 축약형이 필요한 경우에는 팀내 협의를 거쳐서 그에 대한 공통된 지침을 만들어서 사용해야 한다. 축약형을 사용하기로 결정했다면 Pwd만 쓰여야지 Password가 쓰여서는 안된다.

2.7.3 성격에 따라 명명하라

성격에 따른 명명법은 변수보다는 주로 클래스 파일이나 모듈 파일의 명명에 많이 사용하게 되지만, 변수 또한 다르지 않다.

다음과 같은 두 가지 코드에 대해서 한 번 생각해 보자.

ProvinceCodeAdd
UserJobCodeAdd
UserResidenceCodeAdd
UserAdd

AddProvinceCode
AddUserJobCode
AddUserResidenceCode
AddUser

첫번째와 두번째는 모두 다르다. 첫번째는 어떤 대상이 중심이 되는 명명법이고, 두번째는 어떤 행위가 중심이 되는 명명법이다. 필자는 첫번째와 같은 명명법은 주로 변수명에 사용한다. 물론, 변수명이니 뒤에 붙은 Add는 뺀다. 이러한 명명법을 사용하는 경우는 UserName, UserAge, UserPhoneNumber와 같이 어떤 대상을 위주로 명명하는 경우다. 객체에서 클래스를 명명하는 경우에는 User.Name, User.Age, User.PhoneNumber와 같이 명명하게 되고, 이러한 속성들의 값은 UserName, UserAge, UserPhoneNumber등에 저장하는 습관을 같고 있다.

두번째와 같이 주로 어떤 행위에 대해서 명명하는 경우에는 대부분 모듈이나 함수명이 된다. 어떤 행위에 해당하는 동사를 앞으로 두는 것으로 코드를 찾는 것이 쉬워지고, 코드를 유추하는 것도 쉽다. PrintPage, PrintScreen과 같이 Print + 'Print할 디바이스 명'의 형식으로 사용한다.

2.7.4 클래스의 표기법을 통일하라

조금 이상하게 들릴지 모르지만, 프로젝트가 보다 큰 프로젝트라면 하나의 팀에서 모든 작업이 이루어지지 않고, 팀별간에 작업이 나누어지게 된다. 객체지향 언어를 사용하기로 결정했다면, 클래스의 속성, 메소드의 표기법에 대해서 일관성을 갖도록 한다.

여기에는 약간의 문제점이 있다. 팀전체가 다른 팀의 소스까지 모두 볼 수 있도록 전체에서 일관된 코딩 스타일을 사용할 것인가, 아니면 팀내에서는 개개인 각자의 코딩 스타일을 그대로 사용하지만 클래스에서 노출되는 메소드 명명법에 대해서 일관된 스타일을 사용할 것인가라는 것이다.

많은 사람들은 객체 지향 프로그래밍이고, 클래스의 내부에 대해서 신경쓰지 않아도 되고, 각자의 코딩 스타일을 지켜(?)나갈 수 있으므로 두번째를 좋아할지도 모른다. 물론, 두번째를 써도 된다. 그러나 개인적으로는 팀간에 소스 코드를 공유하고, 수정하는 일이 없더라도 소스 코드에 대해서도 일관된 스타일을 사용하는 것이 좋다.

필자는 전체가 일관된 스타일을 쓰는 곳에서 일을 할 때, 그 스타일을 익히는 것은 다소 짜증나고 귀찮았지만, 익숙해지고 몇 개의 프로젝트를 끝낸 다음에, 다른 팀의 완전히 다른 소스를 넘겨받아서 프로젝트를 진행하게 되었을 때도 편하게 코드를 읽을 수 있었고, 코드에서 사용된 흐름도, 코드 체계, 코드 설명에 대한 문서들을 찾는 방법을 알고 있었고, 코드에서 이 부분은 어떤 것을 처리하는 부분인지 쉽게 알 수 있었다.

예를 들면, 이런거다. 1000번대 이름을 갖는 코드들은 현금 계정을 처리하는 코드들이고, 2000번대 이름을 갖는 코드들은 물자 계정을 처리하는 코드들이라는 것이며, S로 끝나는 코드들은 어떤 데이터들의 요약을 행하는 코드들이라는 것과 같은 것이다. 물론, 코드안에 사용된 변수들도 이러한 통일된 규칙을 갖고 있었기 때문에 전혀 다른 업무를 처리하는 프로그램이지만 무리없이 코드를 분석하고 일을 진행할 수 있었다.

2.8 if의 스타일

if, select와 같은 분기문을 사용할 때는 항상 중첩된 분기문을 쓰지 않도록 해야한다. 분기문 내의 코드가 길어지면 길어질수록 코드 중간에 나오는 else와 같은 문장이 어떤 if에 속한 것인지 이해하기가 어렵게 된다.

If 조건문
   아주 긴 코드
   ...
   ...
   ...

else
   코드
end if

이와 같은 구조를 갖는 코드가 있다면 else 부분에 있는 조건문을 if에 있는 조건문과 바꾸는 것이 좋다.

If 조건문
   코드
else
   아주 긴 코드
   ...
   ...
   ...
end if

이와 같이 하는 것이 더 코드를 이해하기 쉽도록 해준다. 또한 if 문과 else 문에는 반드시 어떠한 조건이 일어나는 경우에 분기가 일어나는지 주석을 달아야한다. 종종, 바쁘다는 이유만으로 If나 else에 주석을 생략하거나 간략하게 달아놓은 경우 한참 시간이 흘러서 다시 보게되면 무엇을 판단하는 것인지 이해하기 어려운 경우가 많다. 그래서 어떤 경우에 분기가 일어나는지 이해하는 데 주석을 다는 것보다 더 많은 시간이 걸린다. 대개의 경우에 충분한 주석을 다는 것으로 디버깅시에 여러분의 노력은 충분한 보상을 받을 것이다.

if에 주석을 다는 경우에는 개인적으로 다음과 같은 주석 스타일은 바람직하지 않다.

If 조건문 // 이것은 주석입니다.

대개의 경우에 이것은 간단한 주석인지 모르나, 조건문이 길어지는 경우에 80 컬럼을 넘어가게된다. 모든 프로그래머가 여러분과 같이 80 컬럼이상을 표시하는 디스플레이와 에디터에서 작업하는 것은 아니다. 또한, 필자처럼 1280*1024나 1600*1200과 같은 높은 해상도에서 작업하는 것은 아닐것이다. 여러분의 코드를 보게될 다른 사람을 배려하도록 한다. 개인적으로 만든것이라 하더라도, 누군가는 언젠가 여러분의 코드를 보게 된다.

분기문에는 다음과 같은 주석을 쓰는 것이 좋다.

// trax가 술에 취한 경우에 실행되는 부분입니다.
If 조건문

개인적으로 코드의 주석을 소설처럼 읽듯이 코드를 읽는 경우가 아니라, 코드에 쓰인 조건문이 너무 많아서 코드 전체가 분기문으로 뒤덮인 경우에는 코드에 보다 집중하기 위해 다음과 같은 주석 스타일을 쓰기도 한다.

If 조건문
// trax가 술에 취한 경우에 실행되는 부분입니다.

두 가지의 차이는 첫번째는 주석을 보고 코드를 이해하기 좋다는 것이고, 두번째는 코드에 대해서 이미 잘 알고 있고, 주석보다는 코드의 로직을 판단하는데 더 용이하다는 것이다. 시선이 가장 먼저 가는 것에 따른 차이다. 그러나 대개의 경우에는 첫번째와 같은 주석을 쓰는 것이 좋다.

2.9 함수의 길이

컴퓨터 세계에서 가장 길이길이 남겨진 이야기중에 하나는 아주 크고, 어려운 문제가 있다면 그것을 잘게 쪼개고, 하나씩 해결해 나가다보면 전체를 해결할 수 있다는 것이다. 영어로 쓰면 Divide and Conquer! 라던가?

보통 클래스 전체를 작성하는 경우가 아니라 특정 함수만을 작성하거나 함수를 수정하는 경우에 새로운 주석을 작성한다. 함수의 기능에 대한 주석을 작성할 때, 한 두줄 이내로 함수의 기능이 무엇인지 설명하지 못한다면 함수에 지나치게 많은 기능이 있는 것이다. 이러한 경우에는 좋지못한 함수를 작성한 것이다. 함수는 작고 간결해야한다. 모든 기능을 갖고 있는 만능 함수는 복잡하고 이해하기 어렵기 때문에 버그를 갖기 쉬우며, 유지보수하기 어렵다. 따라서, 함수의 기능을 한 두줄 이내로 설명하기 어렵다면, 설계자에게 설계가 잘못된 것인지 문의해서 확인할 필요가 있고, 설계를 수정해야한다.

대부분의 모듈들이 1,000 - 2,000 라인 정도의 길이를 갖고 있었는데, 그중에 한가지 모듈만이 가장 많은 처리를 하고 있었고, 18,000 라인 정도의 길이로, 다른 코드들에 비해서 10배 이상이나 긴 코드를 갖고 있었다. 결국, 다른 모듈들에 발생한 버그들은 쉽게 처리할 수 있었지만, 모든 기능을 갖고 있는 18,000 라인을 갖고 있는 모듈에서 버그가 발생하면 몇시간이 아니라 며칠에 걸쳐서 디버깅을 해야했던 경험이 있다.

단 몇 줄의 코드를 작성하더라도 여러분이 생각지 못한 버그는 반드시 있다. 그렇다면 여러분은 단 몇시간동안 디버깅하겠는가, 머리를 싸매며 며칠동안 디버깅하겠는가?(야근 필수다~ ^^;)

2.10 들여쓰기 형식

들여쓰기에 대해서는 많은 사람들이 다르게 생각할 것이다. 아마, 아주 구시대 프로그래머들에게는 들여쓰기는 탭의 기본 간격인 8칸일 것이고, COBOL 프로그래머들은 7번째 칸부터 시작하면 주석, 8번째 칸부터 시작하면 데이터 타입 선언과 같은 규칙을 따라야할 것이다.

결국 들여쓰기는 언어에 따라서, 여러분의 취향에 따라서 선택사항이다. 8칸 들여쓰기를 고수하는 사람도 있고, 4칸 들여쓰기, 2칸 들여쓰기를 좋아하는 사람도 있다. 필자는 대부분의 언어에서 2칸 들여쓰기를 좋아한다. 그러나 어떤 사람은 3칸 들여쓰기를 좋아하기도 한다. 어떤 들여쓰기를 하건간에 해당 프로젝트에서는 정해진 들여쓰기 간격을 지켜야한다.

탭 간격을 조정하거나 여백 문자로 바꿀 수 있는 적절한 에디터를 사용하도록 한다. 그렇게 하지 않으면 이러한 탭 여백을 변경할 수 없는 에디터에서는 코드의 들여쓰기가 엉망이 된 채 표시되고, 아무리 간단한 코드라도 이해할 수 없게 된다.

연구 결과에 의하면 3칸 들여쓰기를 사용할 때 코드를 가장 읽기 쉽다고 한다. VI에서는 ~/.vimrc로 설정을 제어할 수 있다. 다음은 VI에서 자주 사용되는 설정들이다.

set tabstop=2
set shiftwidth=2
set expandtab
set backspace=indent,eol,start
set softtabstop=2
set cindent
set autoindent
set smartindent
set incsearch
syntax on
set background=dark
set joinspaces
set smarttab
set showmatch " (), {}, []괄호 매치를 보여준다




[출처 : http://www.hanb.co.kr/network/view.html?bi_id=1139]
저자: 한동훈

[지난기사보기]
프로그래밍 스타일(4)
프로그래밍 스타일(3)
프로그래밍 스타일(2)
프로그래밍 스타일(1)
 
2.4 문서화하라.

프로젝트를 진행하는 동안에 여러분이 작성한 의미없는 문서, 회의시간동안에 남았던 문서들, 프로그램의 버그등을 문서화한 것등의 수 많은 문서가 남아있게 될 것이다. 뿐만 아니라 팀원들의 작업 결과나 작업 진행, 보고서등을 받아보게 될 것이다.

이러한 것들을 팀원들에게 history@xyz.com과 같이 하나의 계정으로 모두 보내도록 하고, 프로젝트가 완료하는 시점에 이것들을 모두 일괄출력해서 보관하도록 한다.

이 문서들은 모두 프로젝트 진행기간 동안 다시 쳐다보는 일도 없을 것이고, 많아야 1-2번 정도 쳐다보게 될 것이다. 그러나 프로젝트가 끝나고 버그가 발생했을 때, 무엇이 문제였는지, 어디를 제일 먼저 확인해야하고 수정해야하는지를 쉽게 찾는 데 도움이 된다.

이러한 것들을 잘 모아두었다면, 프로젝트가 완료되는 시점에서 문서화하는 것도 그리 어려운 것은 아닐 것이다.(적어도 문서화작업이라는 미명하에 처음부터 모든 것을 다시 살펴보는 일은 막을 수 있다)

2.4.1 코드 문서화

코드 문서화는 자바 환경의 JDoc, 닷넷 환경의 NDoc, C/C++과 같은 다양한 언어에 적용할 수 있는 DoxyGen과 같은 문서화도구들이 있다. 이들 도구는 웹에서 바로 이용할 수 있는 HTML, 윈도우 도움말 형식을 비롯해서 PDF 형식으로 만들어주기까지 한다.

이들 도구를 이용하면 코드를 작성하면서 작성한 코멘트가 바로 도움말이 된다. 각 문서화 도구가 제공하는 기능들을 확인해서 프로젝트에 필요한 문서 양식을 공통으로 지정하는 것은 좋은 방법이다. 이런 문서 양식을 미리 작성해 놓은 소스 코드를 시작점으로 삼는 것도 좋은 방법이 될 수 있다. 이런 문서를 뼈대라 하여 뼈대(Skeletion) 문서라고 부른다.

Vim에서도 skel.c/skel.h를 작성해 두고 Template Loader 플러그인을 사용하면 이 파일로부터 새로운 C 소스 코드를 생성할 수 있으며, VS.NET과 같은 개발도구도 템플릿 기능을 이용해서 고유의 프로젝트나 ASP.NET 페이지 양식을 작성할 수 있다. 이러한 도구들을 활용해서 누구나 손쉽게 코드 문서화를 할 수 있는 환경을 구축해야 한다.

참고

Vim Template Loader
ASP.NET 가이드 3. UI 향상 및 사용자 템플릿 만들기

2.5 버그를 기록하라

버그를 기록하는 것이 필요하다. 이러한 버그들을 기록하는 가장 일반적인 형식을 만들어서 팀원들이 모두 같은 양식으로 작성하여 리포트할 수 있도록 해야한다.

대개는 다음과 비슷한 형식이 될 것이다.

- 버그 발견일 : 팀 내부나 테스트 과정에서 버그가 발견되고 보고될 것이다.
- 원인 : 버그의 원인.

기능부호가 H인 경우에 대해서 잘못 처리하는 경우와 같은 로직상의 버그에서부터 참조할 수 없는 메모리를 참조하는 포인트 사용의 오류등과 같이 다양할 것이다.

- 증상 : 버그로 인해 생기는 증상을 자세히 기록한다.

기능부호 H를 소비하는 경우에 신품재고는 그대로 유지되고 재생품재고가 감소해야하는데 신품과 재생품 재고가 모두 감소하는 경우와 같을 것이다.

- 해결책 : 버그를 어떻게 없앴는가.
- 예방법 : 이와 같은 종류의 버그를 예방할 수 있는 방법
- 관련 파일 : 버그가 있는 파일과 버그와 관련되어 같이 변경되어야하는 종속 파일들을 모두 기록한다.
- 최종 수정일 : 누가, 언제 최종적으로 수정했는가

이와 같은 버그 리포트는 모두 bug@xyz.com과 같이 하나의 메일함으로 보내도록 한다. 이렇게 하면 팀원 모두가 어떤 버그가 있었는지 확인할 수 있으며, 프로젝트가 완료후 이러한 기록들을 모두 모아서 버그를 분석할 수 있다.

버그 분석을 통해서 프로젝트에서 사용했던 방법보다 더 빠르고 정확하게 대처할 수 있는 방법을 찾을 수도 있고, 프로젝트 진행 과정에서 어떤 부분에서 버그가 가장 많이 발생했는지 알아낼 수 있고, 진행 과정에 문제가 있는지도 알아낼 수 있다.

버그 리포트는 버그를 발견하는 즉시 작성되어야하며, 수정하지 않거나 미루게 되더라도 bug@xyz.com으로 보내도록 해야한다. 그러나 되도록이면 버그는 발견되는 순간에 제거하도록 해야하며, // fix me와 같이 주석을 남겨서 일을 미뤄서는 안된다.

나중에 코드가 완성된 다음에 버그를 수정하는 경우 나머지 코드와 예기치 않은 오류를 만들어낼 수 있으며, 더욱 더 문제를 어렵게 만들 수도 있다.

1.3 버그 관리에서도 쓴 것처럼 본격적인 버그 관리 도구를 사용하는 것이 바람직하다. 그러나 bug@history.com처럼 작은 변화를 통해 버그 관리의 유용성을 깨닫게 하는 것도 하나의 방법이 될 수 있다.

2.6 변수

변수의 사용에 있어서만큼 프로그래머들에게 많이 이야기 된 것은 없다. 그리고 필자의 몇가지 이야기는 분명히 논쟁거리가 된다는 것을 알고 있다. 아마도, 여러분이 생각하는 상식적인 이야기와 여러분이 생각하기에 비상식적인 이야기로 꽉 찰 것이라는 것이다. 처음에 얘기했듯이 반드시 따라야하는 규칙은 아니다. 마음에 들면 따르고, 마음에 들지 않으면 따르지 않으면 그만이다.

2.6.1 모호한 변수는 사용하지 마라

모호한 변수는 사용하지 마라. 언어에 따라서 variant 형 변수를 지원하거나 지원하지 않을 것이다. 필요하다면 variant를 쓸 수 있지만, 대부분의 경우에는 정확한 데이터 형식을 지정하라. Variant는 사용하지 말아야할 악과 같은 데이터 타입이라고 생각하자.

물론, Perl과 같은 언어는 변수의 데이터 타입이 없으며 variant 타입이 기본 타입이며, 변수에 저장되는 값에 따라서 각각의 서브 타입을 갖고 있다.

그러나 VB와 같은 언어의 경우에 variant 타입과 자동형변환과 같은 암시적 타입 변환은 프로그래머에게 가장 해독한 독과 같다. 암시적 타입 변환의 경우에 언어마다 작동하는 방식이 조금씩 다르기 때문에, 기존 프로그래밍 언어의 지식을 자신이 현재 작업하고 있는 새로운 언어에 작용하는 경우에 여러분이 원하는 대로 작동하지 않게 된다.

A = 123
B = 456

Print A & B
Print A + B
Print "Pay" + A & B

이러한 경우에 여러분은 이것이 어떻게 처리될 지 알 수 없다. A가 정수인지, 문자열인지 알 수 없게 되어 버린다. 따라서 반드시 변수의 타입을 지정해서 사용해야한다.

위에서와 같이 숫자가 문자열로 암시적인 형변환이 일어나는 경우에 해당 언어에 익숙하고, 세세한 것까지 기억하는 프로그래머가 아닌한 틀리기 쉽다. 이러한 경우에는 CStr, CInt, CLng와 같은 함수를 사용해서 변환할 형을 미리 지정해주도록해야한다.(언어에 따라 함수는 다르다)

Variant는 어떤 경우에 사용하는가? 대부분의 경우에 의문을 갖겠지만, VB와 같은 언에서는 컨트롤과 같은 객체를 넘겨받는데 Variant 타입을 사용한다. 그외에 4 * 4와 같이 정사각형의 배열을 만드는 것에 익숙하지만, 경우에 따라 1 행은 7열, 2행은 3열, 3행은 4열과 같은 불규칙 배열이 필요한 경우에 Variant 타입을 쓸 수 있다.(1차원 배열 3개에 대한 컨테이너로 Variant 타입을 이용하는 경우다)

최근의 프로그래밍 언어들은 대부분 변수의 초기값을 설정한다. 숫자형에 대해서는 0, 문자열에 대해서는 문자열의 길이가 0인 문자열, 객체에 대해서는 null로 설정한다. 그러나 이러한 것들이 다른 언어에서 반드시 적용되는 것은 아니다. 변수는 반드시 초기값을 선언하도록 한다.

2.6.2 변수는 나누어 선언한다

int i, j, k;
Dim i, j, k as Integer

첫번째는 C 언어이고 두번째는 VB이다. 이 경우에 첫번째는 3개의 변수 유형이 모두 int 형이지만, 두번째 줄에서는 i, j는 Variant 형이고, k만 Integer 형이 된다. 이와 같이 하나의 언어에서 사용하는 문법이 다른 언어에서는 전혀 다르게 사용될 수 있다. 따라서 위와 같은 코드들은 모두 다음과 같이 고치는 것이 좋다.

Int i;
Int j;
Int k;

Dim i as integer
Dim j as integer
Dim k as integer

이렇게 고침으로서 다른 사람이 코드를 읽고 이해하기 쉬워진다.

위 코드는 또 잘못되어있다. 이 변수들은 모두 초기값을 갖지 않는다. 초기값을 갖도록 선언해야 한다. 또한, 옆에는 각각의 변수가 무엇 때문에 선언되는지 적어야한다.

만약 코드를 작성하는 도중에 필요없게 된 변수가 있어서 코드내에서 해당 변수를 지웠다면 변수의 선언을 지우는 것도 잊지 않도록 한다.

또 다른 예를 살펴보자.

char *string, string2;

C 언어에서 첫번째 변수 string은 char*이지만 두번째 변수 string2는 char가 된다. 이와 같이 C 언어에서는 변수의 선언 여부를 엄격하게 검사하지만, VB와 같은 언어들은 변수의 선언을 엄격하게 검사하지 않는다. 이러한 경우에 VB에서는 Option Explicit등을 사용해서 변수의 선언을 엄격하게 검사할 수 있다. 가장 흔한 실수는 변수명을 PrintPage와 PirntPage와 같이 적는 것이다. 변수 선언 여부를 검사하지 않으면 VB와 같은 언어는 새로운 변수로 생각하여 PirntPage라는 새로운 변수를 생성하게 되고 프로그램이 문제없이 실행되더라도 알지못할 버그를 내포하고 있는 것이다.

2.6.2.1 Option Explicit에 얽힌 이야기

2.6.2에서와 같이 Option Explicit을 사용해서 반드시 사용하는 변수를 선언해서 사용해야한다라는 것은 사람들에 따라 의견이 분분하다. VBScript, VBA를 사용하다가 VB를 사용하는 프로그래머나 Perl 프로그래머들은 그러한 것들을 사용하는 것은 오히려 거추장 스러운 것이라고 얘기한다. 반면에 처음부터 VB를 사용한 프로그래머나 C/C++과 같이 변수 선언을 엄격하게 검사하는 언어를 사용하다가 VB를 사용하는 프로그래머들은 반드시 Option Explicit을 써야한다고 말한다.

아주 능숙한 프로그래머중에도 Option Explicit을 사용하지 않아도 되며 타이핑 오류를 잡는 데에 아주 능숙한 프로그래머들도 많다.

결국, 어떠한 방식을 선택하느냐는 여러분의 몫이다.
필자는 대부분의 언어에서 변수 선언을 검사하기를 좋아한다.


[출처 : http://www.hanb.co.kr/network/view.html?bi_id=1136]
저자: 한동훈

[지난기사보기]
프로그래밍 스타일(3)
프로그래밍 스타일(2)
프로그래밍 스타일(1)

2. 코딩 스타일

코딩 스타일에 있어서는 가장 많은 논란이 있을 것이다. 그러나 코딩 스타일에는 이거다라고 할만한 것은 없다. 언어에는 각각의 언어마다의 표기 방법이 있고, 그들 세계의 규칙이 있다. 그리고 대부분의 경우에 그러한 규칙을 따르는 것이 좋다.

필자는 처음에 C 언어를 익혔고, 그 다음에 HTML, Perl, JavaScript, C++, VBScript, LPI-COBOL, SQL, Informix/4GL… 등의 순서대로 언어를 익혔다. 그리고 혼자서 언어를 익히는 기간동안은 모든 언어에서 사용할 수 있는 코딩 스타일을 만들어나가고자 했다. 즉, C 언어에서도 만족할 수 있고, C++에서도 만족할 수 있고, Perl에서도 만족할 수 있기를 바란 것이다. 그리고 COBOL에 와서 그 꿈은 깨져버렸고, 프로젝트를 시작하면서는 자신의 코딩 스타일에 대해서 염려하기는커녕 팀 내에서의 코딩 스타일을 따라가기에 바빴다. 이미 작성된 수십만줄이 넘는 코드속에 자신이 부속품처럼 끼어들어가 필요한 모듈을 작성해나가야했다. 역시나 많은 시행착오를 겪었지만, 이미 마련되어 있는 지침들에 따라서 코드를 작성해 나갔다. 그리고 그러한 지침들에 익숙해졌을 때는 다른 사람이 작성한 코드도 알아보기 쉬웠다. 물론, 처음 팀별 작업을 하지 않았을 때에는 자신의 개성을 죽이는 것이며, 지금까지 자신이 사용한 스타일을 버리는 것이 되지만, 그렇게 함으로써 팀내에서 의사소통을 원활하게 할 수 있으며, 프로젝트를 빠르게 진행시킬 수 있다는 것은 당연한 것이다.

때문에 여러분이 사용해왔던 프로그래밍 언어가 몇 개가 되든지 간에 관계없다. 여러분이 사용하는 언어에서 사용하는 관습적인 프로그래밍 방법을 따르는 것이 대부분의 경우에 여러분의 삶을 편한하게 만들어줄 것이다. 그리고 여기에 자신만의 개성을 덧붙여 나가라. 만약, 팀원간에 작업을 해야하고, 해당 언어의 관습적인 방법에는 없는 방법을 사용한다면 팀원간에 논의하고 지침을 만들어 모두가 그 지침을 지키도록 해야한다.

리눅스 커널을 비롯한 주요 오픈 소스 프로젝트에는 포함된 문서에는 코딩 스타일과 관련된 문서가 반드시 포함되어 있다. 때문에, 오픈 소스에 공헌하고 싶다면 이런 코딩 스타일을 숙지하고 이러한 스타일로 작성된 코드를 제출해야만 오픈 소스 커뮤니티에 받아들여질 것이다.

2.1 지침을 만들어라

앞에서 시작한 이야기지만, 자신의 프로그래밍 스타일에 대한 지침을 만들어야한다. 그러나 겨우 자신이 자신을 위해서 이러한 것을 만든다는 것은 대부분의 경우에 하지 않는 일이다. 물론, 하지마라. ^^;

그러나 팀원간에 작업을 해야하거나 회사내에서 작업을 하는 경우라면 반드시 코딩 스타일에 대한 지침을 만들어야한다. 그렇지 않으면 옆사무실에 있는 팀이 만든 코드조차 우리 사무실에서 만드는 코드와 스타일이 다르기 때문에 혼란스럽게 된다.

문제는 이것만으로 끝나지 않는다. 우리 팀이 만든 것과 다른 팀이 만든 컴포넌트들을 모두 이용하는 다른 회사에서 이렇게 투덜댈 수도 있다. 같은 회사에서 만든 제품들이 스타일이 달라서 사용하는 데 있어서 두 번의 교육이 필요하다라라고…

그러나 일관된 지침을 사용한다면, 미국에 있는 팀원들이 만든 코드와 한국에 있는 팀원들이 만든 코드 모두 읽기 쉬울 것이고, 코드가 정확히 무엇을 하는지 추측하기가 쉬워진다.

2.1.1 코딩 스타일을 지키기 위한 조언

상황에 따르지만 전산기획팀은 시스템 전체를 기획하고 그에 대한 산출물을 UML로 작성한다. - RUP나 CBD와 같이 따르는 방법론에 따라 산출물의 종류나 이름은 다르지만 표기법은 UML이며, 설령 UML을 사용하지 않는 회사들도 UML을 모방한 표기법을 사용하고 있다 - UML 산출물에 따라 개발자가 개발을 한다. UML 산출물에는 클래스의 설계를 나타내는 클래스 다이어그램, 업무 흐름을 나타내는 시퀀스 다이어그램등이 있다. 이 산출물대로 코드들을 작성해 나간다. 코드 작성이 끝나면 QA팀에 넘긴다. QA를 담당하는 부서나 사람은 해당 코드를 테스트하고, 코드가 회사의 코딩 스타일을 제대로 따르고 있는지 검토한다. UML 산출물과 다른 코드를 갖고 있거나 회사의 코딩 스타일을 따르지 않으면 코드를 해당 개발자에게 다시 반환한다. 또한, 코드가 이해되지 않는 부분에 대해 개발자의 설명을 직접 들어도 QA팀이 이해하지 못할 경우 코드를 쉽게 작성할 것을 요구하며 코드를 반환한다.

QA팀을 통과하지 못하면 그 작업 단위는 완료되지 못한 것이 되기 때문에 모든 개발자는 이를 준수할 수 밖에 없다. QA팀을 두거나 QA를 위해 한 사람을 고용하는 것은 낭비라고 생각할 수 있다. 개발팀이 작성했으나 마치 한사람이 작성한 것과 같은 코드를 산출해 내게 된다면, 문제가 발생했을 때 누구나 쉽게 코드를 읽고 버그를 추적할 수 있게 해준다.
최소한 그 부분의 로직은 너무 복잡해서 "직접 개발한 A씨가 아니면 그 부분은 아무도 손댈 수 없어요"라는 이야기는 나오지 않는다.

반드시 이런 수작업에 의존할 필요는 없다. indent와 같이 코딩 스타일을 조절해 주는 전통적인 프로그램부터 거의 22가지 언어에 대한 작업을 대신해주는 PolyStyle 까지 있다.

"ctags -x rulefile"와 같은 명령어는 C/C++ 코드에 대해서 해당 코딩 스타일을 지키고 있는지 검사까지 대행해준다.

2.2 공격적인 코드를 작성하라

이 부분에 대해서는 많은 의견들이 있다. 필자는 여전히 방어적인 코드를 작성하는 것을 좋아하지만, 필자의 방법역시 굳이 방어적이라고 할만한 것은 아니다.

다음과 같이 팩토리얼을 구하는 함수가 있다고 하자.

function factorial(numeric as integer)
   int retVal

   if numeric = 0 then
      retVal = 1
      return retVal
   else
      retVal = numeric * factorial(numeric - 1)
      return retVal

아마도 이 함수는 입력값으로 0이나 양수일 때는 잘 동작하겠지만, 음수일 때는 동작하지 않을 것이다. 이러한 경우에는 함수의 첫 문장에 debug.assert numeric >= 0과 같은 문장을 넣어서 원하는 값이 들어가지 않는 경우에는 중지시킬 수 있도록 해야한다.

어쨌거나 이러한 코드는 정확히 말해서 명료한 코드도 아니고 잘된 코드도 아니다.(return을 두 곳에 사용한 것도 잘못된 코드 양식이다)

에러가 발생하는 경우에 보통의 프로그래머들은 0이나 -1등을 반환하도록 배웠을 것이다. 그러나 이것은 대부분의 경우에 프로그램에서 적절한 값을 반환했다고 생각하며 문제없이 수행될 소지가 아주 크다. 때문에, 이러한 경우에는 반환할 수 있는 데이터타입에서 가장 큰 값을 반환하도록 해야한다. -32768과 같은 값을 반환하면 여러분은 쉽게 무엇인가가 잘못되었다는 것을 알 수 있을 것이다.

2.2.1 반환값에 대한 이야기

프로그램의 로직에서는 0이 거짓이나 실패를 1이 참이나 성공을 의미한다. 그런데, 함수의 반환값에서는 0이 성공을 나타낸다. 왜 이와 같이 작성했을까?
만약, 성공일 때 1을 반환하는 함수를 작성했다면 2나 3과 같은 숫자도 성공이라고 가정할 수 있게 된다. 성공일 때 0을 반환하는 함수를 작성했다면 1을 비롯한 나머지 숫자들은 모두 에러 상황에 대한 설명으로 대체할 수 있으며 그 의미가 혼동되지도 않는다.
0이라는 단 하나의 상황만을 성공으로 두고, 나머지 값을 실패로 두는 것이다.

2.3 if 문의 기본

대부분의 경우에 If문에 대해서 올바르지 않은 방법을 사용하고 있다고 생각한다. 예를 들어서 다음과 같은 경우다.

' 레코드가 있으면
If rs.RecordCount > 0 Then

'처리 코드

End If

이러한 코드가 반드시 올바르다고 할 수 없다. 그래서 방어적인 코딩을 하라고 조언하곤 한다. 위 코드는 다음과 같이 다시 작성할 수 있을 것이다.

' 레코드가 있으면
If rs.RecordCount > 0 Then

'처리 코드

' 레코드가 없으면
Else

'처리 코드

End If

Else 부분에서 특별히 처리할 것이 없다면 주석에 do nothing과 같은 주석을 달아도 좋다. 그러나 대부분의 경우에 do nothing이라고 주석을 다는 것은 적당하지 않은 방법이다.

따라서 Else 부분에서 아무것도 하지 않더라도, 어떤 경우에 에외가 발생할 수 있는지에 대해서 주석으로 설명해야한다. 필자는 절대로 일어나지 않을 오류인 경우라해도 반드시 if만을 쓰는 경우는 없다. 모든 판별문에는 else를 두고 있다.

오랜시간이 지나서 같은 코드를 보는 경우에 대부분의 복잡한 코드에서 Else 부분이 어떤 경우에 발생할 수 있는지 잘 기억나지 않는다. 코드를 작성하고 있는 시점에서만 Else가 어떤 경우에 발생할 수 있는지 가장 명확하게 이해하고 있으므로, 코드를 작성할 때 Else 부분이 발생하는 경우에 대해서 적어두어야한다.

if 뿐만 아니라 select/switch case와 같은 조건문에도 else/default가 들어가야한다.

C 언어에서 가장 많은 경우지만 다른 언어를 작성하는 프로그래머들 사이에서도 많이 볼 수 있는 유형은 else 부분에서와 같이 절대로 처리되지 않아야하는 부분이 처리되는 경우에 많은 프로그래머들은 0이나 -1등을 반환하도록 프로그래밍을 하고 있다. 그러나 0과 1 같은 반환값은 대부분의 경우에 프로그램의 동작을 멈추게 하지 않으며, 버그를 내부에 숨기게 되는 오류를 범하게 된다. 그렇다면 사용하는 함수의 변수가 사용하는 값 중에 가장 큰 값을 반환하도록 한다. 예를 들면, -31000과 같은 값을 반환하도록 한다. 그러면 여러분은 코드에서 말도 안되는 값을 반환하는 부분을 쉽게 찾아낼 수 있을 것이고, 무엇인가 잘못되었다는 것을 알 수 있을 것이다.

2.3.1 비어있는 Else 문의 위력

2.3의 예제 코드를 다시 생각해보자. 이제는 수개월이 지나도 Else 부분이 레코드가 없는 경우라는 것을 알 수 있다. 레코드는 DB의 검색결과 일 수도 있고, 메일함의 수신메일 개수일수도 있다. 이전에는 수신메일이 없으면 아무것도 보여주지 않았는데, UI팀이 지적한바에 따르면 "빈 목록화면이 시스템 오류인지 수신된 메일이 없는지 구분되지 않기"때문에 "수신된 메일이 없습니다"와 같은 메시지를 출력해주는 것이 좋다고 한다. 이럴 때, 코드를 작성할 때 Else 부분에 대해 작성한 부분 때문에 코드에서 필요한 부분을 보다 쉽게 찾을 수 있게 해주고 지적사항을 남들보다 빠르게 반영할 수 있을 것이다.

나중에는 수신된 메일이 없을 때 사용자를 다른 곳으로 클릭을 유도하기 위해 "수신된 메일이 없습니다. 새로운 소식을 얻는 보다 쉬운 방법으로 XX뉴스가 있습니다"를 출력하게 변경사항 요청이 있을 때도 보다 쉽게 처리할 수 있을 것이다. 자신이 아닌 다른 개발자도 쉽게 이 문제를 해결할 수 있을 것이다.

필자의 또 다른 경험은 절대 예외가 발생하지 않을 것이라 생각한 부분에도 Else문을 두고, "unhandled exception"이라는 문구를 사용자 몰래 기록하게 해놓은 부분을 작성해 놓는다. 정말 아무 문제가 없었는데, 3년후에 특정 조건에서만 문제가 발생한다는 이야기를 들었고, unhandled exception이 숨어있는 것을 발견했다. 물론, 3년전의 코드는 ASP였고 ASP를 사용하지 않은지가 3년이나 되었지만 쉽게 문제가 발생한 곳을 찾아서 문제를 해결해 줄 수 있었다. Else 문은 마치 값싸게 이용할 수 있는 try … catch 문이라 생각할 수 있다.

2.3.2 복합 if문의 단순화 방법

다음과 같은 코드를 생각해보자.

if(A) B
else if(C) D
else if(E) F
else G
 
이와 같이 여러 개의 if문으로 되어 있는 경우에는 코드를 이해하는 것이 어렵다. 이런 구조는 A를 비교해서 없으면 C를 비교해보고, E를 비교해보는 식으로 순차적으로 진행한다.

매우 길고 복잡한 형태의 if문이 된다면 switch/select … case 문을 사용하는 것을 고려해보기 바란다.

2.3.3 다중 if문의 단순화 방법

if(A) B
   if(A') B'
      if(A'') B''
K
else if(C) D
 
이와 같은 코드를 작성했다고 하자. 이 코드의 문제점은 A 조건을 만족하지 않는 경우, A는 만족시키되 A'을 만족하지 않는 경우, A와 A'은 만족하되 A''은 만족하지 않은 경우에 실행되는 코드는 모두 K 지점이 된다는 것이다. 이러한 동작이 의도된 것이라면 다행이지만 대부분의 경우 복잡하게 중첩된 다중 if문은 조건이 복잡하며 문제를 해결하기도 어렵다.

때문에 각 if에 Else를 넣어두는 것도 하나의 방법이 될 수 있지만 여기서는 좋은 방법이 아니다.

이런 경우에는 각 조건 A, A', A'', C에 대해 TRUE/FALSE에 대한 매트릭스를 만들어본다. 매트릭스를 작성하면 보다 간단한 if 문의 형태로 코드를 압축할 수 있으며, 예기치 않은 경우가 발생할 수 있는지를 테스트할 수 있다.


[출처 : http://www.hanb.co.kr/network/view.html?bi_id=1134]

저자: 한동훈

[지난기사보기]
프로그래밍 스타일(2)
프로그래밍 스타일(1)

1.5 코드에 집중하지 마라

프로그래머는 대개의 경우에 코드에 집중해야한다. 그러나 실제로 프로그래머가 집중해야할 것은 설계다. 몇일전 술자리에서 얘기한 것처럼, 필자는 하루종일 조용히 모니터를 쳐다보고 종이에 끄적이다가 퇴근하기 1-2시간 전에 사무실을 날려버릴 것과 같은 키보드 소음공해를 일으키고 온다고 했다. 심지어는 머리가 맑을 때 코드를 작성하는 것이 아니라, 가장 졸리고 신경 안 쓰는 일을 할 때 코드를 작성한다. 아니, 코드를 작성하는 것은 지극히 단순 노동이라고 얘기하곤 한다. - 물론, 실제로 100% 맞는 말은 아닐 것이다. -

실제로 여러분이 해야할 일은 설계다. 코드를 작성하기 전에, 함수 하나 작성하기 전에 여러분은 설계에 집중해야한다.

직원들의 급여를 계산하는 함수를 작성하는 경우를 예로 들어보자. 처음에는 함수의 이름을 먼저 생각한다. 그리고 함수가 무엇을 할지를 쓰게 된다. 그리고 그 순서는 다음과 같을 지도 모른다.

1차 - 기본적인 로직을 생각한다

CalculatePay()
   사원 ID를 받는다.
   사원 ID에 따른 직급을 구한다.
   직급에 따른 월급을 구한다.
   월급을 반환한다.

2차 - 요구사항(직급)에 대한 로직을 추가한다

급여를 사원 ID에 따라 계산한다
CalculatePay(사원ID)
   필요한 변수를 선언한다
   사원 ID에 따른 직급을 구한다.

   직급에 따른 월급을 분류한다.
      사장
      과장
      대리
      사원
구한 월급을 반환한다.

3차 - 오류검사를 추가했다

급여를 사원 ID에 따라 계산한다
CalculatePay(사원ID)
   필요한 변수를 선언한다
   사원 ID가 맞는지 확인한다
      ID가 맞으면 사원 ID에 따른 직급을 구한다.
      틀리면 에러로 넘긴다.

   직급에 따른 월급을 분류한다.
      사장
      과장
      대리
      사원

전체에서 예외가 발생했으면 예외처리로 넘긴다.
전체에서 예외가 발생하지 않았으면 구한 월급을 반환한다.

4차 - 결근, 지각, 조퇴인 경우의 급여계산을 한다

급여를 사원 ID에 따라 계산한다
CalculatePay(사원ID)
   필요한 변수를 선언한다
   사원 ID가 맞는지 확인한다
      ID가 맞으면 사원 ID에 따른 직급을 구한다.
      틀리면 에러로 넘긴다.

   직급에 따른 월급을 분류한다.
      사장
      과장
      대리
      사원

결근인 경우에
      횟수 n만큼의 결근기준급여를 삭감한다
지각인 경우에
      횟수 n만큼의 지각기준급여를 삭감한다
조퇴인 경우에
      횟수 n만큼의 조퇴기준급여를 삭감한다

전체에서 예외가 발생했으면 예외처리로 넘긴다.
전체에서 예외가 발생하지 않았으면 구한 월급을 반환한다.

실제로는 이 외에도 연봉액, 연봉액기준기간, 근무자의 근로일과 근로시간, 지불방법, 각종 수당의 종류와 처리 방법, 연봉 대상기간 도중에 퇴직시의 급여계산 방법들이 나올 것이다.

이와 같이 하나의 함수를 업무 로직에 따라 천천히 확장해가면서 작성하는 방법은 좋은 방법이다. 또한 코드가 아니라 의사코드이기 때문에 이해하는데 큰 무리가 발생하지 않는다. 만약, 이것을 처음부터 코드로 작성하면서 복잡한 급여계산을 처리하려 했다면 코드 작성시간 뿐만 아니라 문제가 발생했을 때의 버그 해결 시간도 길어질 것이다.

이와 같은 로직을 모두 작성하고 문제가 없다고 판단되면 워드에서 출력을 하고, 도서 받침대 같은 곳에 올려놓고 그대로 타이핑해 나가면 될 것이다. 자신이 사용하는 언어에 따라 적절한 문법에 따라 코드로 옮겨놓기만 하면 될 것이다.

이것으로 무엇을 할 수 있었는가? 전체의 설계를 할 수 있었다. 또한, 코드를 작성하기 전에 한 번 점검함으로써 예상치 못한 부분들에 대해서도 생각해 볼 수 있었고, 그에 대한 문제도 적절히 처리할 수 있었다. 위의 의사 코드를 보면 들여쓰기가 되어있을 것이다.

그렇다, 위 의사 코드를 에디터에 그대로 옮겨놓고, 모두 주석 표시를 해주고, 문장 중간 중간에 코드를 써 넣어라. 그러면 된다. 이것으로 여러분이 작성한 코드는 조금 더 버그가 적어질 수 있고, 주석이 풍부한 코드가 될 수 있다.

1.5.1 코드 오염의 최소화

한가지 변경 사항이 발생했을 때 코드의 한 곳을 수정하는 것이 아니라 여러 곳을 모두 수정하는 것은 코드 오염의 전형적인 현상이다. 때문에 좋은 코딩이란 코드 오염을 최소화하는 것이라 할 수 있다. 이것은 좋은 설계에 해당하기도 한다.

4차에서는 몇 곳을 강조해두었다. 해당 부분들은 급여 계산외의 다른 부분에서도 굉장히 자주쓰이는 코드가 될 것이라고 예상되기 때문이다. 이런 코드들은 다른 부분에서 굉장히 자주 중복될 것이고, 이런 중복된 코드는 하나의 메서드로 작성하는 것이 좋다. 하나의 메서드로 작성하면 코드 중복을 최소화할 수 있고 변경사항이 발생했을 때 변경하기도 쉽다.

"사원 ID가 맞는지 확인한다"는 CheckEmpID() 함수로 만들 수도 있다. 하지만, 이런 함수는 단순히 검사만 수행하는지 결과값을 반환하는지 알 수 없다. 디스크 검사를 수행하는 CheckDisk() 함수와 명확히 구별되지 않는다. 이것이 하드 디스크인지 알아내는데 IsHarddisk()와 CheckHarddisk() 함수 이름을 보면 어떤 것이 더 나은지 알 것이다.

IsValidEmpID() 함수와 같은 이름으로 별도의 함수를 만들면 좋다.

"사원 ID에 따른 직급을 구한다" 이 함수는 특정 기준(사원ID)에 따라 직급을 구하는 것이니 GetGrade(사원ID) 라는 이름을 사용할 수 있을 것이다. 그러나, 직급의 이름(대리, 과장, 부장)에 따른 직급을 구하는 함수가 필요할 때 GetGrade(직급명)으로 함수를 작성할 수 없을수도 있다. - 모든 언어가 객체지향의 특징을 제공하는 것이 아니다. PHP4도 그런 예이다 - 때문에 GetGradeByID, GetGradeByName과 같은 함수 명도 사용할 수 있다.

- 나는 급여 계산과 관련한 코드를 작성한 경험이 없다. 이러한 방법을 설명하기 위해 실질적인 예를 드는 방법(OS의 스케줄링이나 가상 메모리 관리 코드)도 있겠지만 지나치게 특정 언어/플랫폼에 종속되어 논지를 전달하기 어렵다고 생각하기 때문에 급여 코드를 기준으로 설명하였다 -

1.6 꾸준히 하기

프로그래머가 짓고 있는 죄 중에 가장 큰 죄가 무엇인가? 그것은 꾸준히 하지 못한다는 것이다. 대부분의 프로젝트는 원대한 꿈(?)을 갖고 시작하지만, 결국에는 마감시간안에 끝내기에 급급하다는 것이다. 그래서 처음에 세웠던 계획대로 코딩을 하지 못하고, 설계에 들이는 시간보다 막바로 코딩을 하기에 바쁘고, 수정한 코드들에 대해서 주석을 남기지 않는 실수를 하게 된다. 그래서 나중에는 코드와 주석에 설명된 내용이 전혀 엉뚱하게 되어버리는 경우도 생기게 된다.

그리고 무엇보다 가장 큰 문제는 시간이 흘러서 예기치 못했던 곳에서 에러가 생긴다고 하는 것이다. 그리고 그 버그를 잡는데 들이는 시간이 여러분이 코드를 작성하는 데 들인 시간보다 더 많다는 것을 알게 될 것이다. 그것은 프로젝트의 끝이 아니다.

아무리 급해도 '빠르고 성급하게'가 아니라 '꾸준히 완전하게'하는 것을 목표로 삼아라.


[출처 : http://www.hanb.co.kr/network/view.html?bi_id=1133]
저자: 한동훈

[지난기사보기]
프로그래밍 스타일(1)

1.3 버그를 관리하기

버그를 관리하는 것은 중요하다. 버그를 발견하고 없애는 것이 전부가 아니기 때문이다. 버그를 관리하기 위한 방법으로는 크게 전통적인 방법과 비교적 새로운 방법, 두 가지로 나누어 볼 수 있다.

전통적인 방법은 버그가 발생한 부분의 코드를 주석으로 남겨두고, 어떤 버그였는지 기록하는 것이다. 나중에 시간이 흘러 버그가 있다고 생각한 로직이 올바른 로직인 경우도 있고, 무엇 때문에 버그가 발생했는지도 알 수 있기 때문에 다음 프로젝트의 코드에서 버그를 예방할 수 있게 해준다. 또한 주석이 많이 있는 부분은 버그가 자주 발생하는 부분, 프로그램에 버그가 발생했을 때 이 부분에서 버그가 발생할 확률이 다른 부분보다 상대적으로 높다는 것을 알려주는 역할도 한다. 그 결과 전체 코드에서 어떤 부분이 가장 중요하고, 복잡한 처리를 하는 부분인가를 쉽게 알 수 있다. 또한, 팀장이라면 코드를 만든 사람의 코드 작성하는 스타일을 비롯하여 어떠한 버그를 자주 만들어내는지 까지도 눈치챌 수 있을 것이다. 프로젝트가 완료된 후에는 각각의 팀원에게 취약한 부분을 지적해 줄 수도 있으며, 잠시 교육기관에 보내서 재교육을 시킬 수도 있다. 하하, 조금 무서운가? 하지만 이것이 팀장 뿐만 아니라 팀원에게도 약이 된다. 여러분이 팀원이라면 스스로 이러한 스타일을 지키고 프로젝트가 끝난 다음, 주석처리된 코드들을 다시 한 번 살펴보기 바란다. 반대로 팀장이라면 팀원들에게 이러한 것을 요구하고 납득시킬 것을 권하는 바이다.

비교적 새로운 방법은 이러한 부분의 주석화를 하지 않는 대신, CVS와 같은 버전 관리 소프트웨어를 적극 활용하는 것이다. 그렇지만 안타깝게도 모든 개발자들이 CVS를 쓰는 것은 아니다(CVS나 Visual Source Safe와 같은 도구를 사용하지 않는 사람이 더 많다). 하지만 버그 관리를 위해, 이들의 사용법을 훈련시키고 익숙하게 만드는 것이 좋다. 버그 발생빈도를 낮추는 또 하나의 도구로 Unit Testing(단위 시험)이 있다. 자바를 위한 JUnit, 닷넷을 위한 NUnit, ASP.NET을 위한 NUnitASP 등의 테스트 프레임워크가 있으며 테스트를 작성하고 코드를 작성하는 것은 매우 좋은 방법이다.

(† 참고: NUnit(닷넷용 Unit Testing Framework), NUnitASP(ASP.NET Unit Testing Framework), JUnit(Java Unit Testing Framework), SQLUnit(데이터베이스 저장 프로시저를 테스트하는 프레임워크))

1.3.1 코멘트 누적에 대한 논란

코멘트를 누적시키는 방법은 일견 좋은 방법이라 여겨진다. 이 방법에는 세 가지 단점이 있다.
  1. 소스 코드를 지저분하게 만든다
  2. 코멘트 누적이 많은 경우 어떤 내용이 수정된 것인지 정확하게 알기가 어렵다
  3. 코드의 동작과 코멘트가 일치하지 않는 코멘트의 거짓말이 발생할 수 있다
이 때문에 버그를 관리하는 좋은 방법은 버그를 추적하고 관리할 수 있는 도구를 사용하는 것이다. 이러한 도구에는 JIRA, WebAsyst, 버그질라와 같은 잘 알려진 도구가 있다.

CVS 같은 소스 코드 관리 도구를 쓰면 반드시 변경된 소스 코드에 대한 변경 사항을 기록하게 되어 있다. 이를 잘 활용한다면 코멘트를 누적시키는 방법을 쓰지 않아도 버그가 자주 발생한 코드를 추적할 수 있으며, 누가 작성한 코드인지 쉽게 알 수 있다. - CVS를 사용하는 대부분의 프로그래머는 이 부분을 작성하지 않는다. 이 경우엔 문제나 변경사항을 알 수 없게 되기 때문에 코멘트 누적보다 더 나쁜 결과를 얻게 된다. CVS는 단순히 코드를 저장하는 공간이 아니며 하나의 문화다.

JIRA 같은 버그 관리 도구를 사용하면 코멘트에서 버그와 관련된 내용을 제거하고 웹 상에서 버그와 관련된 모든 활동을 할 수 있다. 이러한 도구는 반드시 CVS와 같은 소스 코드 관리 도구와 함께 사용되어야 그 진가를 발휘할 수 있다. JIRA의 가장 큰 단점은 기술적인 내용을 모르는 높으신 분들도 웹 상에서 손쉽게 소프트웨어의 문제를 파악할 수 있고, 어떤 개발자가 작성한 코드에서 문제가 자주 발생하는지 알 수 있다는 점이다. :)

이들 도구를 사용하면 소프트웨어의 버그를 체계적으로 관리할 수 있으며, 웹을 통해 사용자로부터 직접 피드백을 받을 수 있다는 장점이 있지만, 개발자에겐 이러한 도구 사용을 교육시키고 적응하게 만드는 데 시간이 걸린다. - 개발자란 아집의 결정체인 것인지 좋다는 것을 알려도 좀체 사용하려 들지 않는다. 회사내에 CVS와 같은 소스 관리 도구를 도입하기 위해 수개월이상 팀과 회사를 설득하려한 개발자들의 글을 블로그 이곳저곳에서 만날 수 있다. 그뿐인가, 개발자 잡지에는 "하드디스크 고장/소스코드 유실"이후에 CVS를 도입했다는 인터뷰가 자랑스럽게 실리니 "소 잃고 외양간 고치기"인 경우이다. 개발자란 어쩌면 "소를 잃어버리기 전에는 외양간을 고칠 생각을 좀처럼 안하는 존재"인지도 모른다.

참고

- 소스 코드 관리도구
CVS:
가장널리 쓰이는 소스코드 관리 도구로 가장 기본적인 기능을 제공하지만 바이너리 파일에 대한 관리 기능이 미약한 단점이 있다.

WinCVS:
윈도우 환경에서 CVS를 편하게 사용할 수 있는 GUI 클라이언트 도구이다. 물론, CVS 서버는 윈도우 환경에서도 사용할 수 있다.

TortoiseCVS:
윈도우 환경에서 탐색기와 연동해서 편하게 소스 코드를 관리할 수 있게 해주는 도구로 윈도우 환경에서 널리 쓰이고 있다.

SubVersion:
CVS 보다 진보한 기능을 추가한 도구로 CVS를 채택하지 않은 곳은 SubVersion을 채택하고 있다.
- 버그 관리도구
JIRA

WebAsyst

버그질라:
버그질라는 오픈소스이며, 무료로 자유롭게 이용할 수 있다. RedHat Linux, FireFox, Debian Linux와 같은 오픈소스 프로젝트들이 사용하고 있다.
1.4 최적화에 대한 문제

조금 이상하게 들릴 것이다. 최적화는 뒤로 미루는 것이좋다. 하지 말라는 것은 아니다. 아마도 신입이나 초급 프로그래머가 작성한 코드를 보면 자신이 직접 코드를 만지고 싶다는 충동을 느끼게 될 것이다. "이것도 코드라고 작성한거냐?"라는 생각으로 머리속이 꽉차고, 당장에 고쳐버리고 싶다고 느끼게 된다.

하지만 일단 잘 작동하는 코드라면 그대로 두도록한다. 프로젝트중에 잘 동작하는 코드를 건드려서 동작하지 않도록 하는 것보다 낫다.

최적화는 크게 프로그램이 수행되는 속도와 메모리 사용량등과 같은 것들을 말한다. 그러나 이러한 최적화들은 대개의 경우에 읽기 어려운 코드를 만들어낸다. 또한, 실제로 프로그램이 수행되는데 있어서 중요하지 않거나 효과도 없는 부분에 자신의 역량을 집중하게 되는 문제를 낳게 된다. 알아보기 어려운 코드는 대개의 경우에 더 큰 버그를 만들어낼 소지를 갖고 있으며, 자신만이 사용하기 쉬운 코드, 이해할 수 있는 코드를 만드는 지름길이 되며, 다른 프로그래머가 이해하는 데 더 많은 시간이 걸리는 코드를 만들어 내게 될 것이다.

만약 A와 B의 값을 교환하는 함수 swap를 만든다고 하자. 대개의 프로그래머는 다음과 같이 코드를 작성할 것이다.

A = 1
B = 2
tmp = A
A = B
B = tmp

이렇게 사용하면 int형 변수 3개를 사용한 것이라면 4 * 3 = 12, 12 바이트를 사용하는 함수가 될 것이다. 그런가 하면 다음과 같이 작성할 수도 있다.

A = 1 0000 0001
B = 2 0000 0010
A = A Xor B (0000 0001) Xor (0000 0010) = 0000 0011
B = A Xor B (0000 0011) Xor (0000 0010) = 0000 0001
A = A Xor B (0000 0011) Xor (0000 0001) = 0000 0010

분명히 이러한 기교를 사용할 수도 있고, C 언어라면 포인터를 사용해서 두 값을 바꿔낼 수도 있다. 그러나 이것은 단지 2바이트를 아끼기 위해서는 너무 큰 희생이다. 이전 코드보다 더 모호해졌다. 대개의 경우에 이런 문제는 더 이상 의미를 가지지 않는다. 512kb, 640kb와 같이 한정된 메모리만을 갖고 있던 XT 시절에나 각광받을(?) 기술이다.

즉, 자신이 갖고 있는 프로그래밍 능력이 어느 정도나 되는지는 관심이 없다. 팀원 전체가 쉽게 읽을 수 있는 코드를 만들수 있으면 된다. 당신이 작성한 코드를 나중에 다른 프로그래머가 볼 수 있으면 그것으로 되는 것이다.

최적화는 코드를 완성한 다음에 해도 늦지 않다. 전체 실행시간의 95%는 5%의 코드가 사용한다. 최적화는 이 5%의 코드에 집중해서 하는 것이 전체의 수행성능 향상을 위해서 좋다.

1.4.1 Xor 최적화의 문제

Xor 최적화는 코드를 이해하기 쉽다는 점 외에 다른 문제점도 알려져있다. 즉, 두 변수의 메모리 위치가 동일할 때 동작하지 않는다. 같은 배열의 같은 위치를 가리킨다거나 같은 변수이거나 메모리 위치의 포인터 값과 같은 경우에는 Xor 연산을 이용한 스왑은 동작하지 않는다.

흥미로운 것은 Xor 스왑(Swap)을 자랑스럽게 이용하는 사람중에 Xor 연산을 사용할 수 없는 경우를 제대로 알지 못하는 사람이 많다는 것이다. - 프로그래밍은 심오해서 제대로 이해하지 않고 사용하는 기술이란 얼마나 위험한 것인지를 잘 알게 해주는 예이다

1.4.2 최적화의 시기

프로젝트 도중에 최적화를 하는 것은 매우 위험한 일이라고 했다. 최적화는 코드를 완성한 다음에 하는 것이 좋다. 그러나, 2001년에 처음 글을 쓸 때와 요즘에 달라진 점이 있다면 단위 테스트(Unit Testing)와 프로파일링 도구의 보편화다. 이에 대한 반증으로 이클립스(Eclipse) 뿐만 아니라 Visual Studio 2005에는 단위 테스트, 프로파일링 기능이 모두 개발툴안에 통합되어 있다.

주요 기능의 개발이 끝나고 안정화 단계에 들어설 때가 최적화를 하기 위한 가장 좋은 시기가 될 것이다. 성능을 최적화하기 위해서는 개발자 자신이 작성한 코드 전체를 리뷰하는 것도 좋은 방법이지만 지금과 같이 분업화되어있고, 전체 코드의 규모가 한 사람이 다 보기 어려운 상황에서는 바람직하지 않다. 프로그래머가 직접 계산하거나 신통력을 발휘하여 "이 부분의 코드를 개선하면 수행성능이 향상될거야"라는 식의 수행 성능 측정 역시 옳지 않다. 복잡한 응용 프로그램의 수행 시간을 직접 계산하는 것도 어리석거니와 자신이 만든 프로그램이라도 실제로 사용자들이 어떤 작업을 가장 많이 사용하는지, 어느 정도의 비율을 차지하는지는 알기 어렵다.

수행 속도를 개선하기 전에 좋은 성능 측정 도구부터 사용해야 한다. 과거와 달리 오늘날의 프로파일링 도구는 매우 막강한 기능을 포함하고 있다.

메모리 누수에 대한 문제, 코드 부분별 메모리 사용량 문제, 실행 시간 문제, CPU 점유율 문제, 전체 코드에서의 실행 비율과 같은 문제를 알려줄 뿐만 아니라 어제의 이미지와 오늘의 이미지를 비교해서 메모리 누수가 있는지 여부까지 측정해 줄 수 있기 때문에 24시간 실행되어야 하는 서버 응용 프로그램의 성능 측정까지 가능하다.

OptimizeIt, DevPartner Studio, CLR Profiler와 같은 다양한 프로파일링 도구가 있기 때문에 자신이 사용하는 환경과 언어에 맞는 프로파일링 도구를 선택하면 된다.

이러한 도구를 사용해서 가장 많은 비중(호출빈도, CPU 점유율, 메모리 사용량)을 차지하는 곳을 중심으로 코드를 최적화하면 될 것이다. 최적화 이후에 단위 테스트를 모두 통과한다면 최소한 각 기능들이 모두 정상적으로 동작하는구나하고 안심할 수 있을 것이다. - 단위 테스트는 특정 순서로 응용 프로그램을 조작할 때 발생하는 문제에 대해서는 해결책을 제시하지 못하니 반드시 별도의 테스트를 해야 한다.

참고

- 최적화 도구

CLR Profiler:
닷넷 환경을 위한 무료 프로파일러

JProfiler:
자바 환경을 위한 프로파일러

YourKit Java Profiler:
매우 비싼 유료 프로파일링 도구지만 자바 환경에 대한 막강한 프로파일링 기능을 제공한다

OptimizeIt:
Borland사의 OptimizeIt은 자바 환경과 닷넷 환경을 위한 두 가지 버전을 제공하고 있으며, 다양한 프로파일링 기능을 제공한다.

DevPartner Studio
Compuware사의 DevPartner Studio는 다소 무거운 툴이지만 VS6, VS.NET 2003등에 직접 임베드되어 사용할 수 있으며 C++/VB/.NET 환경을 위한 다양한 코드 검사와 프로파일링을 수행해준다. DevPartner Studio 이전에는 BoundsChecker라는 제품으로 알려졌으며 지금은 Studio로 통합되어 있다.

WebSphere Studio Page Detailer
IBM의 페이지 디테일러는 웹 페이지의 로딩 속도등을 직접적으로 테스트하기 때문에 가장 느린 페이지를 찾아내기에 좋다. 이것은 WebSphere Studio에 포함되어 있다.

- 참고문서
  • Advanced C# 9. 알고리즘 분석(Algorithm Analysis)
  • The Metric Based Compiler - A Concurrent Requirement, R. Dumke, K. Neumann, K.Stoeffler, ACM SIGPLAN Notices, Vol.27, No.12, Dec. 1992.
  • The Object-Oriented Brewery: A Comparison of Two Object-Oriented Development Methods, R. Sharble, S. Cohen, ACM SIGSOFT Notes, Vol.18, No. 2, 1993, pp.60-73.
  • Measurement of Language-Supported Reuse in Object-Oriented and Object-Based Software, J. Bieman, S. Karunanithi, Journal of Systems Software, Vol. 30, 1995, pp.271-293.



[출처 : http://www.hanb.co.kr/network/view.html?bi_id=1130]

+ Recent posts