생산성 자동화

개발 생산성을 극대화하는 Makefile 활용 전략: 빌드, 테스트, 배포 자동화 실전 가이드

강코의 코딩 일기 2026. 6. 24. 07:16
반응형

복잡한 개발 태스크에 지쳐있나요? Makefile로 빌드, 테스트, 배포 과정을 자동화하여 개발 생산성을 비약적으로 높이는 실전 노하우를 공유합니다.

개발 생산성을 높이는 Makefile 활용 전략: 복잡한 빌드, 테스트, 배포 태스크 자동화 - kaufmann, businessman, gears, work, productivity, mechanics, automation, marketing, concept, automation, automation, automation, automation, automation

Image by geralt on Pixabay

개발 생산성, 정말 높일 수 있을까? Makefile이 답이다.

개발 프로젝트를 진행하면서 반복적인 작업에 시간을 낭비하고 있다는 느낌을 받아본 적 있으신가요? 코드를 빌드하고, 테스트를 실행하고, 최종 결과물을 배포하는 과정은 개발의 핵심이지만, 동시에 가장 많은 수작업과 오류를 유발할 수 있는 부분이기도 합니다. 매번 같은 명령어를 입력하고, 순서를 기억하고, 혹시라도 빼먹은 단계가 없는지 확인하는 일은 개발자의 소중한 시간을 갉아먹는 주범이죠. 저 역시 이러한 반복 작업에 지쳐있던 때가 있었고, 어떻게 하면 이 과정을 더 효율적으로 만들 수 있을까 고민했습니다. 그 결과, Makefile이 훌륭한 해답이라는 것을 직접 경험했습니다.

많은 개발자가 Makefile을 C/C++ 프로젝트의 빌드 도구로만 생각하지만, 실제로 Makefile은 언어나 기술 스택에 구애받지 않고 거의 모든 종류의 반복적인 태스크를 자동화할 수 있는 강력한 도구입니다. 복잡한 셸 스크립트를 관리하는 것보다 훨씬 체계적이고 직관적인 방식으로 프로젝트의 빌드, 테스트, 배포 과정을 표준화하고 자동화할 수 있습니다. 개인적으로 프로젝트에 Makefile을 도입한 후, 작업 전환 시간이 크게 줄고, 팀원 간의 환경 설정 통일성도 높아져 전반적인 개발 생산성이 체감할 정도로 향상되었습니다. 지금부터 제가 직접 Makefile을 활용하여 개발 생산성을 끌어올린 실전 노하우를 상세히 공유하고자 합니다.

왜 Makefile인가? 스크립트와는 다른 강점

물론 셸 스크립트나 npm 스크립트, 혹은 다른 빌드 도구들도 자동화에 사용될 수 있습니다. 하지만 Makefile은 몇 가지 독특한 장점을 가지고 있습니다.

특징 Makefile 셸 스크립트 npm scripts (package.json)
의존성 관리 타겟 간 명확한 의존성 정의 및 증분 빌드 지원 수동으로 순서 제어, 증분 빌드 어려움 스크립트 간 단순 순차 실행 또는 병렬 실행
범용성 언어/환경 독립적, 모든 종류의 태스크에 적용 가능 셸 환경에 의존적 (bash, zsh 등) 주로 Node.js 프로젝트에 특화
가독성 및 구조 타겟-명령어 구조로 직관적, 문서화 효과 자유로운 형식, 복잡해지면 가독성 저하 JSON 형식, 간단한 스크립트에 적합
학습 곡선 초기 학습 필요 (문법, 변수, 함수) 기존 셸 지식 활용, 비교적 낮음 Node.js 개발자라면 접근성 높음
재사용성 변수, 함수, include 등을 통한 높은 재사용성 함수화하여 재사용 가능하나 Makefile만큼 체계적이지는 않음 다른 스크립트를 호출하는 방식

개인적으로 Makefile의 가장 큰 강점은 의존성 관리범용성이라고 생각합니다. 특정 타겟이 다른 타겟에 의존하고 있다면, Makefile은 자동으로 의존하는 타겟을 먼저 실행합니다. 또한, 파일의 변경 사항을 감지하여 불필요한 작업을 건너뛰는 증분 빌드 기능을 기본으로 제공하여 효율성을 극대화합니다. 이는 복잡한 프로젝트에서 빌드 시간을 크게 단축하는 데 기여합니다. 이제 Makefile의 기본 문법부터 차근차근 살펴보겠습니다.

