서론

난 서버 프로그래머라면 Go와 C++ 에는 아주 익숙해야 한다고 생각한다. 물론 레거시나 코드베이스를 생각해서 Java 도 잘해야 한다고 생각은 한다(Java는 개인적으로 정말 싫어하지만서도…ㅠㅠ). 내 생각에 나의 C++ 능숙도는 초보 C++ 개발자와 일반 C++ 개발자 사이에 위치하지만, 회사 생활을 하며 겪은, 기초적인 C++ 에 다뤄보려고 한다.

내 생각에 C++는 꽤나 전지 전능한 언어이다. 요즘은 (심지어) 임베디드에서도 많이 사용되고 있고, 대다수의 저명한 핵심적인 기술(예를 들면 MongoDB, V8, Tensorflow 등) 에는 전부 C++가 백엔드에서 동작한다.

한편 C++는 굉장히 어려운 언어이기도 하다. 여기서 C++ 가 어렵다는 말은 다음과 같다.

  • 문법이 어렵기도 하다
    • 다른 프로그래밍 언어에 비해 배울 것이 굉장히 많다
    • 그래도 시간이 지나면 익숙해지기는 한다
    • C 언어와의 혼동성: C++은 C 에서 Class 만 추가한 게 아닙니다!!! 엄격 근엄 진지.
    • 연도별 언어 표준: C++98, C++11, C++14, C++17, C++20… =_=
  • 운영체제 마다 프로그램을 다르게 작성해야 한다
  • 배포도 쉽지 않다: 라이브러리의 종류(정적/동적)에 따라 다른 배포 전략
  • 메모리 관리가 쉽지 않다: 단 1바이트라도 누적되면 언젠간 뻗고 말 것이다
  • 좋은 소프트웨어를 만들기는 더 어렵다
    • 대부분의 프로그램이 기본적인 흐름은 동일하지만, 요구사항에 맞는 소프트웨어 아키텍처를 설계해야 한다
    • C++ 는 굉장히 Low Level 에서 동작하기 때문에, 운영체제의 동작에 대해 자세히 이해해야 한다

따라서 C++를 잘한다는 말은 단순히 C++ 문법에 익숙하다는 것이 아니다. C++ 을 잘하는 사람은 모든 것을 잘 할 가능성이 높다 (그런 사람에게 컴퓨터에 관해 못하는 것이 있다면, 그것을 잘하기까지 단지 약간의 시간 문제에 불과하리라 생각한다).

프로그램을 실행한다는 것

모든 컴파일 언어가 마찬가지이겠지마는, C++ 역시 제대로 알고 나면 프로그램이 무엇인지에 대해 근본적인 시각을 바꿔버린다.

  • (초등학생 때의 나): 프로그램 실행하는 거? 그냥 .exe 파일 실행하는 거 아니에요?
  • (지금의 나): 프로그램을 실행하는 행위란 파일 디스크립터를 읽어, 디스크에 존재하는 해당 파일을 메모리에 적재한 후, 메모리에 적재된 기계어를 수행하는 것을 말한다.

어셈블리어

기계어(Machine code) 에 대한 정의 자체는 위키피디아에 따르면 ‘컴퓨터의 CPU 에 의해 직접 수행되는 명령어 집합’ 이다. 기계어는 또한 ‘어셈블리어(Assembly Language)’ 라고 불린다.

기계어, 즉 어셈블리어는 CPU 가 직접 수행하는 최소한의 단위이므로, 소프트웨어적인 요소로는 어셈블리어를 실행하는 것보다 빠른 성능을 낼 수 없다. 즉, 성능을 단지 더욱 빠른 실행속도 라고 생각한다면 프로그램의 성능은 해당 프로그램이 목표하는 바와 동일한 동작을 하는 어셈블리어의 길이 라고 볼 수 있다.

