-
[리눅스 기초] Makefile이란? 똑똑한 빌드 자동화임베디드 개발 환경 이야기 2025. 9. 12. 14:56
C/C++ 프로그래밍을 시작하면 우리는 보통 터미널에 이런 명령어를 직접 입력하며 컴파일을 합니다.
gcc -o my_program main.c my_func.c utils.c파일이 한두 개일 때는 괜찮습니다.
하지만 파일이 수십, 수백 개로 늘어나고, 컴파일 옵션이 복잡해지면 어떨까요?
- 작은 코드 한 줄 바꿨을 뿐인데, 매번 저 긴 명령어를 다시 입력해야 합니다.
- 어떤 파일을 바꿔야 어떤 오브젝트 파일을 다시 만들어야 하는지 뇌에 의존해야 합니다.
- 실수로 파일 하나를 빼먹고 컴파일해서 원인 모를 에러와 싸우기도 합니다.
이런 귀찮고 반복적인 '빌드' 과정을 자동화해주고, 최소한의 작업만 수행하도록 똑똑하게 관리해주는 친구가 바로 오늘 배울 Makefile과 make 명령어입니다.
🤖 Makefile은 "어떻게 빌드해야 하는지"가 적힌 '레시피 북'이고, make는 그 레시피 북을 읽고 요리(빌드)를 수행하는 '자동화 로봇'입니다.
- Makefile의 기본 원리: "그래서 뭐가 좋은데?"
Makefile의 핵심 철학은 아주 간단하고 강력합니다.
"결과물(Target)이 재료(Prerequisites)보다 오래되었다면, 레시피(Command)에 따라 결과물을 다시 만들어라."
- 이게 전부입니다. 여기서 말하는 '오래되었다'는 것은 파일의 최종 수정 시간을 기준으로 합니다.
예를 들어, main.c 파일을 수정하면 main.c의 수정 시간이 이전에 만들어진 main.o (오브젝트 파일)보다 최신이 됩니다.
이때 make를 실행하면, 이 관계를 파악하고 정확히 main.o를 만드는 컴파일 과정만 다시 수행합니다.
수정하지 않은 다른 파일(utils.c, my_func.c 등)은 건드리지 않죠.
이 원리 덕분에 우리는 3가지 큰 이점을 얻을 수 있습니다.
1.빌드 자동화: make 명령어 하나로 복잡한 빌드 과정을 끝낼 수 있습니다.
2.효율적인 빌드: 변경된 부분만 골라서 다시 컴파일하므로, 대규모 프로젝트에서도 빌드 시간을 획기적으로 줄일 수 있습니다.
3.일관성 있는 빌드: 누가, 언제 빌드하든 항상 동일한 과정을 보장하여 "제 컴퓨터에선 됐는데요?" 같은 문제를 방지합니다.- Makefile의 뼈대: 규칙(Rule) 이해하기
Makefile은 규칙(Rule)들의 집합으로 이루어져 있습니다. 규칙의 기본 구조는 다음과 같습니다.
TARGET ... : PREREQUISITES ...
COMMAND
- TARGET (타겟): 우리가 만들고 싶은 최종 결과물입니다. 보통 실행 파일이나 오브젝트 파일 이름이 들어갑니다.
- PREREQUISITES (의존성): 타겟을 만드는 데 필요한 재료 파일들입니다. 이 파일들 중 하나라도 타겟보다 최신이라면, 아래의 명령이 실행됩니다.
- COMMAND (명령): 타겟을 만들기 위해 실행해야 할 셸 명령어입니다.
⚠️ 아주 아주 중요한 점!
COMMAND 라인은 반드시 스페이스(공백)가 아닌Tab
키로 시작해야 합니다. 수많은 초보자들이 여기서 실수를 하고 좌절합니다. 꼭 기억하세요!
- 실전 예제: Hello World부터 시작하기
이제 간단한 예제를 통해 Makefile을 직접 만들어 보겠습니다.
STEP 1: 여러 개의 소스 파일 준비
먼저, 3개의 파일(main.c, my_func.c, my_func.h)을 준비합니다.
my_func.h
#ifndef MY_FUNC_H
#define MY_FUNC_H
void print_hello();
#endif
my_func.c
#include <stdio.h>
#include "my_func.h"void print_hello() {
printf("Hello from my_func!\n");
}main.c
#include "my_func.h"int main() {
print_hello();
return 0;
}이 프로젝트를 Makefile 없이 빌드하려면 다음과 같이 복잡한 과정을 거쳐야 합니다.
소스 파일을 오브젝트 파일(.o)로 컴파일
gcc -c -o main.o main.c
gcc -c -o my_func.o my_func.c오브젝트 파일들을 합쳐서(링크해서) 최종 실행 파일 생성
gcc -o hello main.o my_func.o
STEP 2: 첫 번째 Makefile 작성하기
이제 이 과정을 Makefile로 자동화해 보겠습니다. Makefile이라는 이름의 파일을 만들고 아래 내용을 작성하세요.최종 목표는 'hello' 실행 파일을 만드는 것
hello: main.o my_func.o
gcc -o hello main.o my_func.omain.o를 만드는 방법
main.o: main.c my_func.h
gcc -c -o main.o main.cmy_func.o를 만드는 방법
my_func.o: my_func.c my_func.h
gcc -c -o my_func.o my_func.c##빌드 결과물을 청소하는 규칙
clean:
rm -f hello main.o my_func.o이제 터미널에서 make 명령어 하나만 실행해 보세요. Makefile에 적힌 규칙에 따라 모든 빌드 과정이 자동으로 실행됩니다.
만약 my_func.c 파일만 살짝 수정하고 다시 make를 실행하면 어떨까요? my_func.o를 만들고 최종 hello를 링크하는 과정만 실행될 뿐, main.o를 만드는 과정은 건너뛰는 것을 볼 수 있습니다. 정말 똑똑하죠?
4. Makefile을 더 강력하게: 변수와 자동 변수
위 Makefile은 잘 동작하지만, 파일이 추가될 때마다 규칙을 계속 복사/붙여넣기 해야 해서 불편합니다. 이제 변수를 사용해서 더 깔끔하고 확장성 있게 만들어 보겠습니다.
일반 변수 (Variables)
마치 프로그래밍 언어처럼, 자주 사용되는 값들을 변수로 지정할 수 있습니다.CC: 컴파일러 지정
CFLAGS: 컴파일 옵션
TARGET: 최종 실행 파일 이름
OBJS: 오브젝트 파일 목록
변수 정의
CC = gcc
CFLAGS = -Wall -Wextra -std=c11
TARGET = hello
OBJS = main.o my_func.o
규칙에서 변수 사용 ($(변수명))
$(TARGET): $(OBJS)
$(CC) -o $(TARGET) $(OBJS)main.o: main.c my_func.h
$(CC) $(CFLAGS) -c -o main.o main.cmy_func.o: my_func.c my_func.h
$(CC) $(CFLAGS) -c -o my_func.o my_func.cclean:
rm -f $(TARGET) $(OBJS)이제 컴파일러를 clang으로 바꾸고 싶다면, CC = gcc 부분만 CC = clang으로 바꾸면 모든 규칙에 적용됩니다. 훨씬 편리해졌죠!
✨ 자동 변수 (Automatic Variables) & 패턴 규칙
여기서 한 단계 더 나아가면, Makefile의 진정한 마법이 시작됩니다. 바로 자동 변수와 패턴 규칙입니다.$@: 규칙의 '타겟' 이름을 의미합니다.
$<: 규칙의 '첫 번째 의존성 파일' 이름을 의미합니다.
$^: 규칙의 '모든 의존성 파일' 목록을 의미합니다.
이것들을 패턴 규칙 %.o: %.c (모든 .c 파일로부터 .o 파일을 만드는 규칙)과 함께 사용하면, 수많은 .o 파일 생성 규칙을 단 하나로 줄일 수 있습니다.
최종적으로 개선된 Makefile을 보시죠.변수 정의
CC = gcc
CFLAGS = -Wall -Wextra -std=c11
TARGET = hello
SRCS = main.c my_func.c
OBJS = $(SRCS:.c=.o) # SRCS 변수의 .c 확장자를 .o로 치환
Phony Target 정의
.PHONY: all clean
기본 규칙
all: $(TARGET)
링크 규칙
$(TARGET): $(OBJS)
$(CC) -o $@ $^ # $@는 'hello', $^는 'main.o my_func.o'컴파일 패턴 규칙
.c 파일로부터 .o 파일을 만드는 모든 경우에 이 규칙을 적용
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $< # $@는 타겟(.o), $<는 소스(.c)클린 규칙
clean:
rm -f $(TARGET) $(OBJS)이 Makefile은 소스 파일(SRCS) 목록에 new_file.c를 추가하기만 하면, 나머지 빌드 과정은 알아서 처리해주는 매우 유연하고 강력한 구조를 갖게 되었습니다.
.PHONY 란?
clean처럼 실제 파일이 아닌, 명령의 이름으로만 사용되는 타겟을 '가짜(Phony) 타겟'이라고 합니다. 만약 clean이라는 파일이 실수로 생성되면 make clean이 동작하지 않을 수 있는데, .PHONY: clean이라고 선언해두면 clean이라는 파일의 존재 여부와 상관없이 항상 명령을 실행하도록 보장해 줍니다. all도 관례적으로 많이 사용합니다.마무리하며
오늘은 Makefile의 기본 개념부터 변수와 패턴 규칙을 활용한 실용적인 예제까지 살펴보았습니다.'임베디드 개발 환경 이야기' 카테고리의 다른 글
커널(kernel)이란? 시스템의 지배자, 리눅스 커널(Kernel)의 모든 것 (0) 2025.09.15 BSP란? 임베디드 시스템의 통역사 BSP 파헤치기 (0) 2025.09.15 임베디드 시스템이란? & 리눅스 부팅 시퀀스 (부트로더 -> 커널 -> init) (0) 2025.09.15 [Yocto 기초] Yocto란? 핵심 개념(BitBake, Layer, Recipe) 쉽게 이해하기 (0) 2025.09.12 [Gerrit 기초] Gerrit이란? GitHub의 Pull Request와 비교 (0) 2025.09.12