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

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

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

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

어떤 오류가 발생하길래..?

hdr1.h 파일
class DummyBase {
private:
      int _i;
public:
      DummyBase(int i): _i(i) {
      }
     int Get() {
           return _i;
     }
     void Set(int i) {
          _i = i;
      }
};

 hdr2.h 파일
#include "hdr1.h"              // hdr1.h 을 여기서 include 하고 있습니다
class Dummy: DummyBase {
public:
     Dummy(int i): DummyBase(i) {
     }
     int operator() () {
          return Get();
     }
     int operator() (int multi) {
          return Get() * multi;
     }
};

main.cpp 파일
#include "hdr1.h"           // main.cpp 에서도 hdr1.h 을 include 하네요
#include "hdr2.h"
int main()
{
     DummyBase db(200);
     Dummy d(100);
}



main.cpp 에서 한번포함한 hdr1.h 의 헤더를 또 포함하게 되기때문에 에러를 내게된다


헤더파일의 중복정의 방지

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

그럼 어떻게 해야 중복을 피할수있을까?
1) hdr1.h, hdr2.h 의 내용을 모두 main.cpp 에 옴긴다
2) hdr1.h 의 내용을 hdr2.h 로 옮기고 main.cpp 에서는 hdr2.h 만 include 한다.
3) 다른 건 다 그대로 놔두고 tips1.cpp 에서 hdr1.h 은 include 하지 않는다.

작은프로그램이고 포함관계가 적다면 이렇게해도 해결될것이다. 하지만 그게 아니라 프로그램이 거대하고 포함관계가 복잡하다면 .. 그래도 이렇게 하는게 좋을까?
파일을 나누는 이유를생각해보자. 과연 이게 현명한 선택인지..

그럼 어떻게해야할까? 답은 좀 더 근본적인 방법은 모든 헤더 파일을 다음과 같이 작성하는 것이다

// 헤더 파일의 제일 첫 부분
01: #ifndef HDR_H     // 헤더 파일명을 대문자한 매크로가 정의되어 있지 않으면
02: #define HDR_H     // 헤더 파일명 매크로를 정의한다

......                // 헤더 파일 본 내용이 여기 들어갑니다

03: #endif            // ifndef HDR_H 에 매치됩니다


그럼 이방법을 이용해 해결해보자

hdr1.h 파일
#ifndef HDR1_H
#define HDR1_H

class DummyBase {
private:
     int _i;
public:
     DummyBase(int i): _i(i) {
     }
     int Get() {
          return _i;
     }
     void Set(int i) {
          _i = i;
     }
};
#endif

hdr2.h
 파일
#ifndef HDR2_H
#define HDR2_H
#include "hdr1.h"              // hdr1.h 을 여기서 include 하고 있습니다

class Dummy: DummyBase {
public:
     Dummy(int i): DummyBase(i) {
     }
     int operator() () {
          return Get();
     }
     int operator() (int multi) {
          return Get() * multi;
     }
};
#endif

main.cpp 은 같으므로 생략하도록하자.
이렇게 하면 main.cpp 에 있는 #include "hdr1.h" 는 포함하지않고 넘어가게 된다.

다중포함관계
 아래의 예제를 한번 보자. 데이터구조체 등이 정의된 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"가 나오는 경우처럼 말이다.

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

실전엔 수없이 많은 파일이 있다고 했다. 그렇다고 헤더파일을 죄다 소스파일에 인클루드하긴 사실 무리다. 이런 때에는 레이어별로 별도의 헤더파일을 구성하는 방법을 추천한다.

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

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

항상 생각을 하고 살자

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

다만 코드가 길어지고, 그 와중에 대폭 수정을 하거나 구조를 변경하게 되면서부터 정리가 안되기 시작하는 경우가 많다. 코딩을 하다 지치면 귀차니즘(?)의 압박에 못이겨서 적당하지 못한 곳에 한줄 한줄이 들어가다보면 걷잡을 수 없게 된다.
항상 코드를 넣기 전에 이곳이 적당한 곳인지 아닌지를 한번더 생각한 후에 작업을 한다면, 편리를 위한 작업내용이 오류로 돌아오는 왜곡된 경우가 좀 줄어들 수 있지 않을까 싶다.


출처 및 참고
http://chanywa.com
http://yesarang.tistory.com/64

Tag //

Trackback Address :: http://couple.haruschool.com/tc/trackback/35

댓글을 달아 주세요