C++ Makefile | "Make 빌드" 가이드
이 글의 핵심
C++ Makefile에 대한 실전 가이드입니다.
들어가며
Makefile은 Make가 사용하는 빌드 자동화 스크립트입니다. 컴파일과 링킹을 자동화하여 개발 생산성을 높입니다.
증분 빌드·병렬(make -j)은 CMake가 생성하는 Makefile/Ninja와도 같은 맥락입니다. 언어 수준에서는 Rust Cargo·npm 스크립트·Go 빌드가 각각 다른 철학으로 캐시·병렬을 다룹니다. 전체 스택 비교는 C++ 빌드 시스템 완전 비교를 보세요.
1. Makefile 기본
가장 간단한 Makefile
# Makefile
myapp: main.cpp
g++ main.cpp -o myapp
clean:
rm -f myapp
# 빌드
make
# 정리
make clean
출력:
g++ main.cpp -o myapp
기본 문법
# 타겟: 의존성
# 명령어 (반드시 탭으로 시작!)
target: dependencies
command
# 예시
main.o: main.cpp
g++ -c main.cpp -o main.o
구성 요소:
- 타겟(target): 생성할 파일 또는 작업 이름
- 의존성(dependencies): 타겟을 만들기 위해 필요한 파일들
- 명령어(command): 실행할 쉘 명령어 (반드시 탭으로 시작)
2. 변수 사용
기본 변수
# 변수 정의
CXX = g++
CXXFLAGS = -std=c++17 -Wall -O2
INCLUDES = -I./include
LIBS = -lpthread -lm
# 변수 사용
myapp: main.cpp
$(CXX) $(CXXFLAGS) $(INCLUDES) main.cpp -o myapp $(LIBS)
clean:
rm -f myapp
.PHONY: clean
자동 변수
CXX = g++
CXXFLAGS = -std=c++17 -Wall
# 자동 변수
# $@: 타겟 이름
# $<: 첫 번째 의존성
# $^: 모든 의존성
myapp: main.o util.o
$(CXX) $^ -o $@
# $^ = main.o util.o
# $@ = myapp
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
# $< = main.cpp (첫 번째 의존성)
# $@ = main.o (타겟)
자동 변수 정리
| 변수 | 의미 | 예시 |
|---|---|---|
$@ | 타겟 이름 | myapp |
$< | 첫 번째 의존성 | main.cpp |
$^ | 모든 의존성 | main.o util.o |
$? | 타겟보다 새로운 의존성 | main.o |
3. 실전 예제
예제 1: 간단한 프로젝트
# 컴파일러 설정
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra
# 타겟
TARGET = myapp
# 빌드
$(TARGET): main.cpp
$(CXX) $(CXXFLAGS) main.cpp -o $(TARGET)
# 실행
run: $(TARGET)
./$(TARGET)
# 정리
clean:
rm -f $(TARGET)
# 파일이 아닌 타겟
.PHONY: clean run
사용법:
make # 빌드
make run # 빌드 후 실행
make clean # 정리
예제 2: 여러 파일 프로젝트
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra
TARGET = myapp
# 오브젝트 파일
OBJS = main.o calculator.o utils.o
# 링킹
$(TARGET): $(OBJS)
$(CXX) $(OBJS) -o $(TARGET)
# 컴파일
main.o: main.cpp calculator.h utils.h
$(CXX) $(CXXFLAGS) -c main.cpp
calculator.o: calculator.cpp calculator.h
$(CXX) $(CXXFLAGS) -c calculator.cpp
utils.o: utils.cpp utils.h
$(CXX) $(CXXFLAGS) -c utils.cpp
# 정리
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: clean
예제 3: 패턴 규칙 (자동화)
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra
TARGET = myapp
# 모든 .cpp 파일 찾기
SRCS = $(wildcard *.cpp)
OBJS = $(SRCS:.cpp=.o)
# 링킹
$(TARGET): $(OBJS)
$(CXX) $^ -o $@
# 패턴 규칙: 모든 .cpp → .o
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: clean
패턴 규칙 설명:
%.o: %.cpp: 모든.cpp파일을.o로 컴파일$<: 첫 번째 의존성 (.cpp파일)$@: 타겟 (.o파일)
예제 4: 라이브러리 링크
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra
INCLUDES = -I./include
LDFLAGS = -lpthread -lm
TARGET = myapp
SRCS = $(wildcard src/*.cpp)
OBJS = $(SRCS:src/%.cpp=obj/%.o)
# 링킹
$(TARGET): $(OBJS)
$(CXX) $^ -o $@ $(LDFLAGS)
# 컴파일
obj/%.o: src/%.cpp
@mkdir -p obj
$(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@
clean:
rm -rf obj $(TARGET)
.PHONY: clean
4. 고급 기능
조건부 컴파일
CXX = g++
TARGET = myapp
# DEBUG 변수에 따라 플래그 변경
ifeq ($(DEBUG),1)
CXXFLAGS = -std=c++17 -Wall -g -DDEBUG
else
CXXFLAGS = -std=c++17 -Wall -O2 -DNDEBUG
endif
$(TARGET): main.cpp
$(CXX) $(CXXFLAGS) main.cpp -o $(TARGET)
clean:
rm -f $(TARGET)
.PHONY: clean
사용법:
make # Release 빌드
make DEBUG=1 # Debug 빌드
함수 사용
CXX = g++
CXXFLAGS = -std=c++17 -Wall
# wildcard: 파일 패턴 매칭
SRCS = $(wildcard src/*.cpp)
# patsubst: 패턴 치환
OBJS = $(patsubst src/%.cpp,obj/%.o,$(SRCS))
# shell: 쉘 명령 실행
$(shell mkdir -p obj)
myapp: $(OBJS)
$(CXX) $^ -o $@
obj/%.o: src/%.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -rf obj myapp
.PHONY: clean
의존성 자동 생성
CXX = g++
CXXFLAGS = -std=c++17 -Wall
SRCS = $(wildcard *.cpp)
OBJS = $(SRCS:.cpp=.o)
DEPS = $(OBJS:.o=.d)
myapp: $(OBJS)
$(CXX) $^ -o $@
# -MMD: 의존성 파일 생성
%.o: %.cpp
$(CXX) $(CXXFLAGS) -MMD -c $< -o $@
# 의존성 파일 포함
-include $(DEPS)
clean:
rm -f $(OBJS) $(DEPS) myapp
.PHONY: clean
설명:
-MMD: 헤더 의존성을.d파일로 생성-include: 의존성 파일을 포함 (없어도 에러 안남)- 헤더 파일 변경 시 자동으로 재컴파일
5. 자주 발생하는 문제
문제 1: 탭 vs 스페이스
# ❌ 스페이스 사용 (에러!)
target:
command
# ✅ 탭 사용
target:
command
에러 메시지:
Makefile:2: *** missing separator. Stop.
해결 방법:
- 에디터 설정에서 탭을 스페이스로 변환하지 않도록 설정
- Makefile 모드 사용 (자동으로 탭 삽입)
문제 2: 의존성 누락
# ❌ 헤더 의존성 없음
main.o: main.cpp
g++ -c main.cpp
# util.h가 변경되어도 재컴파일 안됨!
# ✅ 헤더 포함
main.o: main.cpp util.h config.h
g++ -c main.cpp
# util.h나 config.h 변경 시 자동 재컴파일
문제 3: .PHONY 누락
# ❌ clean이라는 파일이 있으면 실행 안됨
clean:
rm -f *.o
# ✅ .PHONY 사용
.PHONY: clean
clean:
rm -f *.o
# clean 파일이 있어도 항상 실행됨
문제 4: 병렬 빌드
# 순차 빌드 (느림)
make
# 병렬 빌드 (4개 작업 동시)
make -j4
# CPU 코어 수만큼 병렬 빌드
make -j$(nproc)
실전 팁:
- 병렬 빌드로 컴파일 시간 단축
- 의존성이 올바르게 설정되어야 병렬 빌드 가능
6. 실전 예제: 완전한 프로젝트
프로젝트 구조
project/
├── Makefile
├── include/
│ ├── calculator.h
│ └── utils.h
├── src/
│ ├── main.cpp
│ ├── calculator.cpp
│ └── utils.cpp
└── obj/
└── (빌드 시 생성)
완전한 Makefile
# 컴파일러 설정
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra
INCLUDES = -I./include
LDFLAGS = -lpthread
# 디렉토리
SRC_DIR = src
OBJ_DIR = obj
INC_DIR = include
# 파일
SRCS = $(wildcard $(SRC_DIR)/*.cpp)
OBJS = $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(SRCS))
DEPS = $(OBJS:.o=.d)
# 타겟
TARGET = myapp
# 디버그 빌드
ifeq ($(DEBUG),1)
CXXFLAGS += -g -DDEBUG
else
CXXFLAGS += -O2 -DNDEBUG
endif
# 기본 타겟
all: $(TARGET)
# 링킹
$(TARGET): $(OBJS)
$(CXX) $^ -o $@ $(LDFLAGS)
@echo "빌드 완료: $(TARGET)"
# 컴파일 (의존성 자동 생성)
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
@mkdir -p $(OBJ_DIR)
$(CXX) $(CXXFLAGS) $(INCLUDES) -MMD -c $< -o $@
# 의존성 파일 포함
-include $(DEPS)
# 실행
run: $(TARGET)
./$(TARGET)
# 정리
clean:
rm -rf $(OBJ_DIR) $(TARGET)
# 전체 재빌드
rebuild: clean all
# 도움말
help:
@echo "사용 가능한 타겟:"
@echo " make - Release 빌드"
@echo " make DEBUG=1 - Debug 빌드"
@echo " make run - 빌드 후 실행"
@echo " make clean - 정리"
@echo " make rebuild - 전체 재빌드"
@echo " make -j4 - 병렬 빌드 (4개)"
.PHONY: all run clean rebuild help
사용 예시:
make # Release 빌드
make DEBUG=1 # Debug 빌드
make -j4 # 병렬 빌드
make run # 실행
make clean # 정리
make rebuild # 재빌드
make help # 도움말
정리
핵심 요약
- 기본 문법: 타겟, 의존성, 명령어
- 변수:
CXX,CXXFLAGS, 자동 변수 ($@,$<,$^) - 패턴 규칙:
%.o: %.cpp로 자동화 - 함수:
wildcard,patsubst로 파일 처리 - 의존성 자동 생성:
-MMD로 헤더 의존성 관리
Makefile vs CMake
| 특징 | Makefile | CMake |
|---|---|---|
| 복잡도 | 낮음 | 높음 |
| 크로스 플랫폼 | 제한적 | 우수 |
| 학습 곡선 | 완만 | 가파름 |
| 직접 제어 | 높음 | 낮음 |
| 적합한 프로젝트 | 작은 프로젝트 | 큰 프로젝트 |
실전 팁
-
효율적인 빌드
- 병렬 빌드 사용 (
make -j4) - 의존성 자동 생성 (
-MMD) - ccache로 컴파일 캐싱
- 병렬 빌드 사용 (
-
유지보수
- 변수로 설정 중앙화
.PHONY로 타겟 명확히- 주석으로 복잡한 규칙 설명
-
디버깅
make -n: 명령어만 출력 (실행 안함)make -d: 디버그 정보 출력@echo변수 값 확인
Make 명령어
| 명령어 | 설명 |
|---|---|
make | 기본 타겟 빌드 |
make clean | clean 타겟 실행 |
make -j4 | 4개 작업 병렬 빌드 |
make -n | 명령어만 출력 (dry-run) |
make -B | 모든 타겟 강제 재빌드 |
다음 단계
- C++ Compilation Process
- C++ Linking
- C++ CMake Build System
관련 글
- C++ CMake 완벽 가이드 | 크로스 플랫폼 빌드·최신 CMake 3.28+ 기능·프리셋·모듈
- C++ CMake |
- C++ CMake find_package 완벽 가이드 | 외부 라이브러리 통합
- C++ CMake Targets 완벽 가이드 | 타겟 기반 빌드 시스템
- C++ Conan 완벽 가이드 | 현대적인 C++ 패키지 관리