Make 패턴 규칙과 암시적 규칙 — % 매칭 동작
#왜 패턴 규칙이 필요한가
Ch 3에서 변수로 중복을 한 번 줄였습니다. 하지만 오브젝트 파일마다 규칙을 한 번씩 적어야 한다는 한계는 그대로입니다.
main.o: main.c hello.h gcc -c main.c -o main.o
hello.o: hello.c hello.h gcc -c hello.c -o hello.o
utils.o: utils.c utils.h gcc -c utils.c -o utils.o여기에는 사람이 한눈에 알아채는 패턴이 있습니다. “.c 파일 하나로 같은 이름의 .o 파일 하나를 만든다.” 사람은 이 패턴을 본 순간 100개 파일이라도 머릿속에서 즉시 일반화하지만, Makefile에 이걸 적어 두려면 100줄을 써야 합니다.
패턴 규칙(pattern rule)은 이 일반화를 Makefile의 언어로 표현하는 도구입니다.
%.o: %.c gcc -c $< -o $@%는 임의의 문자열에 매칭되는 와일드카드입니다. Make는 이 와일드카드 매칭을 다음과 같이 풉니다.
- Make가
main.o를 만들 필요가 생김 - 패턴 규칙
%.o: %.c를 시도 - 타겟의
%="main"(stem) - 의존성의
%도 같은"main"으로 치환 →main.c - 레시피는
$<,$@같은 자동 변수를 사용 - 최종 실행:
gcc -c main.c -o main.o
여기서 매칭된 main 부분을 stem이라고 부릅니다. stem은 자동 변수 $*로 가져올 수 있는데, 잠시 후 보겠습니다.
이 한 규칙이 디렉터리 안의 모든 .c → .o 쌍을 처리합니다. .c 파일 100개여도, 1개여도 똑같이 동작합니다.
#패턴 규칙 문법
%.타겟확장자: %.의존성확장자 레시피% 양쪽 모두에 같은 값이 들어간다는 것이 핵심입니다. 즉 %.o: %.c는 “이름 X에 대해 X.c → X.o”라는 함수 모양 정의에 가깝습니다.
#기본 예시 — 흔히 보는 변환들
# C 소스 → 오브젝트%.o: %.c $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
# C++ 소스 → 오브젝트%.o: %.cpp $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@
# 어셈블리 → 오브젝트%.o: %.s $(AS) $(ASFLAGS) -o $@ $<
# Protocol Buffers → C++ 코드 (보조 도구 호출)%.pb.cc %.pb.h: %.proto $(PROTOC) --cpp_out=. $<마지막 예가 흥미롭습니다. 한 입력에서 두 출력이 동시에 생기는 경우입니다. 보통 Make는 이걸 “두 별개 규칙”으로 펴는데, 그러면 protoc이 두 번 호출됩니다. 4.3부터는 grouped target 문법 %.pb.cc %.pb.h &: %.proto를 쓰면 “한 호출이 정말 둘을 함께 만든다”는 의미를 정확히 알려 줄 수 있습니다. 4.3 이전 Make에서는 한 출력만 의존성으로 적고, 다른 출력은 “사이드 이펙트”로 간주하는 회피책을 씁니다.
#디렉터리를 포함한 패턴
빌드 산물을 별도 디렉터리(build/)에 모으는 게 표준 관행입니다. 패턴에 경로를 그대로 적으면 됩니다.
build/%.o: src/%.c @mkdir -p $(@D) $(CC) $(CFLAGS) -c $< -o $@build/main.o를 만들 차례가 되면 Make는 src/main.c를 찾아 컴파일합니다. $(@D)는 Ch 3에서 본 자동 변수의 디렉터리 부분입니다.
#패턴 안의 %는 하나만
%.o: %.c # OK: 타겟·의존성 한 자리씩%-%.o: %-%.c # 안 됨: %가 두 번Make 매뉴얼은 한 패턴에 %는 한 번이라고 못 박습니다. 두 번 이상은 결정적으로 해석할 수 없습니다(어떤 글자를 어디로 매칭할지 모호하기 때문). 다중 매칭이 필요하면 보통 정적 패턴 규칙이나 함수로 풀어냅니다.
#자동 변수 — stem($*) 포함
Ch 3에서 본 자동 변수에 패턴 규칙에서 특히 유용한 한 가지가 더해집니다 — $* (stem).
| 변수 | 의미 | build/main.o: src/main.c 예 |
|---|---|---|
$@ | 타겟 | build/main.o |
$< | 첫 의존성 | src/main.c |
$^ | 모든 의존성 | src/main.c |
$* | stem (% 매칭 부분) | main |
$(@D) | 타겟 디렉터리 | build |
$(@F) | 타겟 파일명 | main.o |
#$*의 함정 — 중첩 디렉터리
$*은 단순히 “확장자를 뗀 파일 이름”이 아닙니다. % 와일드카드가 실제로 매칭한 문자열입니다. 디렉터리가 중첩되면 차이가 드러납니다.
build/%.o: src/%.c @echo "stem = $*" gcc -c $< -o $@build/sub/main.o를 빌드해 봅시다.
- 패턴
build/%.o가build/sub/main.o에 매칭되면,%는sub/main전체에 매칭됩니다. - 따라서
$* = sub/main,$@ = build/sub/main.o,$< = src/sub/main.c.
$*을 그냥 “파일 이름”이라고 가정하고 다른 곳에 쓰면 sub/main처럼 슬래시가 끼어 들어와 사고가 납니다. 파일 이름만 필요하면 $(notdir $*), 디렉터리만 필요하면 $(dir $*)을 씁니다.
build/%.o: src/%.c @echo "Compiling $(notdir $*).c" gcc -c $< -o $@#암시적 규칙 — Make가 미리 알고 있는 변환
Make는 수십 가지 기본 패턴 규칙을 내장하고 있습니다. 이걸 암시적 규칙(implicit rules)이라 부릅니다. 사용자가 명시적으로 적지 않아도, Make는 .c → .o, .cpp → .o, .o → 실행 파일 같은 변환을 자동으로 인식합니다.
#주요 암시적 규칙
| 결과 | 입력 | Make가 내부적으로 가진 명령 |
|---|---|---|
%.o | %.c | $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ |
%.o | %.cpp | $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ |
%.o | %.s | $(AS) $(ASFLAGS) -o $@ $< |
% (실행 파일) | %.o | $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@ |
이 표 덕분에 극단적으로 짧은 Makefile도 동작합니다.
CC := gccCFLAGS := -Wall -g
hello: main.o hello.o utils.o이게 전부입니다. make 한 번 치면:
$ makecc -Wall -g -c -o main.o main.ccc -Wall -g -c -o hello.o hello.ccc -Wall -g -c -o utils.o utils.ccc main.o hello.o utils.o -o hello세 오브젝트 모두 내장 %.o: %.c 규칙으로 빌드되고, 마지막 링크는 % : %.o 규칙이 처리합니다.
#암시적 규칙이 보고 있는 변수
암시적 규칙은 일반 패턴 규칙처럼 자동 변수를 사용합니다. 위 표의 명령에는 $(CC), $(CFLAGS) 같은 표준 변수가 그대로 등장하는데, 이 변수를 정의하지 않으면 빈 값이 들어갑니다.
빈 $(CFLAGS)로 빌드된 결과는 경고도 디버그 정보도 최적화도 없는 상태가 됩니다. 그래서 암시적 규칙에 의존하려면 변수만큼은 반드시 명시적으로 설정해야 합니다.
# 권장CC := gccCFLAGS := -Wall -Wextra -g -O2CPPFLAGS := -IincludeLDLIBS := -lm
hello: main.o hello.o utils.o#암시적 규칙 데이터베이스 보기
Make가 내부적으로 가진 규칙 전부를 보고 싶으면 -p(print database) 옵션을 씁니다.
make -p -f /dev/null 2>/dev/null | less-f /dev/null은 현재 디렉터리의 Makefile 무시하고 내장 규칙만 보고 싶을 때 씁니다. 출력은 길지만, '%.o'로 검색하면 모든 컴파일 규칙이 한자리에 모여 있습니다.
#암시적 규칙 끄기
큰 프로젝트에서는 의도하지 않은 규칙 매칭이 디버깅을 어렵게 만듭니다. “왜 이 .yacc.c 파일이 자기 멋대로 컴파일되지?” 같은 의문이 나오면 보통 암시적 규칙이 범인입니다.
전체 비활성화 두 가지 방법:
# 방법 1: 접미사 규칙 비우기 (구식 방식 차단).SUFFIXES:
# 방법 2: 명령줄 또는 MAKEFLAGSMAKEFLAGS += --no-builtin-rules --no-builtin-variables--no-builtin-rules는 규칙만, --no-builtin-variables는 *기본 변수(CC=cc 등)*까지 끕니다. 둘을 같이 끄면 빈 상태에서 시작할 수 있어, 의도가 명확한 Makefile이 됩니다. Linux 커널 Makefile이 이 방식을 채택해 모든 규칙·변수를 명시적으로 적어 둡니다.
특정 패턴만 끄려면 빈 레시피의 패턴 규칙을 정의합니다.
%.o: %.c # 빈 레시피Make는 이 패턴 규칙이 존재한다고 인식하지만, 레시피가 비어 있어 내장 규칙을 발동시키지 않습니다. 이건 거의 안 쓰는 트릭이지만 알아 둘 만은 합니다.
#정적 패턴 규칙 — 적용 범위를 한정하는 패턴
정적 패턴 규칙(static pattern rule)은 특정 타겟 목록에만 패턴을 적용합니다. 일반 패턴 규칙이 전 우주의 .c에 적용되는 반면, 정적 패턴은 명시한 타겟 N개에만 한정됩니다.
타겟들: 타겟패턴: 의존성패턴 레시피#예시 — 특정 오브젝트만 다른 옵션으로
PROD_OBJS := main.o utils.o config.oTEST_OBJS := test_main.o test_utils.o
# 프로덕션: 최적화$(PROD_OBJS): %.o: %.c $(CC) $(CFLAGS) -O2 -DNDEBUG -c $< -o $@
# 테스트: 디버그 + 커버리지$(TEST_OBJS): %.o: %.c $(CC) $(CFLAGS) -g -O0 --coverage -c $< -o $@일반 패턴 규칙(%.o: %.c)을 두 개 적어 두면 서로 충돌합니다(같은 *.o 타겟에 두 규칙이 매칭). 정적 패턴은 적용 범위를 명시해 이 모호함을 없앱니다.
#일반 패턴 규칙과의 차이
| 측면 | 일반 패턴 %.o: %.c | 정적 패턴 $(OBJS): %.o: %.c |
|---|---|---|
| 적용 범위 | 모든 .o 타겟 | $(OBJS)에 포함된 타겟만 |
| 충돌 시 | 다른 패턴과 우선순위 다툼 | 명시 범위라 일찍 결정 |
| 디버깅 | ”왜 이 규칙이 적용됐지?” 추적 어려움 | 적용 대상이 명시되어 한눈에 |
규모가 커지면 정적 패턴 규칙이 유지보수상 분명한 이점을 갖습니다. 새 종류의 .o 타겟이 생겨도 영향 범위가 보호되기 때문입니다.
#규칙 검색 순서 — 우선순위표
Make가 한 타겟의 빌드 방법을 찾을 때 어떤 규칙을 먼저 시도하는지 — 이 순서를 알면 “왜 내 규칙이 안 먹히지?” 답이 보입니다.
- 명시적 규칙 — 타겟이 정확히 이 파일
- 정적 패턴 규칙 —
$(OBJS): %.o: %.c - 일반 패턴 규칙 —
%.o: %.c(사용자 정의) - 암시적 규칙 — Make 내장
- 매칭이 없으면 에러:
No rule to make target...
여러 패턴이 동시에 매칭될 때는 더 구체적인 패턴이 이깁니다. 예컨대 %.o와 lib%.o가 둘 다 libfoo.o에 매칭되면, 더 긴 stem을 가지지 않은 쪽(여기서는 lib%.o)이 우선합니다.
실제 예:
# 일반 패턴%.o: %.c $(CC) $(CFLAGS) -c $< -o $@
# config.o만 다른 옵션config.o: config.c config.h defaults.h $(CC) $(CFLAGS) -DVERSION="1.0" -c $< -o $@config.o는 명시적 규칙(2번째)이 있어 그쪽으로, 나머지 .o는 일반 패턴(3번째)으로 빌드됩니다.
#VPATH / vpath — 소스 검색 경로
지금까지는 소스가 Makefile과 같은 디렉터리에 있다고 가정했습니다. 하지만 실제 프로젝트는 src/, lib/, include/로 흩어집니다. 패턴 규칙에서 %.c 자리를 어디까지 뒤져 볼지 알려 주는 메커니즘이 VPATH와 vpath입니다.
#VPATH — 전역 검색 경로
VPATH = src:lib:third_party
%.o: %.c gcc -c $< -o $@VPATH는 콜론(또는 공백)으로 구분된 디렉터리 목록입니다. Make는 %.c를 찾을 때 현재 디렉터리에 없으면 VPATH 경로를 순서대로 검색합니다.
$ lsMakefile src/main.c lib/utils.c
$ make main.o# Make: main.c가 현재 디렉터리에 없네. VPATH 검색...# → src/main.c 발견 → gcc -c src/main.c -o main.o$<이 발견된 경로(src/main.c)로 풀린다는 점이 중요합니다. 컴파일러는 정확한 경로를 받습니다.
#vpath — 패턴별 검색 경로 (소문자)
대문자 VPATH가 모든 파일 타입에 적용되는 반면, 소문자 vpath는 패턴별로 지정합니다.
vpath %.c srcvpath %.h includevpath %.cpp src:third_party/src이게 VPATH보다 더 정확합니다. %.c는 src/만 검색하고, %.h는 include/만 검색합니다. 잘못된 디렉터리의 파일이 우연히 매칭되는 사고를 줄입니다.
vpath는 지시자(directive)라 함수가 아닙니다. 명령으로 호출하는 게 아니라 Makefile 최상위에 적습니다.
#흔한 함정 — $@는 발견 경로가 아니라 원래 타겟
VPATH = src
%.o: %.c gcc -c $< -o $@main.o를 빌드할 때:
$<은 발견된 경로src/main.c$@은 우리가 요청한main.o(검색 안 됨)
즉 출력 파일은 현재 디렉터리에 생성됩니다. 이게 원하는 동작이면 좋지만, 출력도 별도 디렉터리에 두고 싶다면 VPATH로는 부족합니다.
# build/ 안에 출력하려면 패턴을 직접 적어야 함build/%.o: src/%.c @mkdir -p build gcc -c $< -o $@실무에서 VPATH는 작은 프로젝트나 재귀 빌드 보조에 가끔 등장하고, 큰 프로젝트는 명시적 경로를 더 선호합니다. 안전하고 디버깅이 쉽기 때문입니다.
#Grouped Target — 한 명령이 여러 파일을 만들 때 (4.3+)
GNU Make 4.3(2020)에 추가된 &: 문법입니다.
# 기존 (각 출력이 별개 규칙으로 해석되어 protoc이 두 번 호출됨)%.pb.cc %.pb.h: %.proto $(PROTOC) --cpp_out=. $<
# 4.3+: 한 명령이 두 파일을 동시에 만든다고 명시%.pb.cc %.pb.h &: %.proto $(PROTOC) --cpp_out=. $<&:로 묶인 출력은 한 레시피 호출로 함께 만들어진다는 약속입니다. Make는 둘 중 하나가 필요해도 한 번만 명령을 실행합니다. 코드 생성기(protoc, yacc, bison, swig)를 다룰 때 매우 유용합니다.
4.3 이전 환경을 지원해야 한다면 회피책으로 intermediate 파일을 만들어 두 출력을 그 파일에 의존하게 잡습니다. 약간 복잡해지므로, 가능하면 4.3+ 사용을 권장합니다.
#실전 예시
CC := gccCFLAGS := -Wall -Wextra -g -std=c11 -O2CPPFLAGS := -Iinclude
SRCDIR := srcBUILDDIR := build
SRCS := $(wildcard $(SRCDIR)/*.c)OBJS := $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRCS))
TARGET := $(BUILDDIR)/myapp
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS)
# 정적 패턴 규칙: $(OBJS)에만 적용$(OBJS): $(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR) $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
$(BUILDDIR): mkdir -p $@
clean: $(RM) -r $(BUILDDIR)이 Makefile은 다음을 만족합니다.
src/*.c모든 파일을 자동 감지(wildcard).- 출력은
build/에 격리. - 정적 패턴으로 오브젝트만 이 규칙을 받아 다른
.o(예: 외부 라이브러리)와 충돌하지 않음. - 디렉터리 생성은 order-only로 안전.
wildcard, patsubst는 Ch 5: 함수에서 상세히 다룹니다.
#흔한 실수
#1. %가 여러 번 등장
%-%.o: %.c # 안 됨%는 한 패턴에 한 번만 허용됩니다. 두 번 이상 매칭하려면 정적 패턴이나 함수로 풀어야 합니다.
#2. stem에 경로가 끼어들음
build/%.o: src/%.c# build/sub/main.o의 stem = "sub/main"이걸 모르고 $*.c 같은 식으로 원본 위치 가정을 깔면 사고가 납니다. src/$*.c는 의도와 같은 경로지만, 다른 자리에서 $*을 파일 이름처럼 쓰면 문제가 됩니다.
해결: 파일 이름이 필요하면 $(notdir $*), 디렉터리는 $(dir $*).
#3. 명시적 규칙이 패턴 규칙을 막음
%.o: %.c gcc -c $< -o $@
main.o: main.c header.h # 레시피 없음main.o에 레시피 없는 명시적 규칙을 적으면 Make는 이걸 “특수한 의존성 추가”로 받아들이지 않습니다. 대신 “이 타겟에는 별도 규칙이 있다(레시피가 빈 채로)“로 해석해 패턴 규칙 적용을 막을 수 있습니다.
의도가 “패턴 규칙은 그대로 쓰되, 의존성만 추가”라면 레시피 없이 의존성만 적습니다.
%.o: %.c gcc -c $< -o $@
# 의존성만 추가 (콜론 + 의존성, 레시피 없음 = OK)main.o: header.h이건 묘하게 보이는 GNU Make 관용입니다. 같은 타겟에 의존성을 두 번 적으면 합쳐진다는 사실(Ch 2에서 본 “분리 의존성”)을 활용합니다.
#4. 암시적 규칙에 의존하면서 변수를 잊음
hello: main.o# CC, CFLAGS 등 미설정Make는 내장 규칙으로 빌드해 주지만, 변수가 비어 있어 경고도 디버그 정보도 없는 상태가 됩니다.
해결: 최소한 CC, CFLAGS, CPPFLAGS, LDLIBS는 설정.
#5. 4.3 이전에서 grouped target 가정
%.pb.cc %.pb.h &: %.proto # 4.3 이상 필요오래된 시스템에서 안 돌면 &: 때문일 수 있습니다. make --version을 먼저 확인하세요.
#정리
- 패턴 규칙
%.o: %.c은 같은 모양의 변환을 한 줄로 일반화한다.%는 한 번만. - stem
$*은%가 매칭한 실제 문자열 — 중첩 디렉터리에서는 경로가 포함된다. - 암시적 규칙은 Make가 내장한 변환 표.
CC/CFLAGS같은 표준 변수가 필요. - 정적 패턴
$(OBJS): %.o: %.c은 적용 범위를 명시해 충돌·모호함을 막는다. - 검색 순서: 명시적 → 정적 패턴 → 일반 패턴 → 암시적. 더 구체적인 게 이긴다.
- Grouped target
&:(4.3+)은 한 명령이 여러 출력을 만드는 경우. - 큰 프로젝트는
--no-builtin-rules로 시작해 명시적으로 규칙을 적는 게 안전.
#다음 장 예고
Ch 5: 함수에서는 Make의 내장 함수들 — wildcard, patsubst, filter, foreach, shell 등 — 을 다룹니다. 텍스트 처리만으로 거대한 Makefile을 수십 줄로 줄이는 도구들입니다.
#참고 자료
관련 글
실전 Makefile 예제 — C/C++ 프로젝트용 기본 골격
기본 C/C++부터 라이브러리, 크로스 컴파일, 테스트 통합까지 — 실제 프로젝트에 그대로 쓰는 Makefile 패턴.
Make 조건문과 include — ifeq·ifdef·include·-include
파싱 시점 조건 분기, Makefile 분할, 그리고 -MMD -MP로 헤더 의존성을 자동 추적하는 표준 패턴.
Make 함수 분석 — wildcard·patsubst·foreach·shell
내장 함수로 텍스트·파일·조건을 다루기 — wildcard / patsubst / filter / shell / foreach / call / eval.