전방선언 : 컴파일러가 필요한 정보

https://eeeuns.github.io/2024/08/09/understandingcomputercompileo/ https://eeeuns.github.io/2024/09/03/reducebuild/

컴파일 과정에 대한 포스팅은 몇번했었는데, 이 중 전방선언에 대해 이야기하고자 한다.

컴파일과 링킹 관점

개별 cpp 파일을 컴파일할 때, 정보가 없는 심볼을 사용한다면 컴파일러는 이 심볼에 대한 정보를 모르므로 컴파일 에러를 낸다.

링킹 단계에서 해당 심볼의 상세 정보가 없으면 링킹 에러가 발생한다. 그러나 컴파일 단계에서는 개별 파일별로 해당 심볼이 유효하다는 정보(이 심볼이 다른 파일에 있다는 정보 포함)만 있으면 충분하다.

전방 선언은 이때 작성자가 “이 심볼은 알고 있고, 다른 파일에 있다”는 정보를 컴파일러에게 알려주는 것이다.

컴파일러는 심볼이 존재함을 알고, 파일을 컴파일하는 데 상세 정보가 필요하지 않다면 해당 파일을 정상적으로 컴파일한다.

전방선언의 첫 만남과 그 의미

전방선언은 보통 순환 include 참조 문제를 겪을 때 처음 접하게 된다. 사실 컴파일 속도 최적화의 측면에서도 굉장히 중요하다.

헤더 파일의 #include를 최대한 줄이고 크기 자체를 감소시키는 것이 중요하다.

컴파일 시간 개선으로 헤더 파일 종속성을 정리하는 작업을 하다 보면 어떤 경우에 전방선언이 가능하고 불가능한지 알게 된다. 원리는 컴파일러 동작 방식을 생각하면 쉽게 이해할 수 있다.

말했듯이 전방선언은 C 언어에서 가장 처음 배우는 선언과 정의 중 그 선언에 해당한다.

선언은 컴파일러에게 정확한 정보를 알지 못하는 심볼에 대해 “이 심볼이 이후에 있거나 다른 파일에 있다”는 정보를 전달하는 것이다. 선언은 헤더 파일의 존재 의의 그 자체다.

포인터와 전방선언

64비트 프로세스에서 포인터는 항상 8바이트다. 따라서 모든 자료형을 포인터로 다룬다면 객체를 전방선언만으로 사용할 수 있다. 이것이 Pimpl 패턴의 핵심 아이디어다.

전방선언이 불가능한 경우들

헤더 파일의 #include를 제거할 때 전방선언으로 불가능한 것들이 있다.

C++을 컴파일할 때 함수 주소는 링킹 단계에서 처리할 수 있다. 하지만 함수가 가지는 스택 크기 등은 컴파일 단계에서 결정되어야 한다. 즉, 객체의 실제 크기는 컴파일 단계에서 확정되어야 한다. (실제로 static_assertsizeof 비교를 하는 것이 흔하다.)

1. 상속

상속은 상속받는 객체의 크기를 계산할 때 필수 정보다. 따라서 전방선언으로는 객체 정보를 컴파일 타임에 알 수 없어 불가능하다.

2. Composition

이것도 객체의 크기를 계산할 때 필수 정보다. 그러나 포인터로 다룬다면 가능하다.

3. Template

내부 구현체를 접근하는 경우 객체 크기와 함수 정보가 필수다.

포인터 멤버 변수는 어쩔 수 없는 방안이지만, 컴파일 관점에서는 상속보다 composition이 낫다.

Template과 구현 특성

Template의 구현 특성상 구현체가 헤더 파일에 있어야 한다. 전방선언과 상성이 최악이며, 이로 인해 헤더 파일 #include를 많이 포함할 수밖에 없다.

위 케이스에 적용은 불가능하겠지만, Template 자료구조 같은 경우는 컨테이너 같은 클래스들을 void*로 다룰 수 있는 경우도 있다. 이렇게 하면 Template 사용을 최대한 줄일 수 있다.

마치며

헤더 파일 개선을 하려고 몸을 비틀어봤지만 수정 범위가 큰 작업이고 코드 복잡도가 올라가는 문제가 있다. 유니티 빌드(Unity Build)을 알아보자..

C#에는 partial class라는 똑똑한 기능이 있지만, 오래된 C++에는 그런 기능이 없다.

이러한 C++의 제약으로 인한 다양한 우회 방법론이 나왔지만, 모듈 시스템을 사용하는 현대의 언어에는 모두 의미 없는 기술일 것이다. 결국 이것은 미래에 잃어버린 기술이 되지 않을까 싶다.

Posted 2025-11-24