Make 이해하기

August 1, 2026 · 방문자

make는 내가 다뤄본 거의 모든 임베디드 및 시스템 프로젝트에 등장한다 — Yocto 레시피, 커널 빌드, U-Boot, 소규모 펌웨어 저장소까지. 또한 사람들이 그 밑바탕의 모델을 제대로 파악하지 않은 채 이전 프로젝트의 Makefile을 그대로 복사해 여러 해 동안 모방으로 사용해 온 도구 중 하나이기도 하다. 여기서 그 모델을 있는 그대로 풀어본다.

Make란 무엇인가?

Make는 빌드 자동화 도구다: 파일(Makefile)을 읽어 일련의 타깃(target), 각 타깃이 의존하는 전제조건(prerequisite), 그리고 그 전제조건들로부터 타깃을 만들어내는 데 필요한 **레시피(recipe, 셸 명령어)**를 기술한다. 이 기술을 바탕으로 make는 어떤 타깃이 최신이 아닌지 파악하고, 그것을 최신 상태로 만드는 데 필요한 레시피만 실행한다 — 그 이상은 하지 않는다.

main: main.o utils.o
	gcc -o main main.o utils.o

main.o: main.c
	gcc -c main.c

make를 실행하면, main.cmain.o보다 최신이거나(또는 main.o가 아직 존재하지 않을 때) main.o만 빌드한다. 그리고 main.outils.o가 변경되었을 때만 main을 링크한다. 불필요한 재빌드는 전혀 일어나지 않는다.

누구를 위한 것인가?

Make는 1976년 4월 벨 연구소(Bell Labs)의 Stuart Feldman이 만들었다. 그의 회고에 따르면, 계기는 동료 — yacc의 저자인 Steve Johnson — 가 사실은 멀쩡한 프로그램을 디버깅하느라 하루 아침을 허비한 사건이었다. 바이너리가 소스 변경 이후 다시 링크되지 않았을 뿐이었다. Feldman은 이 공로로 2003년 ACM 소프트웨어 시스템 상(ACM Software System Award)을 받았다. 그 오후에 탄생한 이 도구 — 의존성을 한 번 추적해 두면 기계가 알아서 무엇을 다시 해야 할지 파악하게 한다는 아이디어 — 는 소프트웨어 도구 분야에서 가장 널리 모방된 아이디어 중 하나가 되었다. 오늘날 거의 모든 사람이 사용하는 버전인 GNU Make는 GNU 프로젝트가 만든 무료 재구현이다 (1) (5) (6).

실무에서는, 작은 조각들로부터 사소하지 않은 결과물 — 컴파일된 바이너리, 문서, 생성된 파일들의 집합 — 을 조립하는 사람이라면 누구에게나 유용하다. 매번 처음부터 전부 다시 빌드하는 것이 낭비이거나 느릴 때가 그렇다. 여기에는 C/C++ 프로젝트, 커널 및 부트로더 빌드, LaTeX 문서, 그리고 코드 컴파일과는 전혀 무관한 수많은 임시 자동화 작업까지 포함된다.

언제 사용해야 하는가?

다음 세 가지가 동시에 성립할 때 Make는 제 역할을 한다.

상호 의존성이 없는 파일 몇 개라면, 셸 스크립트가 더 단순하고 정직한 경우가 많다. "이게 정말로 다시 빌드될 필요가 있었나?"라는 질문을 수동이 아니라 자동으로 물을 가치가 생기는 시점부터 Make는 제값을 하기 시작한다.

툴체인에서 어디에 위치하는가?

Make는 대개 컴파일러보다 한 계층 위, 사람이 직접 실행하는 것보다 한 계층 아래에 위치한다. 전형적인 C 프로젝트에서는: 소스 파일 → (Make가 구동하는 컴파일러) → 오브젝트 파일 → (Make가 구동하는 링커) → 최종 바이너리 순이다. Make 자체는 아무것도 컴파일하거나 링크하지 않는다 — 각 레시피 안의 셸 명령어로 그 일을 하는 도구들을 호출할 뿐이다.

Make는 또한 더 상위 수준의 빌드 설정 도구들 아래에 위치하는 경향이 있다. 예를 들어 CMake, Autotools, Yocto의 BitBake는 Make를 대체한다기보다는 실제 실행 백엔드로서 Makefile(또는 그에 준하는 것)을 생성해 구동한다. 그래서 Makefile을 직접 보여주지 않는 프로젝트조차도 파이프라인 어딘가에서 Make를 실행하고 있는 경우가 많다.

왜 이런 방식으로 동작하는가?

핵심 설계 선택 — 파일 수정 시각을 비교해 무엇이 오래되었는지 판단하는 방식 — 이야말로 Make가 큰 트리에서도 빠른 이유다: 타임스탬프를 확인하는 비용은 저렴하므로, 한 줄만 바꾼 뒤 거대한 프로젝트를 재빌드해도 거의 모든 것을 건너뛰면서도 여전히 정확할 수 있다 — 단, Make에게 주어진 의존성 그래프가 정확하다는 전제 하에서다.

