-----------------
title : 씹어먹는 C++ - <19 - 1. Make 사용 가이드 (Makefile 만들기)>
cat_title : 19 - 1. Make 사용 가이드 (Makefile 만들기)
chapter : C++ 개발자가 알면 좋을 것들
path : /C++
publish_date : 2020-06-10
tex_title : Make 사용 가이드 (Makefile 만들기)
next_page : 332
-----------------

이번 강좌에서는

* \serif{make} 의 작동 이해
* *Makefile* 문법 이해
* 만능 *Makefile* 만들기

에 대해서 다루어 보겠습니다.

![](/img/ChewingCpplogo.png)

안녕하세요. 이번 강좌 부터는 씹어먹는 C++ 시리즈의 **부록** 과 같은 개념으로 C++ 언어 자체와는 직접 관련은 없지만 실제로 C++ 을 프로그래밍 하기 위해서 필요한 지식들과 툴들에 대해서 이야기 하고자 합니다. 그 첫 번째 타자로 *Makefile* 만들기가 되겠습니다.

아무래도 리눅스 환경에서 프로그래밍을 하신 분들은 아시겠지만, 보통 프로그램을 컴파일 할 때 \serif{make} 라는 프로그램을 사용합니다. 윈도우에서 비주얼 스튜디오를 사용하신다면 컴파일 버튼을 누르면 알아서 컴파일 되는 것과는 달리, 쉘 상에서 컴파일을 하려면 **어떤 파일들을 컴파일 하고, 어떠한 방식으로 컴파일 할 지 직접 컴파일러에게 알려줘야 합니다**.

물론 매번 명령어를 치면 문제가 없겠지만 프로젝트의 크기가 커지고 파일들이 많아진다면 매번 그렇게 친다는 것이 불가능에 가까워집니다. 이 문제를 해결하기 위해서 리눅스에서는 \serif{make} 라는 프로그램을 제공하는데, 이 프로그램은 *Makefile* 라는 파일을 읽어서 주어진 방식대로 명령어를 처리하게 합니다. 덕분에 많은 수의 파일들을 명령어 한 번으로 컴파일 할 수 있습니다.

이번 글에서는 \serif{make} 프로그램이 어떠한 방식으로 작동되고, 프로그램들을 우리가 원하는 방식으로 컴파일 하기 위해서는 어떠한 방식으로 *Makefile* 을 작성해야 하는지 살펴보도록 하겠습니다.

### 소스 코드에서 완성된 프로그램까지

먼저 \serif{make} 가 어떠한 방식으로 작동하는지 알기 전에 컴파일러를 통해 프로그램을 빌드하게 되면 어떠한 방식으로 소스 코드에서 하나의 프로그램이 완성되는지 살펴보도록 합시다.

#### 컴파일 (Compile)

가장 먼저 여러분의 머리 속으로 떠오르는 과정은 바로 **컴파일(Compile)** 일 것입니다. 이 컴파일이라는 과정은 여러분의 소스 코드를 컴퓨터가 이해할 수 있는 어셈블리어로 변환하는 과정입니다.

예를 들어서 아래와 같이 `foo.h` 에 정의된 `foo` 함수와 `bar.h` 에 정의된 `bar` 함수를 `main` 함수에서 호출하는 간단한 프로그램을 살펴봅시다.

* foo.h

```cpp-formatted
int foo();
```

* foo.cc

```cpp-formatted
#include <iostream>

#include "foo.h"

int foo() {
  std::cout << "Foo!" << std::endl;
  return 0;
}
```

* bar.h

```cpp-formatted
int bar();
```

* bar.cc

```cpp-formatted
#include <iostream>

#include "bar.h"

int bar() {
  std::cout << "Bar!" << std::endl;
  return 0;
}
```

* main.cc

```cpp-formatted
#include "bar.h"
#include "foo.h"

int main() {
  foo();
  bar();
}
```

예를 들어서 `main.cc` 를 컴파일 하기 위해서는 아래와 같이 하면 됩니다.\sidenote{만일 clang 을 사용하고 싶다면 g++ 자리에 clang 을 넣으시면 됩니다. clang 과 g++ 은 사용 명령어가 거의 똑같습니다.}

```info-term
$ g++ -c main.cc
```

`g++` 에 전달하는 `-c` 명령어는 인자 다음에 주어지는 파일을 컴파일 해서 **목적 파일(object file)** 을 생성하라는 의미 입니다.

실제로, 성공적으로 컴파일 하였다면 아래와 같이 `main.o` 라는 파일이 생성된 것을 알 수 있습니다.

```info-term
$ ls main.o
main.o
```

이 `main.o` 는 `main.cc` 를 컴파일 한 어셈블리 코드가 담겨있는 파일 입니다. 실제로 `main.o` 를 뜯어보면

