본문 바로가기
develop

#include를 조심하라

by 찬이 2004. 7. 28.

written by 김시찬 (chanywa), 2004-07-28
Homepage : http://chanywa.com
Email : chany@chanywa.com

#include 모르면 간첩

C를 비롯한 수많은 언어들이 공통적으로 가지고 있으면서, 그리고 프로그래밍 입문 당시부터 보게 되는 include라는 존재가 있다. 주로 다른 파일에서 선언된 함수나 변수, 상수값에 대한 정보를 가지고 있는데, 같은 데이터가 여러 파일에 걸쳐서 사용되는 경우에 유용하게 쓰인다.

프로그래밍을 조금이라도 해본 사람이라면 누구나가 알겠지만, 이것을 사용하는데 있어서 주의하지 않으면 얼마나 많은 시간을 삽질해야 할지 모른다. 한번쯤 눈여겨봐두면 실무를 하다가 언젠가 한번쯤 써먹게 될만한 유용한 것이라 생각되어 또 몇자 적어본다.


#include의 역할

일단, C를 기준으로 설명하겠다. #include는 보통 헤더파일이라고 불리는 파일을 인클루드 한다. 이 헤더파일은 보통 라이브러리로 제공되는 함수들의 원형들을 제공하고 있지만, 그외에도 #define 등으로 정의된 상수, 혹은 typedef 등으로 선언된 기타 데이터형이나 구조체 등을 담고 있기도 한다.

입문시절에는 보통 한개의 소스파일로만 프로그래밍을 하기 때문에 라이브러리 사용을 위한 경우가 전부라 할 수 있다. 점차 자신만의 프로그램을 작성하기 위해서 한개 두개 소스파일들을 늘려가면서 자신만의 헤더파일들을 만들어 사용하게 된다.

하지만 곧 문제에 부닺치게 된다. 왜냐하면 처음보는 컴파일 에러가 뜰 확률이 높기 때문이다.


헤더파일의 중복정의 방지


비록 파일은 분리되어 있어도 인클루드하게 되는 헤더파일의 내용들은 소스파일의 일부인 셈이다. 그래서 두번 이상 중복정의되는 경우가 쉽게 발생할 수 있다.

아래의 예제를 한번 보자. 데이터구조체 등이 정의된 datatype.h, 메인프로그램인 main.c와 그에 필요한 몇가지 정의가 되어 있는 main.h등이 있다. 일반적으로 헤더파일들은 하나의 소스상에서 여러번 인클루드 될 수 있기 때문에 그런것을 방지하기 위해서 #ifndef, #define 등을 사용한다. 아래의 헤더파일들도 그런 방식으로 중복인클루드를 방지하고 있다.

define.h 파일
#ifndef DEFINE_H // 중복인클루드 방지
#define DEFINE_H // 중복인클루드 방지

#include "main.h" // DEFINE_BUS

#define DEFINE_CAR
#undef  DEFINE_PLANE

#ifdef DEFINE_BUS
#define CAR_TYPE "Bus"
#define CAR_NUMBER 10
#else
#define CAR_TYPE "Taxi"
#defien CAR_NUMBER 2
#endif

#endif // DEFINE_H // 중복인클루드 방지

자동차에 대한 정의파일이다. 버스에 대한 기능으로 동작하게 컴파일하고 싶으면, DEFINE_BUS을 define하면 되고, 그렇지 않으면 택시에 대한 기능으로 동작하게 컴파일이 된다.

큰 프로젝트에서는 대부분 이런식으로 진행된다. 구버전이나 혹은 다른 모델의 프로그램에다 추가기능이 있다 싶으면 그 기능에 대한 것은 모두 #define 된 것을 이용해서 #ifdef 형식으로 코딩하게 된다. 그렇게 코딩하면 유지보수가 쉬울 뿐 아니라, 동일한 소스로 두개 이상의 모델에 적용할 수 있고, 원할 때라면 언제라도 기능을 넣거나 빼는 것이 쉽기 때문이다.

datatype.h

#ifndef DATATYPE_H
#define DATATYPE_H

#include "define.h"

typedef struct
{
    int number[ CAR_NUMBER ];
} DATA;

#endif // DATATYPE_H

여기서는 데이터 타입을 정의하고 있다. 그런데 자동차의 개수인 CAR_NUMBER가 필요하기 때문에 그것이 정의된 "define.h"를 인클루드 하고 있다.

main.h 파일

#ifndef MAIN_H // 중복인클루드 방지
#define MAIN_H // 중복인클루드 방지

#include "datatype.h"  // main.c에서 DATA 사용하기 위해

#ifdef DEFINE_CAR
#define DEFINE_BUS
#define DEFINE_TAXI
#else
#define DEFINE_ROKET
#define DEFINE_AIRPLANE
#endif

#endif // MAIN_H // 중복인클루드 방지

메인 프로그램을 위한 헤더파일로 프로그램의 전체적인 설정내용이 들어 있다. 어떤 것이 정의되었고 정의되지 않았느냐에 따라서 프로그램 자체가 바뀌어버릴 수 있다.

 main.c 파일

#include "main.h"

void main(void)
{
    int i;
    DATA data;

    for( i=0; i < CAR_NUMBER; i++ )
        data.number[i] = i;

    printf("%s", CAR_TYPE);
}

