고수들이 절대 가르쳐 주지 않는 C/C++ 프로그래밍 팁 #1 - include guard

Posted at 2007.06.14 07:30 // in S/W개발/고절가주팁 // by 김윤수




(podics.com 에 올려 놓은 이 글에 대한 podcast도 같이 연결해 놓았습니다. 플레이 버튼을 누르고 아래 글을 읽으시면 이해에 도움이 될 것입니다)

기존에 제가 써 오던 C++ 이야기 시리즈와는 별도로 얘기하고 싶은 것이 있어서 새로운 시리즈로 "고수들이 절대 가르쳐 주지 않는 C/C++ 프로그래밍 팁"을 시작해 볼까 합니다. 시리즈 이름이 너무 길어서 고절가주팁이라고 줄여서 말해렵니. ^^ 얼마나 많이 나올 수 있을지는 모르겠지만 어찌 됐든 또 쭉~ 한 번 가보는 거죠.

첫번째 팁으로는 헤더 파일이 중복 포함되는 걸 방지하는 법에 대해 설명드릴까 합니다. 다 알고 계신다구요 ? 예~ 그러시겠죠. 알고 계신다면 C/C++ 프로그래밍 고수임에 틀림이 없습니다. 그러시다면 아래까지 쭉~ 읽어 보시고 잘못된 부분은 없나 추가할만한 얘기는 없나 보고 댓글 남겨 주시는 센~스! 부탁드리겠습니다.

제가 첫 회사에 입사하고 나서 처음으로 프로젝트를 할 때였습니다. 그 때, 누가 요구 분석하고, 구조 설계하고, 상세 설계하고 가이드해 줄 선배가 없었습니다. 제가 처음으로 해 본 프로젝트이자 회사도 처음으로 해 본 프로젝트였으니까요(저의 첫 회사는 조그만 중소기업이었습니다). 거의 그냥 맨땅에 헤딩하다시피 하면서 열정과 패기만으로 프로젝트를 수행하고 있었습니다.

정말 한참 동안 코딩을 하고 나서(머리 속으로 설계하고 바로 코딩에 들어갔죠. 좋은 방법은 아닙니다. 오해 없으시길...), 처음으로 컴파일을 시도했습니다. 무식하면 용감하다고 제 기억으로 거의 이삼만 라인 이상을 작성하는 동안 한 번도 컴파일하지 않다가 처음으로 그것도 보무도 당당하게 컴파일을 시도했습니다. 이제 컴파일만하고 실행시켜 보면 된다. 제 머리속에서는 완벽하게 돌았으니깐 문제 없을거야하며 엔터를 치는 순간 아니! 이게 웬 일입니까. 정말 수 많은 에러들이 수십 페이지가 넘도록 발생하다가 결국 컴파일러가 에러가 너무 많아서 컴파일할 수 없다고 포기해 버리는 것 아니겠습니까 ?

그 수 많은 에러를 보는 순간 어찌나 눈 앞이 캄캄하던지... 개발해 보면서 컴파일 에러에 압도된 적이 있으신가요 ? 우와~ 정말 미치겠더군요. 저는 그 때 정말 에러 메시지에 압도 돼 버렸습니다. 순간 개발자라는 직업이 나를 거부하나 보다 지금이라도 직종 전환할까하고 생각도 했습니다. 한동안 정신 못 차리고 있다가 겨우 마음을 추스리고 나서 이틀 내내 그 많은 컴파일 에러들 잡느라 고생 고생했습니다. 거짓말 아닙니다. 정말 컴파일 에러만 없애는데 이틀이 꼬박 걸렸습니다.

그런데 그 중에 제일 많은 에러를 발생시킨 게 어떤 거였는지 상상이 되십니까 ? 그건 바로 오늘의 주제인 헤더 파일 중복 포함 때문이었습니다. 그때 저는 프로그래밍 경험이 일천한지라, 그리고 누가 가르쳐 주는 사람도 없는지라 헤더 파일이 중복 포함되는 걸 방지하는 방법을 쓰는 게 아니라 헤더 파일 포함 순서를 바꿔보고, 코드를 이리 저리 옮겨 보고 하는 식으로 정말 죽을 똥 살 똥하면서 그 많은 컴파일 에러를 잡아 냈습니다. 정말 인간 승리였죠.