```info-term
$ objdump -S main.o

main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
	 0:	f3 0f 1e fa          	endbr64 
	 4:	55                   	push   %rbp
	 5:	48 89 e5             	mov    %rsp,%rbp
	 8:	e8 00 00 00 00       	callq  d <main+0xd>
	 d:	e8 00 00 00 00       	callq  12 <main+0x12>
	12:	b8 00 00 00 00       	mov    $0x0,%eax
	17:	5d                   	pop    %rbp
	18:	c3                   	retq   
```

와 같이 `main.cc` 의 어셈블리 코드가 잘 들어있음을 알 수 있습니다.\sidenote{위 어셈블리가 뭔지 이해가 전혀 안가도 Make 가 뭔지 이해하는데 전혀 상관 없습니다. 그냥 '목적파일에는 소스 코드를 컴파일 한 어셈블리가 들어 있구나' 정도로 이해하시면 됩니다.}

한 가지 재미있는 점은 컴파일러가 `main.cc` 를 컴파일 할 때 다른 파일들을 참조하지 않았다는 점입니다. 실제로 `main.o` 의 어셈블리를 슬쩍 보면 `foo` 나 `bar` 에 관한 내용이 하나도 없음을 알 수 있습니다.

따라서 `main.o` 자체로는 우리의 프로그램을 만들 수 없습니다. 왜냐하면 `main.o` 에는 **`foo` 라는 함수를 호출해라!** 라는 내용만 있지, **`foo` 는 어디에 있고 이러이러한 방식으로 동작한다** 에 관한 이야기는 없기 때문이죠.\sidenote{눈썰미가 좋으신 분들은 위 어셈블리의 callq 자리에 함수 이름 대신에 이상한 값이 들어있음을 알 수 있습니다.} 실제로 작동하는 `foo` 에 관한 정보를 얻기 위해서는 `foo.cc` 를 컴파일 해서 만들어진 `foo.o` 가 필요하고, 마찬가지로 `bar` 에 관한 정보를 얻기 위해서는 `bar.cc` 를 컴파일 해서 만든 `bar.o` 가 필요합니다.

따라서 실제 프로그램을 제작하기 위해서는 이 각각의 `main.o, foo.o, bar.o` 를 하나로 합치는 과정이 필요하겠죠. 이와 같은 과정을 아래 그림과 같이 **링킹(Linking)** 이라고 합니다.

![](/img/cpp/a.1.1.png)

#### 링킹 (Linking)

링킹이 이름 그대로 **링크 하는 작업** 인 이유는 실제로 서로 다른 파일에 흩어져 있던 함수나 클래스들을 한 데 묶어서 링크해주는 작업이기 때문이죠. 이 과정에서 `main` 함수 안에 `foo` 함수가 어디에 정의되어 있는지 위치를 찾고 제대로 된 함수를 호출할 수 있게 됩니다. 사실 이 링킹에 관해서 이야기만 해도 한참 할 수 있는데, 일단 이 글의 목표는 빌드 파일 만들기 이므로 여기서 줄이겠습니다.

아무튼 이 링킹 과정은 아래와 같이 컴파일러에 목적파일을 전달함으로써 수행됩니다.

```info-term
$ g++ main.o foo.o bar.o -o main
```

여기서 `-o` 옵션은 그 뒤에 링킹 후에 생성할 실행 파일 이름을 이야기 합니다. 위 경우 `main` 이라는 실행 파일을 만들었고 만약에 이를 따로 지정하지 않는다면 그냥 `./a.out` 이라는 파일을 디폴트로 생성하게 됩니다.

실제로 `main` 파일을 실행하게 된다면

```exec
Foo!
Bar!
```

와 같이 잘 나옵니다.

### 그렇다면 왜 make 를 쓰는지?

일단 위와 같이 `main` 실행 파일을 생성하기 위해서 입력한 쉘 명령어를 쭉 나열해보자면;

```info-term
$ g++ -c main.cc
$ g++ -c foo.cc
$ g++ -c bar.cc
$ g++ main.o foo.o bar.o -o main
```

와 같습니다. 물론 파일 개수가 작다면 매 번 입력하는 것이 크게 문제가 되지는 않습니다만, 프로젝트 크기가 커지면 쳐야할 명령어가 더 많아지게 되서 복잡하겠지요 (특히 파일들이 다른 디렉토리에 있는다면 말이죠.)

물론 쉘 스크립트를 조금 아시는 분이라면

> 그냥 위 명령어들을 순차적으로 실행하는 쉘 스크립트를 짜면 되는거 아닌가?

라고 물으실 수 있습니다. 그런데 이 방식으로 한다면 한 가지 문제점이 있는데, 예를 들어서 여러분이 `main.cc` 를 수정하였다고 해봅시다.

그렇다면 실제로는

```info-term
$ g++ -c main.cc
$ g++ main.o foo.o bar.o -o main
```