어셈블리어를 직접 작성하는 일은 생각만큼 괴랄하게 어렵지는 않다. 단순히 본인이 작성하고자 하는 CPU 에서 지원하는 어셈블리어를 공부하고, 컴퓨터의 구조에 대한 이해만 있으면 나머지는 시간 문제이기 때문이다. 근데 어셈블리어로 무언가 만드는 것은 너무 심각하게 어렵다. 작성해야 할 어셈블리어가 너무 너무 너무나도 많기 때문이다. 그래서 어셈블리어로 무언가 직접 만드는 경우는 핵심적인 엔진을 만드는 경우(그것도 일부분의 로직만) 밖에 없다고 하더라. 나도 예전에 좀 해봤는데, 작성하다가 중간에 틀려서 그 틀린 지점부터 밑단을 전부 다시 작성해야 한다는 사실에 좌절하고 포기했었다…

그래서 우리 인간은 컴파일러에게 어셈블리어를 만들어달라고 하고, 컴파일러를 정교하게 잘 만들었다. 지금은 컴파일러가 사람보다 똑똑하게 일을 잘 한다고 하니, 그냥 컴파일러를 믿도록 하자.

컴파일

컴파일(Compile)은 ‘사람이 이해할 수 있는 코드’를 기계가 이해할 수 있는 언어로 바꿔주는 작업이다. 주로 기계가 이해할 수 있는 언어는 어셈블리어를 뜻하지만, 아닌 경우도 있다 (Java의 bytecode). 컴파일 또한 재미있는 내용이 많지만, 토픽을 벗어나므로 나중에 더 생각하기로 하자.

C++ 코드를 컴파일 하면 확장자가 .o 인 파일이 나온다. Object code, 목적 코드 파일은 어셈블리어로 이루어진 바이너리 파일이며, 이는 실행할 수 있는 프로그램의 일부 조각이다. 하지만 ‘일부 조각’ 이므로 단지 목적 코드 파일만 가지고서는 운영체제에서 실행할 수는 없다.

라이브러리

라이브러리(Library) 라는 용어 자체는 어떠한 기능을 제공하기 위하여 내부 동작을 감추고, 외부 인터페이스만으로 원하는 동작을 할 수 있도록 한 개념이다. 수학적으로 생각하면 함수와 유사하다. 다만 반드시 출력 결과물이 존재하지는 않기 때문에 함수와 같지는 않다.

C++ 에서 명하는 라이브러리는 컴파일 된 결과물을 이용하여 다른 사람이나 다른 프로그램에서 해당 라이브러리가 제공하는 기능을 사용할 수 있도록 한다. 그래서 사람이 알아볼 수 있는 코드의 형태로 되어 있지 않고, 기계가 알아볼 수 있는 형태로 되어있다. 이 글은 C++ 에 관한 글이므로, 앞으로 칭하는 라이브러리는 전부 C++ 에서 명하는 라이브러리를 의미한다.

라이브러리에는 정적 라이브러리(Static Library)와 동적 라이브러리(Dynamic Library) 두 종류가 존재한다. 정적 라이브러리는 해당 라이브러리가 제공하는 기능을 수행하기 위한 코드를 모두 포함하는 라이브러리이며, 따라서 동적 라이브러리에 비해 크기가 크다. 반면 동적 라이브러리는 해당 라이브러리가 제공하는 기능 외의 기능을 서로 참조하며 동작하는 라이브러리이며, 크기가 작은 편이다.

링킹

링크(Link), 또는 링킹(Linking)은 ‘컴파일 된 결과물’ 들을 연결해서 운영체제에서 실행 가능하도록 만들어주는 작업이다.

여기서 컴파일 된 결과물은 비단 우리가 작성한 코드 뿐 아니라 남이 작성한 코드까지도 포함한다. 따라서 Linux 에서 제공하는 컴파일 된 결과물(표준 라이브러리)이 있을 것이며, 우리가 작성한 코드도 있을 것이다.


장황하면서도 간략하게 프로그램의 기본에 대해 설명했다. 다음에는 Linux 상에서 실제로 어떻게 코드를 컴파일하고, 라이브러리를 만들고, 링크 하는지 다뤄보자.