그땐 정말 악몽 같았지만 그런 경험이 있기 때문에 오늘 이런 글을 쓸 수 있는 것 아니겠습니까 ? 제가 이렇게 장황하게 제 개인적인 경험 이야기를 늘어 놓는 건 이 팁이 사소하게 느껴질 수도 있지만 정말 중요한 팁이라는 걸 말씀드리고 싶었기 때문입니다. 아마 보통은 제가 말씀 드리려는 이 팁이 회사의 Coding Standard 에 들어 있어서 그걸 따르면 제가 겪었던 문제는 발생하지 않을 것입니다. 어떻게 보면 그런 문제를 해결한 경험이 이미 회사의 무형 자산으로 쌓여 있는 것이겠죠. 아니면 이미 경험한 선배들의 머리 속에 있거나요.

자! 그럼 제가 경험한 문제를 여러분도 한 번 실제로 경험해 볼 수 있도록 다음 코드를 작성한 후 컴파일 해 보시겠어요 ?

// 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;
  }
};

// tips1.cpp 의 내용
#include "hdr1.h"           // tips1.cpp 에서도 hdr1.h 을 include 하네요
#include "hdr2.h"

int
main()
{
  DummyBase db(200);
  Dummy d(100);
}


이미 중복 포함 문제가 어떻게 발생하는지를 아시는 분은 어떤 에러가 어떤 지점에서 발생될지 상상이 되시죠 ? 제가 사용하는 컴파일러로는 다음과 같은 에러가 발생하네요.

c:\documents and settings\김윤수\my documents\visual studio 2005\projects\tips\tips1\hdr1.h(1) : error C2011: 'DummyBase' : 'class' type redefinition
        c:\documents and settings\김윤수\my documents\visual studio 2005\projects\tips\tips1\hdr1.h(1) : see declaration of 'DummyBase'
c:\documents and settings\김윤수\my documents\visual studio 2005\projects\tips\tips1\hdr2.h(3) : error C2504: 'DummyBase' : base class undefined
c:\documents and settings\김윤수\my documents\visual studio 2005\projects\tips\tips1\hdr2.h(5) : error C2614: 'Dummy' : illegal member initialization: 'DummyBase' is not a base or member
c:\documents and settings\김윤수\my documents\visual studio 2005\projects\tips\tips1\hdr2.h(9) : error C3861: 'Get': identifier not found
c:\documents and settings\김윤수\my documents\visual studio 2005\projects\tips\tips1\hdr2.h(13) : error C3861: 'Get': identifier not found
Build log was saved at "file://c:\Documents and Settings\김윤수\My Documents\Visual Studio 2005\Projects\Tips\Tips1\Debug\BuildLog.htm"
Tips1 - 5 error(s), 0 warning(s)


어때요 ? 보기만 해도 그냥 갑갑하시죠 ? 제가 표시한 파란 부분을 보니 'DummyBase' 클래스가 재정의됐다고 하네요. 그 다음 에러들은 다 첫번째 에러 때문에 파생된 에러들입니다. 이런 에러 패턴이 헤더 파일이 중복 포함되어 발생하는 것이라는 걸 알고 있는 사람은 해결하기가 쉽지만 잘 모르는 사람은 문제를 해결하는데 한참이 걸리게 마련입니다. 에러 메시지 자체가 클래스가 재정의됐다고만 말하고 있기 때문에 헤더 파일이 중복 포함되었다는 데까지 생각이 미치기 어려고, 게다가 그 다음에 딸려 오는 에러들은 진짜 문제점(root cause)를 찾는 데 방해만 되기 때문입니다.

자~ 그럼 이 문제를 어떻게 해결할 수 있을까요 ? 제가 생각할 수 있는 방법은 다음과 같습니다.

1) hdr1.h, hdr2.h 의 내용을 모두 tips1.cpp 로 모두 옮긴다. ^^;
2) hdr1.h 의 내용을 hdr2.h 로 옮기고 tips1.cpp 에서는 hdr2.h 만 include 한다.
3) 다른 건 다 그대로 놔두고 tips1.cpp 에서 hdr1.h 은 include 하지 않는다.

이것말고 다른 방법이 떠 오르시나요 ? 위 방법들은 여기에 제시된 코드에만 적용할 수 있는 임시방편적인 방법이라고 할 수 있구요, 좀 더 근본적인 방법은 모든 헤더 파일을 다음과 같이 작성하는 겁니다.

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

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

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


위와 같이 헤더 파일을 작성하게 되면 다음과 같은 과정이 일어나게 됩니다. 컴파일러와 역지사지해서 생각해 보겠습니다.

1. 처음으로 헤더 파일이 포함될 때, 컴파일러(좀 더 정확히 말하면 프리프로세서)가 01 라인을 만나게 되면 HDR_H 매크로가 정의되어 있지 않으므로 02 라인을 해석하게 됩니다.
2. 02 라인을 만나면 이제 HDR_H 매크로를 정의하게 됩니다.
3. 다음에 다시 같은 헤더 파일이 포함될 때는 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