딱 이 두 명령어만 실행하면 됩니다. 왜냐하면 `main` 파일을 바꾼다고 해서 `foo` 나 `bar` 의 컴파일 된 내용이 바뀌지 않기 때문이죠. 하지만 위 처럼 단순하게 쉘 스크립트를 짜게 된다면 파일 하나만 바꿔도 전체 모든 파일들을 컴파일 하게 되므로 컴파일 시간이 **매우** 길어지게 됩니다. 하지만 \serif{make} 를 사용하면 일부 파일을 수정할 경우 필요한 명령만 빠르게 컴파일을 할 수 있도록 도와줍니다.

자 그렇다면 거두절미하고 \serif{make} 의 사용법과 \serif{make} 를 위한 *Makefile* 을 어떻게 작성하는지 알아보겠습니다.

### make

\serif{make} 는 간단히 말하자면 **주어진 쉘 명령어들을 조건에 맞게 실행하는 프로그램** 이라고 볼 수 있습니다. 이 때 어떠한 조건으로 명령어를 실행할지 담은 파일을 *Makefile* 이라고 부르며, \serif{make} 를 터미널 상에서 실행하게 된다면 해당 위치에 있는 *Makefile* 을 찾아서 읽어들이게 됩니다.

그렇다면 *Makefile* 에는 어떠한 방식으로 조건을 기술할까요?

```info-verb
target … : prerequisites …
(탭)recipe
				…
				…
```

*Makefile* 은 기본적으로 위와 같이 3 가지 요소로 구성되어 있습니다.

#### target

\serif{make} 를 실행할 때 `make abc` 과 같이 어떠한 것을 \serif{make} 할 지 전달하게 되는데 이를 타겟(target) 이라고 부릅니다. 만일 `make abc` 를 하였을 경우 타겟 중에 `abc` 를 찾아서 이에 대응되는 명령을 실행해줍니다.

#### 실행할 명령어(recipes)

주어진 타겟을 \serif{make} 할 때 실행할 명령어들의 나열입니다. 한 가지 중요한 점은 recipe 자리에 명령어를 쓸 때 **반드시 탭 한 번으로 들여쓰기를 해줘야만 합니다**. 보통 요즘의 편집기의 경우 (예를 들어 VSCode), 자동으로 탭을 스페이스로 바꿔주는 옵션이 활성화 되어 있을 텐데 \serif{make} 가 *Makefile* 을 제대로 읽어들이기 위해서는 반드시 탭을 사용해야만 합니다!

#### 필요 조건들(prerequisites)

주어진 타겟을 \serif{make} 할 때 사용될 파일들의 목록 입니다. 다른 말로 의존 파일(dependency) 이라고도 합니다. 왜냐하면 해당 타겟을 처리하기 위해 건드려야 할 파일들을 써놓은 것이기 때문입니다.

**만일 주어진 파일들의 수정 시간 보다 타겟이 더 나중에 수정되었다면 해당 타겟의 명령어를 실행하지 않습니다.** 왜냐하면 이미 이전에 타겟이 만들어져있다고 간주하기 때문이죠.

예를 들어서 타겟이 `main.o` 이고, 명령어가 `g++ -c main.cc` 라면, 필요 조건들은 `main.cc`, `foo.h`, `bar.h` 가 되겠죠. 왜냐하면 이들 파일들 중 하나라도 바뀐다면 `main` 을 새로 컴파일 해야 하기 때문이죠. 반면에 `main.o` 의 생성 시간이 `main.cc`, `foo.h`, `bar.h` 들의 마지막 수정 시간 보다 나중이라면, 굳이 `main.o` 를 다시 컴파일 할 필요가 없습니다.

그렇다면 위 내용을 바탕으로 *Makefile* 을 간단하게 구성해보겠습니다.

```info-verb
foo.o : foo.h foo.cc
	g++ -c foo.cc

bar.o : bar.h bar.cc
	g++ -c bar.cc

main.o : main.cc foo.h bar.h
	g++ -c main.cc

main : foo.o bar.o main.o
	g++ foo.o bar.o main.o -o main
```

```lec-warning
위 Makefile 을 그대로 복사해서 실행한다면 아마 실행이 안될 것입니다. 왜냐하면 탭이 스페이스로 바뀌어서 그런데요, 제대로 Makefile 을 만드시기 위해서는 g++ 앞에 오는 스페이스 두 번을 탭으로 바꾸어서 저장해보세요.
```

우리의 목표는 실행 파일인 `main` 을 만드는 것이기 때문에 `make main` 을 실행해보면

```info-term
$ make main
g++ -c foo.cc
g++ -c bar.cc
g++ -c main.cc
g++ foo.o bar.o main.o -o main
```

그리고 실제로 `main` 이 잘 만들어짐을 알 수 있습니다.

여러분이 한 번도 컴파일 하지 않은 상태에서 `make main` 을 실행하면 \serif{make} 파일은 다음과 같은 순서로 명령어를 처리합니다.