Makefile 기본 문법 완전 정복: 핵심만 쏙쏙

Makefile은 매우 간결하지만 강력한 문법을 가지고 있습니다. 핵심은 타겟(Target), 의존성(Prerequisites), 명령어(Commands) 세 가지입니다. 제가 처음 Makefile을 접했을 때 가장 헷갈렸던 부분은 탭(Tab) 문자를 사용해야 한다는 점이었는데, 이 점만 주의하면 나머지는 비교적 쉽게 익힐 수 있습니다.


# 주석은 #으로 시작합니다.

# 기본 타겟: 'make'만 입력했을 때 실행됩니다.
all: build test

# 빌드 타겟
build:
    @echo "프로젝트 빌드 시작..."
    # 실제 빌드 명령어를 여기에 작성합니다.
    # 예: go build -o myapp ./cmd/main.go
    # 예: npm run build
    @echo "프로젝트 빌드 완료."

# 테스트 타겟
test:
    @echo "테스트 실행 시작..."
    # 실제 테스트 명령어를 여기에 작성합니다.
    # 예: go test ./...
    # 예: pytest
    @echo "테스트 실행 완료."

# 정리 타겟 (빌드 결과물 등을 삭제)
clean:
    @echo "빌드 결과물 정리 시작..."
    # 예: rm -rf dist/ build/ *.o
    @echo "빌드 결과물 정리 완료."

.PHONY: all build test clean
    

위 예시에서 all, build, test, clean타겟입니다. all 타겟은 buildtest의존하므로, make all을 실행하면 build가 먼저 실행되고 이어서 test가 실행됩니다. 각 타겟 아래 들여쓰기된 줄은 해당 타겟이 실행될 때 수행될 명령어입니다. @는 명령어가 터미널에 출력되는 것을 막아줘서 출력을 깔끔하게 만듭니다.

PHONY 타겟, 왜 중요할까?

.PHONYMakefile에서 매우 중요한 개념입니다. .PHONY: target_name과 같이 선언된 타겟은 실제 파일이 아닌 가상의 타겟으로 취급됩니다. 만약 프로젝트 루트에 build라는 이름의 파일이 존재하고, build 타겟을 .PHONY로 선언하지 않았다면, make build를 실행했을 때 Makefilebuild 파일이 이미 존재하고 최신 상태라고 판단하여 명령어를 실행하지 않을 수 있습니다. .PHONY를 사용하면 항상 해당 타겟의 명령어가 실행되도록 보장할 수 있습니다. 제가 처음 Makefile을 사용했을 때 이 부분 때문에 삽질했던 기억이 있어서 강조하고 싶습니다. 빌드, 테스트, 클린과 같이 실제 파일을 생성하지 않는 동작들은 반드시 .PHONY로 선언하는 것이 좋습니다.

복잡한 빌드 프로세스, Makefile로 한 방에!

다양한 언어와 프레임워크를 사용하는 프로젝트에서 빌드 과정은 종종 복잡해집니다. 백엔드는 Go로, 프론트엔드는 React로 개발하고 있다면, 각각의 빌드 명령어를 따로 실행해야 하고 순서도 맞춰야 합니다. Makefile을 사용하면 이 모든 과정을 하나의 명령어로 통합하여 자동화할 수 있습니다.


# 변수 정의
APP_NAME := my-fullstack-app
BACKEND_DIR := backend
FRONTEND_DIR := frontend
BUILD_DIR := dist

.PHONY: all build backend-build frontend-build clean

all: build

build: backend-build frontend-build
    @echo "모든 빌드 완료: ${APP_NAME}이(가) 배포 준비되었습니다."

backend-build:
    @echo "백엔드 빌드 시작..."
    cd ${BACKEND_DIR} && go build -o ../${BUILD_DIR}/${APP_NAME}-backend ./cmd/main.go
    @echo "백엔드 빌드 완료."