다시 컴파일해 보시면 에러가 발생하지 않는 걸 확인하실 수 있을 겁니다. 이해가 되시나요 ? 아마 요즘 대부분의 개발 조직에서 이 정도의 팁은 Coding Standard 로 가지고 있지 않을까 합니다. Coding Standard 에 왜 그런 항목이 있었는지를 이해하실 수 있다면 더 좋겠지요. 이유는 헤더 파일이 중복 포함되어 발생하는 컴파일 에러를 방지하기 위한 목적입니다.

여러분 컴퓨터에 설치되어 있는 헤더 파일을 열어 보면 위와 같이 헤더 파일이 중복 포함되는 걸 방지하기 위한 방법을 사용하고 있는 걸 확인하실 수 있을 겁니다.

그럼, 이번 글은 이 정도로 마무리 하고, 다음에는 C/C++ Mixed Programming 기법에 대해 설명드리도록 하겠습니다. 고절가주팁이 쭉~ 갈 수 있도록 많은 관심 부탁드립니다. 여러분이 알고 있는 팁을 트랙백이나 댓글로 좀 알려 주시면 더할 나위 없이 좋겠네요. 그리고, 혹시 Podcast 로 들으신 분들은 느낌이 어떤지, 이해에 도움이 되는지 댓글로 좀 남겨 주시면 감사하겠습니다.

제 글이 유익하셨다면 오른쪽 버튼을 눌러 제 블로그를 구독하세요. ->
블로그를 구독하는 방법을 잘 모르시는 분은 2. RSS 활용을 클릭하세요.
RSS에 대해 잘 모르시는 분은 1. RSS란 무엇인가를 클릭하세요.