1. `make main` 이니까 *Makefile* 에서 타겟이 `main` 인 녀석을 찾아보자.
2. 오. `main` 의 필요한 파일들이 `foo.o bar.o main.o` 이네? 이들 파일을 어떻게 만드는지 각각의 파일 이름으로된 타겟들을 찾아보자.
3. `foo.o` 의 경우 필요한 파일이 `foo.cc` 네? 아직 `foo.o` 가 없으니까 주어진 명령어를 실행해야 하겠군!
4. 마찬가지로 `bar.o, main.o` 도 컴파일
5. 자 이제 마지막으로 `g++ foo.o bar.o main.o -o main` 를 실행해야지

만약에 여러분이 `make main` 을 한 번 실행한 상태에서 `foo.cc` 만 수정하였다고 해봅시다. 그렇다면 아래와 같이 필요한 부분만 재컴파일 됩니다.

1. `make main` 이니까 *Makefile* 에서 타겟이 `main` 인 녀석을 찾아보자.
2. 오. `main` 의 필요한 파일들이 `foo.o bar.o main.o` 이네? 이들 파일을 어떻게 만드는지 각각의 파일 이름으로된 타겟들을 찾아보자.
3. `foo.o` 의 경우 필요한 파일이 `foo.cc` 네? 그런데 `foo.o` 의 생성 시간 보다 `foo.cc` 의 수정 시간이 더 나중이군! 주어진 명령어를 다시 실행해야겠어
4. `bar.o` 의 경우 필요한 파일이 `bar.cc` 인데, `bar.o` 의 생성 시간이 `bar.cc` 의 수정 시간 보다 나중이네. 굳이 명령어들을 실행하지 않아도 되겠어
5. `main.o` 도 마찬가지로 다시 안바꿔도 되겠군!
6. `main` 의 필요한 파일들 중 `foo.o` 가 바뀌었으니 내 명령어들도 다시 실행해야겠어.

따라서 딱 필요한 `g++ -c foo.cc` 와 `g++ foo.o bar.o main.o -o main` 만이 실행됩니다.

### 변수

재미있게도 *Makefile* 내에서 변수를 정의할 수 있습니다.

```info-verb
CC = g++
```

위 경우 `CC` 라는 변수를 정의하였는데, 이제 *Makefile* 내에서 `CC` 를 사용하게 된다면 해당 변수의 문자열인 `g++` 로 치환됩니다. 이 때 변수를 사용하기 위해서는 `$(CC)` 와 같이 `$()` 안에 사용하고자 하는 변수의 이름을 지정하면 됩니다. 예를 들어서

```info-verb
CC = g++

foo.o : foo.h foo.cc
	$(CC) -c foo.cc
```

는 사실


```info-verb
CC = g++

foo.o : foo.h foo.cc
	g++ -c foo.cc
```

과 정확히 같은 명령이 됩니다.

참고로 정의하지 않는 변수를 참조하게 된다면 그냥 빈 문자열로 치환됩니다.

#### 변수를 정의하는 두 가지 방법

```lec-warning
이 부분은 TMI 이므로 바쁘신 분들은 그냥 넘어가셔도 됩니다.
```

*Makefile* 상에서 변수를 정의하는 방법으로 = 를 사용해서 정의하는 방법과 `:=` 를 사용해서 정의하는 방법이 있습니다. 이 둘은 살짝 다릅니다.

= 를 사용해서 변수를 정의하였을 때, 정의에 다른 변수가 포함되어 있다면 해당 변수가 정의되기 될 때 까지 변수의 값이 정해지지 않습니다. 예를 들어서

```info-verb
B = $(A)
C = $(B)
A = a
```

`C` 의 경우 `B` 의 값을 참조하고 있고, `B` 의 경우 `A` 의 값을 참조하고 있습니다. 하지만 `B = $(A)` 를 실행한 시점에서 `A` 가 정의되지 않았으므로 `B` 는 그냥 빈 문자열이 되어야 하지만 `=`로 정의하였기 때문에 `A` 가 실제로 정의될 때 까지 `B` 와 `C` 가 결정되지 않습니다. 결국 마지막에 `A = a` 를 통해 `A` 가 `a` 로 대응되어야, `C` 가 `a` 로 결정됩니다.

```info-verb
B := $(A)
A = a
```

반면에 `:=` 로 변수를 정의할 경우, *해당 시점에의 변수의 값만 확인* 합니다. 따라서 위 경우 `B` 는 그냥 빈 문자열이 되겠지요.

대부분의 상황에서는 `=` 나 `:=` 중 아무거나 사용해도 상관 없습니다. 하지만

* 만일 변수들의 정의 순서에 크게 구애받고 싶지 않다면 `=` 를 사용하는 것이 편합니다.
* `A = $(A) b` 와 같이 자기 자신을 수정하고 싶다면 `:=` 를 사용해야지 무한 루프를 피할 수 있습니다.

만 명심하면 됩니다.

그렇다면 이들 변수를 사용해서 *Makefile* 을 조금 더 깔끔하게 바꾸어보겠습니다.

