C++ Makefile | "Make 빌드" 가이드

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         # 도움말

정리

핵심 요약

  1. 기본 문법: 타겟, 의존성, 명령어
  2. 변수: CXX, CXXFLAGS, 자동 변수 ($@, $<, $^)
  3. 패턴 규칙: %.o: %.cpp로 자동화
  4. 함수: wildcard, patsubst로 파일 처리
  5. 의존성 자동 생성: -MMD로 헤더 의존성 관리

Makefile vs CMake

특징MakefileCMake
복잡도낮음높음
크로스 플랫폼제한적우수
학습 곡선완만가파름
직접 제어높음낮음
적합한 프로젝트작은 프로젝트큰 프로젝트

실전 팁

  1. 효율적인 빌드

    • 병렬 빌드 사용 (make -j4)
    • 의존성 자동 생성 (-MMD)
    • ccache로 컴파일 캐싱
  2. 유지보수

    • 변수로 설정 중앙화
    • .PHONY로 타겟 명확히
    • 주석으로 복잡한 규칙 설명
  3. 디버깅

    • make -n: 명령어만 출력 (실행 안함)
    • make -d: 디버그 정보 출력
    • @echo 변수 값 확인

Make 명령어

명령어설명
make기본 타겟 빌드
make cleanclean 타겟 실행
make -j44개 작업 병렬 빌드
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++ 패키지 관리