신고
  1. A2

    2007.06.14 07:38 신고 [수정/삭제] [답글]

    틈틈히 올려주시는 좋은 팁들 감사합니다.

  2. 쟤시켜 알바

    2007.06.14 07:38 신고 [수정/삭제] [답글]

    다 알고 있으리라고 생각해서 안알려줄지도 모르죠^^

    저도 위의 include를 이용한 클래스 정의를 아무도 안알려줬더랬죠...
    그래서 삽질했던 아려한 기억이 떠오릅니다.^^

    좋은 포스트 많이 올려주세요~

    • 김윤수

      2007.06.14 07:41 신고 [수정/삭제]

      예, 저도 이 팁을 솔직히 쓸 필요가 있을까 생각하기도 했는데, 아직 프로그래밍을 많이 안 해 본 분들을 같은 실수를 할 수 있을 것 같아서 올려 보기로 맘 먹었답니다.

  3. esstory

    2007.06.14 08:04 신고 [수정/삭제] [답글]

    #pragma once 이전에는 위와 같이 많이 코딩했었는데. 역시 한줄이라 #pragma once 을 자주 쓰게 되더군요 ^^;

  4. alones

    2007.06.14 09:48 신고 [수정/삭제] [답글]

    글 잘 읽었습니다.
    java나 c# 같은 좀 더 고급(?) 언어들은 C나 C++의 header file 중복 문제를 근본적으로 compiler가 알아서 해줄 것입니다.
    (※ 그렇다고 다 좋은 건 아닐 것입니다. 컴파일 속도 (어쩌면 run-time 시 속도도)를 아주 조금은 포기해야 할 것입니다)

    하지만 header file을 전처리 지시 (#ifdef or #if defined(), #pragma)를 써가며 (고민하며) 구성하고 어떤 경우는 실제 알면서도 PL이 전처리 지시자 사용을 금하는 경우들은

    좀더 나이스한 설계를 위함 일 것입니다.

    실제 library 성의 code가 아니면 source file (.cpp, c 등)에서 header file을 중복으로 include하는 것 자체가 (어쩌면) 설계를 좀 더 refine해야 하는 것을 의할 수도 있을 것입니다.
    (※ 앞 예에서 든 PL의 노림수이겠지요)

    실제 header file에 들어갈 내용 (class, struct, variable, etc)들을 잘 못 정해 두면 코드의 확장이나 변경 시 header file들 때문에 아주 고생하는 경우가 많았던 것 같습니다. (전처리 지시자도로 해결하기 힘든)
    ※ 그런 경우에는 cpp에서 include하고 type이 필요한 header에서는 class AClass; 로 임시 방편하기도 하지만.. 이와 같은 것을 하는 것 자체가 설계가 올바르지 못한 것을 의미할 것입니다.

    아무튼 ^^;; 기본적인 것이지만 너무나 중요하고 많은 것을 의미할 수 있는 글을 써주셔서 잘 읽고 갑니다. 좋은 밤 되세요~

  5. ㅁㄴㅇㄹ11

    2007.06.14 09:56 신고 [수정/삭제] [답글]

    라이브러리만 갖다 쓰는 분들은 아마 첨보는 내용일거라고 생각이 듭니다.ㅋ 꾸준한 연재부탁드려요^^;

  6. Paromix

    2007.06.14 19:07 신고 [수정/삭제] [답글]

    깔끔하게 정리해주셔서 감사합니다.
    정확한 내용은 모르고 사용만하고 있었는데, 덕분에 이해가 되었답니다.^^

  7. Jaws

    2007.06.14 20:51 신고 [수정/삭제] [답글]

    #pragma once는 비쥬얼C++계열에서만 동작하는 매크로가 아닌가요?
    그렇게 알고있는데;

  8. 풀리비

    2007.06.15 10:17 신고 [수정/삭제] [답글]

    안녕하세요, 김윤수님의 포스팅은 항상 기쁘게 보고 많이 배우고 있습니다. 얼마 전에는 멤버포인터라는 난생 처음보는 C++ 문법까지 알게 되었습니다. (C++ Primer Plus란 책에도 없던...)
    #ifndef _HEADER_H
    #define _HEADER_H
    ...
    #endif
    와 같은 헤더 중복을 방지하기 위한 guard조차도 중복이 생길 수 있어 MS VC++ 컴파일러 명령으로 #pragma once라는 것을 만들었다고 합니다.

    중복 포함은 그나마 해결이 그리 어려운 것이 아닌데, 더 어려운 것은 상호 포함이라고 생각합니다.. 이거는 컴파일러 에러 메시지가 애매해서 더 헷갈립니다. MFC에서 View-Document 기반 클래스들을 정의하고 쓰다 보면, 두 개의 클래스가 서로를 직접 참조하는 것이 성능이나 코딩 상으로 매우 편리할 때가 있습니다. 그럴 때는 고육지책으로 forward declaration을 사용하는 꼼수를 쓰곤 합니다...

    앞으로도 좋은 포스팅 부탁합니다.

  9. 지호

    2007.08.05 12:52 신고 [수정/삭제] [답글]

    보통 매크로 가드나 인클루드 가드라고 부르죠?
    이름을 딱 정해주는게 여러모로 좋을 것 같아요.

    폴리비 > 사용자가 직접 만드는 이름은 밑줄문자(_)로 시작하지 않는게 좋습니다. 매크로 이름도 마찬가지고요. 밑줄 문자로 시작하는 이름은 컴파일러에서 사용하기로 약속한것이기 때문에 이름 충돌 가능성이 더 높습니다.

  10. 완전초보

    2007.08.28 22:31 신고 [수정/삭제] [답글]

    좋은 팁 잘보고 갑니다^^*

  11. 최익필

    2008.06.04 07:31 신고 [수정/삭제] [답글]

    저도 #pragma once 를 많이 쓰게 되더라구요.

  12. 강민호

    2010.05.30 18:46 신고 [수정/삭제] [답글]

    좋은 글 남겨 주셔서 감사합니다^^ 저는 주로 자바로 일해왔는 데 갑자기 c, c++로 해야될 어려운 프로젝트가 생겨서 난감하던 차에 이렇게 좋은 글을 보겠됐네요~ 평안하세요^^

  13. 드디어

    2013.10.16 18:51 신고 [수정/삭제] [답글]

    찾았습니다!! 왜 쓰는지 알고싶어서 구글링하던 도중 이제야.. ㅠㅠ 감사합니다!

  14. 지나가던행인

    2015.10.16 05:23 신고 [수정/삭제] [답글]

    저는 cpp을 아두이노 헤더파일 만들면서 배웠는데, 다행히 처음부터 저걸 알려주는데서 배워 무리 없이 넘어갔습니다. 하지만 저걸 왜 쓰는지는 오늘 첨 알았네요 ㅋㅋ 감사합니다.

  15. ㅇㅇ

    2017.01.13 07:53 신고 [수정/삭제] [답글]

    글을 포스팅하신지 10년이 지난 뒤에도 지망생이 읽으니 너무나 좋은 글인것 같습니다 감사합니다!!!!!

  16. anna

    2017.10.24 22:13 신고 [수정/삭제] [답글]

    include guard를 배우면서 도대체 이걸 왜 해야되는지 위에 떡하니 써있는데 같은 헤더파일을 include할 일이 어딨지?라고 생각했는데 다른파일에서 중복으로 포함시키는 경우가 생기는군요... 생각이 짧았네요.. 예시가 참 찰떡같군요 큰 도움 받았습니다 감사합니다

댓글을 남겨주세요.