```info-verb
CC = g++
CXXFLAGS = -Wall -O2
OBJS = foo.o bar.o main.o

foo.o : foo.h foo.cc
	$(CC) $(CXXFLAGS) -c foo.cc

bar.o : bar.h bar.cc
	$(CC) $(CXXFLAGS) -c bar.cc

main.o : main.cc foo.h bar.h
	$(CC) $(CXXFLAGS) -c main.cc

main : $(OBJS)
	$(CC) $(CXXFLAGS) $(OBJS) -o main
```

\serif{make} 를 실행해보면

```info-term
$ make main
g++ -Wall -O2 -c foo.cc
g++ -Wall -O2 -c bar.cc
g++ -Wall -O2 -c main.cc
g++ -Wall -O2 foo.o bar.o main.o -o main
```

와 같이 잘 나옵니다.

`CC` 와 `CXXFLAGS` 는 *Makefile* 에서 자주 사용되는 변수로 보통 `CC` 에는 사용하는 컴파일러 이름을, `CXXFLAGS` 에는 컴파일러 옵션을 주는 것이 일반적 입니다. 참고로 이는 C++ 의 경우 이고, C 의 경우 CFLAGS 에 옵션을 줍니다.

우리의 경우 `g++` 컴파일러를 사용하며, 옵션으로는 `Wall` (모든 컴파일 경고를 표시) 과 `O2` (최적화 레벨 2) 를 주었습니다.\sidenote{사실 -Wall 은 이름과는 다르게 모든 경고를 표시하지 않습니다.}

### PHONY

*Makefile* 에 흔히 추가하는 기능으로 빌드 관련된 파일들 (.o 파일들)을 모두 제거하는 명령을 넣습니다.

```info-verb
clean:
	rm -f $(OBJS) main
```

실제로 `make clean` 을 실행해보면 생성된 모든 목적 파일과 `main` 을 지워버림을 알 수 있습니다.

그런데, 만약에 실제로 `clean` 이라는 파일이 디렉토리에 생성된다면 어떨까요? 우리가 `make clean` 을 하게 되면, \serif{make} 는 *clean 의 필요 파일들이 없는데, clean 파일이 있으니까 clean 파일은 항상 최신이네? recipe 를 실행 안해도 되겠네!* 하면서 그냥 `make clean` 명령을 무시해버리게 됩니다.

```info-term
$ ls clean
clean
$ make clean
make: 'clean' is up to date.
```

실제로 디렉토리에 `clean` 이라는 파일을 만들어놓고 실행해보면 위와 같이 이미 `clean` 은 최신이라며 `recipe` 실행을 거부합니다.

이와 같은 상황을 막기 위해서는 `clean` 을 `PHONY` 라고 등록하면 됩니다.\sidenote{Phony 는 '가짜의, 허위의' 이라는 뜻입니다.} 아래와 같이 말이지요.

```info-verb
.PHONY: clean
clean:
	rm -f $(OBJS) main
```

이제 `make clean` 을 하게 되면 `clean` 파일의 유무와 상관 없이 언제나 해당 타겟의 명령을 실행하게 됩니다.

### 패턴 사용하기

우리의 경우 파일이 3 개 밖에 없어서 다행이였지만 실제 프로젝트에는 수십~ 수백 개의 파일들을 다루게 될 것입니다. 그런데, 각각의 파일들에 대해서 모두 빌드 방식을 명시해준다면 *Makefile* 의 크기가 엄청 커지겠지요.

다행이도 *Makefile* 에서는 패턴 매칭을 통해서 특정 조건에 부합하는 파일들에 대해서 간단하게 `recipe` 를 작성할 수 있게 해줍니다.

```info-verb
foo.o : foo.h foo.cc
	$(CC) $(CXXFLAGS) -c foo.cc

bar.o : bar.h bar.cc
	$(CC) $(CXXFLAGS) -c bar.cc
```

일단 먼저 비슷하게 생긴 위 두 명령들을 어떻게 하면 하나로 간단하게 나타낼 수 있는지 보겠습니다.

```info-verb
%.o: %.cc %.h
	$(CC) $(CXXFLAGS) -c $<
```

먼저 `%.o` 는 와일드카드로 따지면 마치 `*.o` 와 같다고 볼 수 있습니다. 즉, `.o` 로 끝나는 파일 이름들이 타겟이 될 수 있겠지요. 예를 들어서 `foo.o` 가 타겟이라면 % 에는 `foo` 가 들어갈 것이고 `bar.o` 의 경우 % 에는 `bar` 가 들어갈 것입니다.

따라서 예를 들어 `foo.o` 가 타겟일 경우

```info-verb
foo.o: foo.cc foo.h
	$(CC) $(CXXFLAGS) -c $<
```

가 됩니다. 참고로 패턴은 타겟과 `prerequisite` 부분에만 사용할 수 있습니다. `recipe` 부분에서는 패턴을 사용할 수 없습니다. 따라서 컴파일러에 `foo.cc` 를 전달하기 위해서는 *Makefile* 의 자동 변수를 사용해야 합니다.

`$<` 의 경우 `prerequisite` 에서 첫 번째 파일의 이름에 대응되어 있는 변수 입니다. 위 경우 `foo.cc` 가 되겠지요. 따라서 위 명령어는 결과적으로