메인 프로그램이다. 여기서 CAR_TYPE을 출력하게 된다. 무엇이 출력될까? 그리고 CAR_NUMBER는 무슨 값일까...?


중복인클루드로 인해 생길 수 있는 문제

위에서 예로 든바와 같이 각각의 헤더파일들은 중복인클루드를 방지하기 위한 코드가 거의 필수로 들어가게 되는데, 이것이 자칫잘못하면 큰 오류를 범할 수 있다. 위의 소스도 예외는 아니다. 다시 한번 잘 살펴보기 바란다. 컴파일이 제대로 될까?

위의 예제는 헤더파일이 꼬인 문제를 예제로 만들어보기 위해서 억지로 꾸민 것이긴 한데, 이런 경우 컴파일이 되지 않거나 원하지 않는 결과를 낳게 된다.

main.c
{
    main.h
    {
        datatype.h
        {
            define.h
            {
                  main.h 인클루드 안됨
                  #ifdef DEFINE_BUS
                  #define CAR_NUMBER
                  #define CAR_TYPE
                  #endif
             }
             int number[CAR_NUMBER];
         }
         #ifdef DEFINE_CAR
         #define DEFINE_BUS
         #define DEFINE_TAXI
         #else
         ...
         #endif
    }
}

대략 위와 같은 구조로 실행된다. define.h 에서 DEFINE_BUS를 위해서 main.h를 인클루드 했다. 하지만 main.h는 이미 인클루드 되었었기 때문에 다시 인클루드를 할 수 없으나, #define DEFINE_BUS는 아직 실행되지 않은 상태이다.

바로 이렇게 물고 물리는 관계로 헤더파일들이 서로서로 인클루드하게 되면 문제가 생길 수 있다는 것을 얘기해 주고 싶은 것이다.


해결방법은......?


말은 간단하다. 헤더파일들끼리는 서로를 인클루드 하지 않도록 하는 것이다. 즉, main.c에서 필요한 순서대로 헤더파일들을 죄다 인클루드 해주면 된다.

큰 프로그램을 짜보지 않은 경우라면, 이런 상황을 이해하기 힘들지도 모른다. 그냥 머리좀 써서 헤더파일을 정리해주면 괜찮지 않은가 하고 말이다. 맞는 말이다. 위의 상황에서 헤더파일안의 내용들을 순서를 약간씩 조정해주면 해결은 가능하다.

하지만 실전은 다르다. 소스파일과 헤더파일이 만개가 넘는 경우도 허다하게 있기 때문에 연결된 헤더파일들도 수백개 이상이 되곤 한다. 아니, 어쩌면 수천개 일지도 모른다. 한 사람이 모든 내용을 알고 작업을 했다면 얘기가 달라지겠지만, 몇년에 걸쳐 수십 수백명의 손을 거치는 경우도 있기에 아무도 장담은 못한다.

가급적 헤더파일들은 헤더파일들끼리 연결시키지 말고, 가급적 소스상에서만 인클루드 하길 권한다. 차라리 에러라도 나면 다행이지만, 원하지 않는 결과가 나왔을 경우에는 수천개의 파일을 뒤져보더라도 원인을 못찾을 수 있다. 위의 소스에서 출력결과가 "Bus"가 아니라, "Taxi"가 나오는 경우처럼 말이다.


수십 수백개의 파일을 어떻게 다 하라고


실전엔 수없이 많은 파일이 있다고 했다. 그렇다고 헤더파일을 죄다 소스파일에 인클루드하긴 사실 무리다. 이런 때에는 레이어별로 별도의 헤더파일을 구성하는 방법을 추천한다.
프로그램의 모든 소스가 동등한 레벨에 있지는 않을 것이다. 예를 들어 전화번호부 프로그램이라면 화면입출력에 관련된 처리가 있을 것이고, 그 하위에 데이터 검색, 수정 같은 유틸개념이 있을 것이며, 그 하위에는 데이터를 실제로 조작관리하는 부분이, 그 하위에는 디스크에 직접 파일로 다루게 되는 부분도 있을 것이다. 대부분의 소스는 각각 계층별로만 연계되도록 해야 하며, 이때 공통적으로 사용되는 헤더파일들을 모아서 한두개의 헤더파일로 모아서 사용하면 된다.

하지만 모든 헤더파일을 모으는 것이 가장 최선이라고는 할 수 없다. 아주 공통적인 헤더파일들만 모으고, 그 이외에는 소스에서 직접 인클루드 해주면 된다.


항상 생각을 하고 살자


만약 헤더파일들을 처음의 예제처럼 서로서로 인클루드 하더라도 제대로만 만든다면 문제가 없다. 헤더파일들 간에도 레이어 개념처럼 상위와 하위개념을 두고 제대로된 위치에 코딩을 한다면 별 문제가 없다.

다만 코드가 길어지고, 그 와중에 대폭 수정을 하거나 구조를 변경하게 되면서부터 정리가 안되기 시작하는 경우가 많다. 코딩을 하다 지치면 귀차니즘(?)의 압박에 못이겨서 적당하지 못한 곳에 한줄 한줄이 들어가다보면 걷잡을 수 없게 된다.

항상 코드를 넣기 전에 이곳이 적당한 곳인지 아닌지를 한번더 생각한 후에 작업을 한다면, 편리를 위한 작업내용이 오류로 돌아오는 왜곡된 경우가 좀 줄어들 수 있지 않을까 싶다. <끝>

댓글