바로 이 마지막 전제가 사람들이 마주치는 거의 모든 "Make가 고장 났다"는 불만의 근원이며, 곱씹어볼 가치가 있다: Make는 자신에게 주어진 그래프만큼만 정확하다. .c 파일이 어떤 헤더를 #include했는데 그 헤더가 해당 .o 파일의 전제조건으로 나열되어 있지 않다면, 그 헤더를 수정해도 재빌드는 일어나지 않는다 — Make가 혼란스러워서가 아니라, 그런 의존성이 존재한다고 아무도 알려준 적이 없기 때문이다. 그래서 실제 현업의 Makefile들은 손으로 작성한 헤더 목록보다는 컴파일러가 생성하는 의존성 단계(GCC/Clang의 -MMD -MP)와 짝을 이루는 경우가 거의 대부분이다 — 컴파일러는 이미 실제 include 그래프를 알고 있으므로, 그것이야말로 유일하게 신뢰할 수 있는 출처다 (4).

실제로 어떻게 동작하는가?

타깃, 전제조건, 레시피. 기본 단위는 다음과 같다.

target: prerequisite1 prerequisite2
	recipe line 1
	recipe line 2

레시피 줄은 스페이스가 아니라 으로 들여쓰기해야 한다 — 오늘날까지도 사람들의 발목을 잡는 유명한 역사적 결함이다.

**자동 변수(automatic variables)**는 반복을 줄여준다.

%.o: %.c
	gcc -c $< -o $@

$@는 타깃, $<는 첫 번째 전제조건이다 — 이 패턴 규칙 하나만으로도 파일마다 한 줄씩 쓰지 않고도 대응하는 .c 파일로부터 모든 .o를 빌드할 수 있다.

.PHONY 타깃은 실제 파일에 대응하지 않는 이름들 — clean, all, test — 을 표시해서, 같은 이름의 파일이 우연히 존재하더라도 Make가 혼동하지 않고 타임스탬프를 확인하는 대신 항상 레시피를 실행하도록 만든다 (2).

.PHONY: clean
clean:
	rm -f *.o main

**변수(variables)**는 플래그를 여기저기 반복하지 않게 해 주고 프로젝트 간 재사용을 쉽게 만든다.

CC = gcc
CFLAGS = -Wall -O2

main.o: main.c
	$(CC) $(CFLAGS) -c main.c

**자동 의존성 생성(automatic dependency generation)**은 앞서 설명한 간극을 메운다.

CFLAGS = -Wall -O2 -MMD -MP
-include $(wildcard *.d)

%.o: %.c
	$(CC) $(CFLAGS) -c $<

-MMD는 컴파일러가 오브젝트마다 .d 파일을 생성하도록 해서, 해당 컴파일 과정에서 실제로 포함된 모든 헤더를 나열하게 한다. -include는 그 파일들을 다시 Makefile로 끌어들여서, 누군가 수작업으로 관리하지 않아도 헤더가 바뀔 때마다 의존성 그래프가 정확하게 유지되도록 한다 (3).

마치며

Make의 특이한 점처럼 보이는 것들 대부분 — 타임스탬프 비교, 탭에 대한 고집, 누락된 의존성이 요란하게가 아니라 조용히 실패하는 방식 — 은 1976년에 내려진 하나의 설계 결정에서 비롯된다: 모델은 단순하게, 검사는 저렴하게 유지하고, Makefile이 현실을 정확하게 기술한다고 신뢰하는 것. 이 관점으로 보면, "Make가 이상하게 군다"고 느껴지는 순간의 대부분은 사실 "내가 준 그래프가 불완전했다"는 문제로 밝혀진다 — 이는 훨씬 고치기 쉬운 문제다.

관련 글

참고 문헌

  1. Feldman, S. I. (1979). Make — A Program for Maintaining Computer Programs. Software: Practice and Experience, 9(4), 255–265. — Feldman이 직접 쓴, Make를 소개하고 원래의 동기를 설명하는 논문.
  2. GNU Make Manual — GNU Make의 문법과 의미론에 대한 완전하고 권위 있는 레퍼런스.
  3. GNU Make Manual: Automatic Prerequisites-MMD/-include 패턴을 관리자들이 직접 설명한 문서.
  4. GCC: Preprocessor Options (-M family)-MMD-MP가 내부적으로 무엇을 하는지.
  5. Make (software) — Wikipedia — Make 구현체들(BSD Make, GNU Make, NMAKE 등)의 일반적인 역사와 계보.
  6. Stuart Feldman — Wikipedia — Feldman의 벨 연구소 시절 연구와 2003년 ACM 소프트웨어 시스템 상에 대한 배경.

댓글