```info-verb
foo.o: foo.cc foo.h
	$(CC) $(CXXFLAGS) -c foo.cc
```

가 되어서 이전의 명령어와 동일하게 만들어냅니다.

*Makefile* 에서 제공하는 자동 변수로는 그 외에도 아래 그림과 같이 `$@`, `$<`, `$^` 등등이 있습니다.

![](/img/cpp/a.1.2.png)

* `$@` : 타겟 이름에 대응됩니다.
* `$<` : 의존 파일 목록에 첫 번째 파일에 대응됩니다.
* `$^` : 의존 파일 목록 전체에 대응됩니다.
* `$?` : 타겟 보다 최신인 의존 파일들에 대응됩니다.
* `$+` : `$^` 와 비슷하지만, 중복된 파일 이름들 까지 모두 포함합니다.

하지만 애석하게도 위 패턴으로는

```info-verb
main.o : main.cc foo.h bar.h
	$(CC) $(CXXFLAGS) -c main.cc
```

를 표현하기에는 부족합니다. 왜냐하면 의존 파일 목록에 `main.h` 가 없고 `foo.h` 와 `bar.h` 가 있기 때문이죠. 사실 곰곰히 생각해보면 이 의존파일 목록에는 는 해당 소스 파일이 어떠한 헤더파일을 포함하냐에 결정되어 있습니다. `main.cc` 가 `foo.h` 와 `bar.h` 를 `include` 하고 있기 때문에 `main.o` 의 `prerequisite` 로 `main.cc` 외에도 `foo.h` 와 `bar.h` 가 들어가 있는 것입니다.

물론 매번 이렇게 일일히 추가할 수 있겠지만, 소스 파일에 헤더 파일을 추가할 때 마다 *Makefile* 을 바꿀 수는 없는 노릇이니까요. 하지만 다행이도 컴파일러의 도움을 받아서 의존파일 목록 부분을 작성할 수 있습니다.

### 자동으로 prerequisite 만들기

컴파일 시에 `-MD` 옵션을 추가해서 컴파일 해봅시다.

```info-term
$ g++ -c -MD main.cc
```

그렇다면 `main.d` 라는 파일을 생성합니다. 파일 내용을 살펴보면;

```info-term
$ cat main.d  
main.o: main.cc /usr/include/stdc-predef.h foo.h bar.h
```

놀랍게도 마치 *Makefile* 의 `target: prerequisite` 인것 같은 부분을 생성하였습니다. 그렇습니다. 컴파일 시에 `-MD` 옵션을 추가해주면, 목적 파일 말고도 컴파일 한 소스파일을 타겟으로 하는 의존파일 목록을 담은 파일을 생성해줍니다.

참고로 `main.cc, foo.h, bar.h` 까지는 이해가 가는데 왜 생뚱맞은 `/usr/include/stdc-predef.h` 이 들어가 있냐고 물을 수 있는데, 이 파일은 컴파일러가 컴파일 할 때 암묵적으로 참조하는 헤더 파일이라고 보시면 됩니다. 아무튼 이 때문에 컴파일러가 생성한 의존 파일 목록에는 포함되었습니다.

문제는 이렇게 생성된 `main.d` 를 어떻게 우리의 *Makefile* 에 포함할 수 있냐 입니다. 이는 생각보다 간단합니다.

```info-verb
CC = g++
CXXFLAGS = -Wall -O2
OBJS = foo.o bar.o main.o

%.o: %.cc %.h
	$(CC) $(CXXFLAGS) -c $<

main : $(OBJS)
	$(CC) $(CXXFLAGS) $(OBJS) -o main

.PHONY: clean
clean:
	rm -f $(OBJS) main

include main.d
```

위 `include main.d` 는 `main.d` 라는 파일의 내용을 *Makefile* 에 포함하라는 의미 입니다.

그렇다면 아싸리

```info-verb
%.o: %.cc %.h
	$(CC) $(CXXFLAGS) -c $<
```

부분을 아예 컴파일러가 생성한 `.d` 파일로 대체할 수는 없을까요? 물론 있습니다!

```info-verb
CC = g++
CXXFLAGS = -Wall -O2
OBJS = foo.o bar.o main.o

%.o: %.cc
	$(CC) $(CXXFLAGS) -c $<

main : $(OBJS)
	$(CC) $(CXXFLAGS) $(OBJS) -o main

.PHONY: clean
clean:
	rm -f $(OBJS) main

-include $(OBJS:.o=.d)
```

`$(OBJS:.o=.d)` 부분은 `OBJS` 에서 `.o` 로 끝나는 부분을 `.d` 로 모두 대체하라는 의미 입니다. 즉, 해당 부분은 `-include foo.d bar.d main.d` 가 되겠죠. 참고로 `foo.d` 나 `bar.d` 가 `include` 될 때 이미 있는 `%.o: %.cc` 는 어떻게 되냐고 물을 수 있는데 **같은 타겟에 대해서 여러 의존 파일 목록들이 정해져 있다면 이는 make 에 의해 모두 하나로 합쳐집니다.** 따라서 크게 걱정하실 필요는 없습니다.