frontend-build:
    @echo "프론트엔드 빌드 시작..."
    cd ${FRONTEND_DIR} && npm install && npm run build
    mkdir -p ${BUILD_DIR}/public
    cp -r ${FRONTEND_DIR}/build/* ${BUILD_DIR}/public/
    @echo "프론트엔드 빌드 완료."

clean:
    @echo "빌드 결과물 정리 시작..."
    rm -rf ${BUILD_DIR}
    cd ${FRONTEND_DIR} && rm -rf node_modules build
    @echo "빌드 결과물 정리 완료."
    

위 예시에서 build 타겟은 backend-buildfrontend-build에 의존합니다. make build를 실행하면 Makefile이 알아서 백엔드와 프론트엔드를 순차적으로 빌드하고 최종 결과물을 dist 디렉토리에 모아줍니다. 이렇게 함으로써 개발자는 make build라는 단 하나의 명령어만 기억하면 됩니다. 실제로 이 방법을 적용한 후, 신입 개발자도 복잡한 빌드 과정을 단번에 이해하고 실행할 수 있게 되어 온보딩 시간이 30% 이상 단축되는 효과를 보았습니다.

다양한 언어/프레임워크에서의 빌드 자동화

Makefile은 특정 언어에 종속되지 않는다는 점에서 큰 강점을 가집니다. 예를 들어, Python 프로젝트에서는 가상 환경 설정부터 패키지 설치, 그리고 배포용 패키징까지 Makefile자동화할 수 있습니다. Java 프로젝트에서는 Maven이나 Gradle 명령어를 Makefile 타겟으로 감싸서 사용할 수 있습니다. 저는 개인적으로 Go 프로젝트에서 Makefile을 활용하여 빌드 시점에 Git 커밋 해시나 빌드 시간 같은 정보를 바이너리에 포함시키는 데 사용했습니다. 이는 배포된 바이너리의 버전을 추적하고 문제 발생 시 디버깅하는 데 엄청난 도움이 됩니다.


# Go 프로젝트 예시: 빌드 정보 포함
VERSION := $(shell git describe --tags --always --dirty)
BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
LDFLAGS := -X "main.version=${VERSION}" -X "main.goBuildTime=${BUILD_TIME}"

build-with-info:
    go build -ldflags "${LDFLAGS}" -o bin/${APP_NAME} ./cmd/main.go
    

이처럼 Makefile은 단순히 명령어를 나열하는 것을 넘어, 변수함수를 활용하여 훨씬 더 동적이고 유연한 빌드 파이프라인을 구축할 수 있도록 돕습니다.

개발 생산성을 높이는 Makefile 활용 전략: 복잡한 빌드, 테스트, 배포 태스크 자동화 - crane, construction site, construction worker, track, rails, work, track construction, construction company, construction site, construction site, construction site, construction site, construction site, construction worker, construction worker, work, work, work, work, work

Image by KVNSBL on Pixabay

신뢰할 수 있는 개발 환경의 시작: 테스트 자동화

테스트는 소프트웨어 품질을 보장하는 핵심 요소입니다. 하지만 테스트 실행 역시 반복적이고 지루한 작업이 될 수 있습니다. 특히 다양한 종류의 테스트(단위 테스트, 통합 테스트, E2E 테스트 등)가 존재할 경우, 각각의 테스트 스위트를 개별적으로 실행하는 것은 비효율적입니다. Makefile을 사용하면 모든 테스트를 하나의 명령어로 자동화하고, 특정 상황에 맞는 테스트만 선택적으로 실행할 수도 있습니다.


.PHONY: test unit-test integration-test e2e-test

test: unit-test integration-test
    @echo "모든 개발 단계 테스트 완료."

unit-test:
    @echo "단위 테스트 실행..."
    # 예: pytest tests/unit
    go test ./pkg/...
    @echo "단위 테스트 완료."

integration-test:
    @echo "통합 테스트 실행..."
    # 실제 통합 테스트를 위한 DB 또는 외부 서비스 구동이 필요한 경우, 여기에 포함시킬 수 있습니다.
    # 예: docker-compose up -d db && sleep 5
    go test -tags=integration ./internal/...
    # 예: docker-compose down
    @echo "통합 테스트 완료."

e2e-test:
    @echo "E2E 테스트 실행 (Docker 환경 필요)..."
    # 예: docker-compose up -d --build && cypress run
    # 예: npx playwright test
    @echo "E2E 테스트 완료."
    

Makefiletest 타겟이 unit-testintegration-test에 의존하도록 설정하여 make test 명령 한 번으로 두 가지 유형의 테스트를 순차적으로 실행합니다. e2e-test는 별도의 타겟으로 분리하여 필요할 때만 실행하도록 했습니다. 실제로 이 방식을 도입한 후, 개발자들이 테스트 실행을 훨씬 더 자주 하게 되었고, 버그 발견율이 20% 이상 증가하는 긍정적인 효과를 경험했습니다.

테스트 리포트 생성 및 활용

테스트를 실행하는 것만큼 중요한 것은 테스트 리포트를 생성하고 활용하는 것입니다. 많은 테스트 프레임워크는 JUnit XML 형식이나 HTML 형식의 리포트 생성을 지원합니다. Makefile은 이러한 리포트 생성 명령어를 포함시켜 테스트 결과를 시각화하고, CI/CD 파이프라인에서 활용할 수 있도록 자동화할 수 있습니다.


TEST_REPORT_DIR := test-reports

.PHONY: test-report

test-report:
    @echo "테스트 리포트 생성..."
    mkdir -p ${TEST_REPORT_DIR}
    go test -v ./... -json > ${TEST_REPORT_DIR}/go_test_results.json
    # JUnit XML 리포트 생성 예시 (go-junit-report 도구 사용)
    go test -v ./... | go-junit-report > ${TEST_REPORT_DIR}/junit.xml
    @echo "테스트 리포트가 ${TEST_REPORT_DIR}에 생성되었습니다."
    

이렇게 생성된 리포트는 Jenkins, GitLab CI, GitHub Actions와 같은 CI/CD 도구에서 파싱하여 대시보드에 표시하거나 빌드 실패 조건으로 활용할 수 있습니다. Makefile테스트 리포트 생성을 자동화함으로써, 개발자는 테스트 결과 분석에 더 집중할 수 있게 됩니다.

배포 자동화: 수동 작업의 위험을 없애다

수동 배포는 가장 흔하게 휴먼 에러를 유발하는 작업 중 하나입니다. 잘못된 파일을 업로드하거나, 특정 단계를 누락하거나, 서버를 잘못 재시작하는 등의 실수는 서비스 장애로 이어질 수 있습니다. Makefile은 이러한 배포 과정을 표준화하고 자동화하여 이러한 위험을 최소화할 수 있습니다. 저는 Docker 기반의 서비스 배포Makefile을 적극적으로 활용합니다.


# Docker 관련 변수
DOCKER_IMAGE_NAME := my-service
DOCKER_TAG := $(shell git rev-parse --short HEAD)
REGISTRY := myregistry.example.com

# 배포 관련 변수
SERVER_USER := deployuser
SERVER_HOST := example.com
DEPLOY_PATH := /opt/my-service

.PHONY: docker-build docker-push deploy-local deploy-remote

docker-build:
    @echo "Docker 이미지 빌드 시작..."
    docker build -t ${DOCKER_IMAGE_NAME}:${DOCKER_TAG} .
    docker tag ${DOCKER_IMAGE_NAME}:${DOCKER_TAG} ${REGISTRY}/${DOCKER_IMAGE_NAME}:latest
    docker tag ${DOCKER_IMAGE_NAME}:${DOCKER_TAG} ${REGISTRY}/${DOCKER_IMAGE_NAME}:${DOCKER_TAG}
    @echo "Docker 이미지 ${DOCKER_IMAGE_NAME}:${DOCKER_TAG} 빌드 완료."

docker-push: docker-build
    @echo "Docker 이미지 푸시 시작..."
    docker push ${REGISTRY}/${DOCKER_IMAGE_NAME}:latest
    docker push ${REGISTRY}/${DOCKER_IMAGE_NAME}:${DOCKER_TAG}
    @echo "Docker 이미지 푸시 완료."

deploy-local: docker-build
    @echo "로컬 환경에 서비스 배포 (Docker Compose)..."
    docker-compose up -d --build
    @echo "로컬 배포 완료."

deploy-remote: docker-push
    @echo "원격 서버 ${SERVER_HOST}에 서비스 배포 시작..."
    # SSH를 통해 원격 서버에서 Docker Pull 및 서비스 재시작
    ssh ${SERVER_USER}@${SERVER_HOST} " \
        cd ${DEPLOY_PATH} && \
        docker login ${REGISTRY} && \
        docker pull ${REGISTRY}/${DOCKER_IMAGE_NAME}:latest && \
        docker-compose down && \
        docker-compose up -d \
    "
    @echo "원격 서버 ${SERVER_HOST}에 배포 완료."
    

Makefiledocker-build로 이미지를 빌드하고, docker-push로 컨테이너 레지스트리에 푸시합니다. 그리고 deploy-local로 로컬에서 Docker Compose를 이용해 서비스를 실행하거나, deploy-remote로 SSH를 통해 원격 서버에 접속하여 최신 이미지를 받아와 서비스를 재시작합니다. deploy-remote 타겟은 docker-push에 의존하므로, 원격 배포 전 반드시 최신 이미지가 푸시되도록 보장합니다.

이러한 자동화된 배포 스크립트를 사용한 후, 배포에 소요되는 시간이 80% 이상 단축되었고, 배포 오류 발생률이 거의 제로에 가까워졌습니다. 이제 개발자는 배포 자체에 대한 걱정 없이 코드 작성에 집중할 수 있게 되었습니다.

CI/CD 파이프라인과의 연동

Makefile은 CI/CD 파이프라인의 핵심 구성 요소로 활용될 때 진가를 발휘합니다. Jenkins, GitLab CI, GitHub Actions 등의 CI/CD 도구에서 Makefile 타겟을 호출하는 방식으로 빌드, 테스트, 배포 단계를 쉽게 통합할 수 있습니다. 예를 들어, GitLab CI의 .gitlab-ci.yml 파일에서 다음과 같이 Makefile 타겟을 호출할 수 있습니다.


# .gitlab-ci.yml 예시
stages:
  - build
  - test
  - deploy

build_job:
  stage: build
  script:
    - make docker-build

test_job:
  stage: test
  script:
    - make test
    - make test-report
  artifacts:
    paths:
      - test-reports/

deploy_production_job:
  stage: deploy
  script:
    - make deploy-remote
  only:
    - main # main 브랜치에 푸시될 때만 실행
    

이처럼 Makefile은 CI/CD 파이프라인의 복잡성을 줄이고, 자동화된 워크플로우를 구축하는 데 있어 매우 유용한 도구입니다. CI/CD 환경과 로컬 개발 환경 모두에서 동일한 명령어로 빌드, 테스트, 배포를 실행할 수 있다는 점은 일관성을 유지하고 디버깅을 용이하게 하는 데 큰 장점입니다.

개발 생산성을 높이는 Makefile 활용 전략: 복잡한 빌드, 테스트, 배포 태스크 자동화 - turnip, vegetables, harvest, agriculture, nourishment, naturally, machine, fields, tuber, nature, floor, farmer, sugar beet, arable land, technology, vehicle, harvest time, fall, harvest, harvest, harvest, harvest, harvest

Image by Wolfgang-1958 on Pixabay

Makefile, 더 스마트하게 활용하기: 고급 팁과 모범 사례

Makefile은 단순한 명령어 실행 도구를 넘어, 변수, 함수, 조건문 등을 활용하여 더욱 강력한 자동화 도구로 거듭날 수 있습니다. 제가 프로젝트에서 유용하게 사용했던 몇 가지 고급 팁을 공유합니다.

  • 변수 활용: 반복되는 경로, 파일 이름, 명령어 옵션 등을 변수로 정의하여 관리하면 Makefile의 가독성이 높아지고 유지보수가 쉬워집니다. 위 예시들에서 APP_NAME, BUILD_DIR 등을 변수로 사용한 것을 볼 수 있습니다.
  • Shell 함수 사용: Makefile 내부에서 $(shell command) 구문을 사용하여 셸 명령의 결과를 변수에 할당할 수 있습니다. Git 커밋 해시를 가져오거나 현재 날짜/시간을 얻는 데 유용합니다.
  • 와일드카드 및 foreach 함수: 여러 파일에 대해 동일한 작업을 수행해야 할 때 $(wildcard pattern)$(foreach var,list,text) 함수를 함께 사용하면 매우 효율적입니다. 예를 들어, 특정 디렉토리의 모든 .go 파일에 대해 go fmt를 실행할 수 있습니다.
    
    GO_FILES := $(wildcard *.go)
    
    format:
        $(foreach file,$(GO_FILES),go fmt $(file);)
                
  • 조건문 (ifeq, ifneq): 특정 조건에 따라 다른 명령어를 실행하도록 할 수 있습니다. 예를 들어, 개발 환경과 프로덕션 환경에 따라 빌드 옵션을 다르게 가져갈 수 있습니다.
    
    ENV ?= dev # 기본값은 dev
    
    build:
    ifeq ($(ENV), prod)
        @echo "프로덕션 빌드..."
        go build -tags=production -o bin/app-prod ./cmd/main.go
    else
        @echo "개발 빌드..."
        go build -o bin/app-dev ./cmd/main.go
    endif
                
    make build ENV=prod와 같이 환경 변수를 넘겨줄 수 있습니다.
  • include 지시어: Makefile을 여러 파일로 분리하여 관리할 수 있습니다. 프로젝트가 커지면 Makefile도 길어지는데, 이를 기능별로 분리하면 관리가 훨씬 용이합니다. 예를 들어, Makefile.docker, Makefile.test 등으로 분리하고 메인 Makefile에서 include Makefile.*로 불러올 수 있습니다.

프로젝트 템플릿으로서의 Makefile

새로운 프로젝트를 시작할 때마다 Makefile을 미리 구성해두는 것은 개발 생산성을 높이는 좋은 방법입니다. 저는 개인적으로 자주 사용하는 빌드, 테스트, 클린, 배포 타겟들을 포함하는 Makefile 템플릿을 만들어두고, 새 프로젝트를 시작할 때마다 복사해서 사용합니다. 이렇게 하면 프로젝트마다 일관된 개발 워크플로우를 유지할 수 있고, 매번 자동화 스크립트를 작성하는 시간을 절약할 수 있습니다. 이는 특히 여러 프로젝트를 동시에 진행하는 경우에 빛을 발합니다.

마치며: Makefile, 개발자의 든든한 조력자

지금까지 Makefile을 활용하여 개발 생산성을 높이는 다양한 전략, 특히 복잡한 빌드, 테스트, 배포 태스크자동화하는 방법에 대해 저의 실전 경험을 바탕으로 이야기했습니다. 처음에는 Makefile 문법이 낯설고 어렵게 느껴질 수 있지만, 일단 익숙해지고 나면 이 작은 파일 하나가 얼마나 큰 생산성 향상을 가져다주는지 직접 체감하게 될 것입니다.

제가 Makefile을 사용하면서 가장 크게 느낀 점은 반복적인 수작업의 제거를 통해 개발자가 더 중요한 문제 해결에 집중할 수 있게 된다는 것입니다. Makefile은 단순히 명령어를 실행하는 도구가 아니라, 프로젝트의 워크플로우를 표준화하고, 팀원 간의 협업 효율을 높이며, 궁극적으로 소프트웨어의 품질을 향상시키는 데 기여하는 든든한 조력자입니다.

여러분의 프로젝트에도 Makefile을 적용하여 개발 생산성을 한 단계 끌어올려 보세요. 혹시 Makefile을 사용하면서 겪었던 재미있는 경험이나 유용한 팁이 있다면 댓글로 공유해 주세요! 함께 더 효율적인 개발 문화를 만들어 나갈 수 있기를 바랍니다.

📌 함께 읽으면 좋은 글

  • [생산성 자동화] 반복적인 코드 작성, 에디터/IDE 스니펫 및 매크로 자동화 전략으로 생산성을 극대화하는 방법
  • [이슈 분석] 생성형 AI 시대 개발자 역할 변화: 미래 커리어 전략 심층 분석
  • [생산성 자동화] 코드 스캐폴딩 자동화: 템플릿 기반 개발 워크플로우 가속화 전략 비교 분석

이 글이 도움이 되셨다면 공감(♥)댓글로 응원해 주세요!
궁금한 점이나 다루었으면 하는 주제가 있다면 댓글로 남겨주세요.

반응형