덧붙여 `include` 에서 `-include` 로 바꾸었는데, `-include` 의 경우 포함하고자 하는 파일이 존재하지 않아도 \serif{make} 메세지를 출력하지 않습니다.

맨 처음에 \serif{make} 를 할 때에는 `.d` 파일들이 제대로 생성되지 않은 상태이기 때문에 `include` 가 아무런 `.d` 파일들을 포함하지 않습니다. 물론 크게 문제 없는 것이 어차피 `.o` 파일들도 \serif{make} 가 `%.o: %.cc` 부분의 명령어들을 실행하면서 컴파일을 하기 때문에 다음에 \serif{make} 를 하게 될 때에는 제대로 `.d` 파일들을 로드할 수 있겠죠.

### 최종 정리

아래와 같이 간단한 프로젝트 구조를 생각해봅시다.

```info-term
$ tree
.
├── Makefile
├── obj
└── src
    ├── bar.cc
    ├── bar.h
    ├── foo.cc
    ├── foo.h
    └── main.cc
```

모든 소스 파일은 `src` 에 들어가고 빌드 파일들은 `obj` 에 들어갑니다. 종종 헤더 파일들을 따로 `include` 에 빼는 경우가 있는데 저는 굳이 라이브러리를 만드는 경우가 아니라면 별로 선호하지 않습니다. (굳이 다른 디렉토리에 넣을 이유가 뭔지 모르겠습니다.)

아무튼 이와 같은 구조에서 항상 사용할 수 있는 **만능** *Makefile* 은 아래와 같습니다.

```lec-warning
복사한 후에 `$(CC)` 와 `rm` 앞에 스페이스 두 개를 꼭 TAB 으로 치환해주세요! 안 그러면 \serif{make} 가 읽지 못합니다.
```

```info-verb
CC = g++

# C++ 컴파일러 옵션
CXXFLAGS = -Wall -O2

# 링커 옵션
LDFLAGS =

# 소스 파일 디렉토리
SRC_DIR = ./src

# 오브젝트 파일 디렉토리
OBJ_DIR = ./obj

# 생성하고자 하는 실행 파일 이름
TARGET = main

# Make 할 소스 파일들
# wildcard 로 SRC_DIR 에서 *.cc 로 된 파일들 목록을 뽑아낸 뒤에
# notdir 로 파일 이름만 뽑아낸다.
# (e.g SRCS 는 foo.cc bar.cc main.cc 가 된다.)
SRCS = $(notdir $(wildcard $(SRC_DIR)/*.cc))

OBJS = $(SRCS:.cc=.o)

# OBJS 안의 object 파일들 이름 앞에 $(OBJ_DIR)/ 을 붙인다.
OBJECTS = $(patsubst %.o,$(OBJ_DIR)/%.o,$(OBJS))
DEPS = $(OBJECTS:.o=.d)

all: main

$(OBJ_DIR)/%.o : $(SRC_DIR)/%.cc
	$(CC) $(CXXFLAGS) -c $< -o $@ -MD $(LDFLAGS)

$(TARGET) : $(OBJECTS)
	$(CC) $(CXXFLAGS) $(OBJECTS) -o $(TARGET) $(LDFLAGS)

.PHONY: clean all
clean:
	rm -f $(OBJECTS) $(DEPS) $(TARGET)

-include $(DEPS)
```

추가된 부분만 간단히 부연 설명을 하자면

```info-verb
# Make 할 소스 파일들
# wildcard 로 SRC_DIR 에서 *.cc 로 된 파일들 목록을 뽑아낸 뒤에
# notdir 로 파일 이름만 뽑아낸다.
# (e.g SRCS 는 foo.cc bar.cc main.cc 가 된다.)
SRCS = $(notdir $(wildcard $(SRC_DIR)/*.cc))
```

먼저 `SRC_DIR` 안에 있는 모든 파일들을 `SRCS` 로 읽어들이려 하고 있습니다. `wildcard` 는 함수로 해당 조건에 맞는 파일들을 뽑아내게 되는데, 예를 들어서 `foo.cc, bar.cc, main.cc` 가 있을 경우 `$(wildcard $(SRC_DIR)/*.cc)` 의 실행 결과는 `./src/foo.cc ./src/bar.cc ./src/main.cc` 가 될 것입니다.

여기서 우리는 `foo.cc bar.cc main.cc` 로 깔끔하게 경로를 제외한 파일 이름만 뽑아내기 위해 `notdir` 함수를 사용합니다. `notdir` 은 앞에 오는 경로를 날려버리고 파일 이름만 깔끔하게 추출해줍니다.

```info-verb
OBJS = $(SRCS:.cc=.o)
```

따라서 이 부분에서 `OBJS` 는 `foo.o bar.o main.o` 가 될 것입니다.

이제 이 `OBJS` 를 바탕으로 실제 `.o` 파일들의 경로를 만들어내고 싶습니다. 이를 위해서는 이들 파일 이름 앞에 `$(OBJ_DIR)/` 을 붙여줘야 겠지요. 이를 위해선 `patsubst` 함수를 사용하면 됩니다.

```info-verb
# OBJS 안의 object 파일들 이름 앞에 $(OBJ_DIR)/ 을 붙인다.
OBJECTS = $(patsubst %.o,$(OBJ_DIR)/%.o,$(OBJS))
```

`patsubst` 함수는 `$(patsubst 패턴,치환 후 형태,변수)` 의 같은 꼴로 사용합니다.

따라서 위 경우 **`$(OBJS)` 안에 있는 모든 `%.o` 패턴을 `$(OBJ_DIR)/%.o` 로 치환해라** 라는 의미가 될 것입니다. 아무튼 덕분에 `OBJECTS` 에는 이제 .`/obj/foo.o ./obj/bar.o ./obj/main.o` 가 들어가게 됩니다.

그 뒤에 내용은 앞의 글을 잘 따라 오신 분들이라면 잘 이해 하실 수 있으리라 믿습니다.

#### 헤더 파일들을 따로 뽑는 경우

만약에 헤더 파일들만 따로 뽑는다면 아래와 같은 파일 구조를 가지겠죠.

```info-term
$ tree
.
├── include
│   ├── bar.h
│   └── foo.h
├── Makefile
├── obj
└── src
    ├── bar.cc
    ├── foo.cc
    └── main.cc
```

이 경우 *Makefile* 을 아래와 같이 간단히 수정하면 됩니다.

```info-verb
CC = g++

# C++ 컴파일러 옵션
CXXFLAGS = -Wall -O2

# 링커 옵션
LDFLAGS =

# 헤더파일 경로
INCLUDE = -Iinclude/

# 소스 파일 디렉토리
SRC_DIR = ./src

# 오브젝트 파일 디렉토리
OBJ_DIR = ./obj

# 생성하고자 하는 실행 파일 이름
TARGET = main

# Make 할 소스 파일들
# wildcard 로 SRC_DIR 에서 *.cc 로 된 파일들 목록을 뽑아낸 뒤에
# notdir 로 파일 이름만 뽑아낸다.
# (e.g SRCS 는 foo.cc bar.cc main.cc 가 된다.)
SRCS = $(notdir $(wildcard $(SRC_DIR)/*.cc))

OBJS = $(SRCS:.cc=.o)
DEPS = $(SRCS:.cc=.d)

# OBJS 안의 object 파일들 이름 앞에 $(OBJ_DIR)/ 을 붙인다.
OBJECTS = $(patsubst %.o,$(OBJ_DIR)/%.o,$(OBJS))
DEPS = $(OBJECTS:.o=.d)

all: main

$(OBJ_DIR)/%.o : $(SRC_DIR)/%.cc
	$(CC) $(CXXFLAGS) $(INCLUDE) -c $< -o $@ -MD $(LDFLAGS)

$(TARGET) : $(OBJECTS)
	$(CC) $(CXXFLAGS) $(OBJECTS) -o $(TARGET) $(LDFLAGS)

.PHONY: clean all
clean:
	rm -f $(OBJECTS) $(DEPS) $(TARGET)

-include $(DEPS)
```

사실 기존과 별 차이 없고 그냥 컴파일러 옵션에 `-Iinclude/` 를 추가해주면 됩니다. 여기서 `include` 는 헤더파일 경로 입니다.

### 멀티 코어를 활용해서 Make 속도를 올리자

한 가지 팁으로 그냥 \serif{make} 를 실행하게 되면 1 개의 쓰레드만 실행되어서 속도가 꽤나 느립니다. 특히 GCC 나 커널을 컴파일 할 경우 한 두 시간은 그냥 걸리지요. 만일 여러분의 컴퓨터가 멀티 코어 CPU 를 사용한다면 (**아마 대부분 그럴 것이라 생각합니다**) \serif{make} 를 여러 개의 쓰레드에서 돌릴 수 있습니다. 이를 위해서는 인자로 `-j` 뒤에 몇 개의 쓰레드를 사용할 지 숫자를 적어서 전달하면 됩니다.

예를 들어서

```info-term
$ make -j8
```

을 하면 \serif{make} 가 8 개의 쓰레드에 나뉘어서 실행됩니다. 아마 \serif{make} 속도가 월등하게 향상되는 것을 보실 수 있을 것입니다. 통상적으로 **코어 개수 + 1** 만큼의 쓰레드를 생성해서 돌리는 것이 가장 속도가 빠릅니다.

만약에 내 컴퓨터의 코어 개수를 모른다면 리눅스 터미널의 경우

```info-term
$ make -j$(nproc)
```

으로 하면 `$(nproc)` 이 알아서 내 컴퓨터의 현재 코어 개수로 치환됩니다.

##@ chewing-cpp-end