본문 바로가기
CI. CD/Jenkins

[Jenkins] Maven Gradle 빌드 캐시 최적화 빌드 시간 10배 단축의 비밀

"코드는 고작 한 줄 수정했는데, 젠킨스 빌드 시간은 무려 15분이나 걸려?"

자바(Java/Spring Boot) 프로젝트나 Node.js 프로젝트의 파이프라인을 운영하다 보면 가장 많이 듣게 되는 불만 중 하나입니다.
코드를 컴파일하고 테스트하는 데 걸리는 시간은 1분도 안 되지만, 스프링 프레임워크와 수많은 서드파티 라이브러리(.jar 파일들)를 Maven Central이나 원격 저장소에서 다운로드하는 데 14분이 걸리기 때문입니다.
특히 앞서 우리가 칭송했던 'Docker Agent(1회용 멸균 빌드 환경)'를 사용할 경우, 이 문제는 최악의 형태로 발현됩니다.
컨테이너가 뜰 때마다 백지상태이므로 매 빌드마다 수백 메가바이트의 라이브러리를 인터넷에서 새로 받아와야 하니까요.

 

이번 글에서는 인프라 병목의 주범을 완벽하게 색출하고, 볼륨 마운트(Volume Mount)를 통한 무적의 빌드 캐시(Cache) 재활용 전략에 대하여 다뤄보겠습니다.

1. Docker 구성의 단점

우리는 간섭 문제를 막기 위해 agent { docker { image 'maven:3.8-jdk-11' } } 처럼 컨테이너 기반으로 빌드하는 것을 최선으로 배웠습니다. 빌드가 끝나면 컨테이너는 rm(삭제) 되며 흔적 없이 사라집니다.

이것은 보안과 환경 격리 측면에서는 완벽하지만, '캐시(Cache)' 관점에서는 끔찍한 재앙입니다.
Maven은 라이브러리를 다운받으면 컨테이너 내부의 /root/.m2/repository 경로에 저장해 둡니다. 그런데 빌드가 끝나고 컨테이너가 삭제되면? 기껏 받아놓은 라이브러리들도 함께 우주 미아가 되어 증발해 버립니다.
다음 빌드 때 새로운 컨테이너가 뜨면, Maven은 "어? 라이브러리가 하나도 없네?"라며 또다시 10분에 걸쳐 인터넷에서 다운로드를 시작합니다. 이른바 무한 다운로드의 저주에 빠지는 것입니다.

 

2. 해결책: 볼륨 마운트(Volume Mount)를 통한 캐시 영속화

해결책은 의외로 간단합니다. 다운로드받은 라이브러리를 곧 삭제될 컨테이너 내부에 저장하지 말고, 젠킨스가 설치된 호스트 서버(물리 장비)의 안전한 디렉토리에 저장하도록 연결(Mount)해 주면 됩니다. 컨테이너가 삭제되더라도 호스트 서버에 저장된 .jar 파일들은 영원히 남아(영속화) 있게 됩니다. 다음번 빌드 때 새로 뜨는 컨테이너에게 "야, 인터넷에서 받지 말고 저기 호스트 서버 폴더에 있는 라이브러리들 그대로 가져다 써!"라고 다리를 놔주기만 하면 됩니다.

핵심 디렉토리 매핑 지식:

  • Maven (Java): 호스트 폴더 $HOME/.m2 ↔ 컨테이너 폴더 /root/.m2
  • Gradle (Java): 호스트 폴더 $HOME/.gradle ↔ 컨테이너 폴더 /home/gradle/.gradle
  • NPM (Node.js): 호스트 폴더 $HOME/.npm ↔ 컨테이너 폴더 /root/.npm


3. 실무 적용 극한의 예제 파헤치기

이러한 볼륨 마운트는 파이프라인의 docker 블록 내부에 args 속성을 사용하여 도커 런(Docker Run) 파라미터를 강제로 주입함으로써 달성할 수 있습니다.

pipeline {
    agent {
        docker {
            // Java 17 환경의 Maven 공식 이미지 사용
            image 'maven:3.8.5-openjdk-17'
            
            //  실무 핵심 최적화 라인 
            // -v 옵션으로 호스트의 ~/.m2 폴더와 컨테이너의 /root/.m2 폴더를 동기화(마운트)시킵니다.
            // 이렇게 하면 Maven이 다운받는 모든 파일이 호스트 서버의 디스크에 쌓이게 됩니다.
            args '-v $HOME/.m2:/root/.m2'
        }
    }
    stages {
        stage('Maven Build with Cache') {
            steps {
                echo ' 캐시가 적용된 초고속 빌드를 시작합니다...'
                
                // 주요 빌드: 호스트 폴더가 비어있으므로 인터넷에서 다운로드함 (약 10분 소요)
                // 다음 빌드: 호스트 폴더에 이미 파일이 꽉 차있으므로 다운로드 스킵! (약 30초 컷)
                sh 'mvn clean package -DskipTests'
            }
        }
    }
}

실무 꿀팁: 만약 Gradle을 쓴다면 args '-v $HOME/.gradle:/home/gradle/.gradle'로 변경해 주면 됩니다. 이렇게 단 한 줄의 args 속성을 추가하는 것만으로, 15분 걸리던 무거운 엔터프라이즈급 자바 프로젝트의 빌드 시간이 1분대로 드라마틱하게 단축되는 기적을 목격하실 수 있습니다.

 

4. 마무리 및 주요 발전 방향

지금까지 Docker Agent 환경의 최대 단점인 '캐시 증발 현상'을 정확히 분석하고, 볼륨 마운트(Volume Mount)를 통해 다운로드 시간을 0에 가깝게 소거하는 극강의 빌드 캐시 최적화 기법에 대해 다루어 보았습니다. 이제 개발자들은 커피 머신에 다녀오기도 전에 배포가 끝나는 쾌적한 CI/CD 속도에 감탄하며 인프라 팀을 찬양하게 될 것입니다.

이렇게 빠르고 완벽한 젠킨스 서버를 수년간 애지중지 키웠는데, 어느 날 회사에서 "기존 서버 장비를 폐기하고 최신형 클라우드 서버로 인프라를 이전(Migration)하라"는 미션이 떨어졌습니다. 수십 개의 파이프라인, 수백 명의 계정, 플러그인 세팅을 다운타임(Downtime) 없이 새로운 서버로 이사하려면 어떻게 해야 할까요? 새로 하나하나 다 손으로 세팅해야 할까요? 본 설명에서는 서버 이전의 두려움을 완벽히 없애주는 'Jenkins 무중단 데이터 마이그레이션(Migration) 전략과 rsync 동기화 비법'에 대해 아주 뼈 때리는 생생한 실무 노하우와 함께 파헤쳐 보겠습니다!


잘 굴러가던 젠킨스 서버를 3년쯤 운영하다 보면 반드시 거대한 장벽과 마주치게 됩니다. "서버 OS 버전이 너무 낮아서 더 이상 보안 패치를 받을 수 없다", "회사 방침으로 AWS에서 GCP로 클라우드를 통째로 이전해야 한다" 같은 거부할 수 없는 인프라 개편 지시가 떨어집니다. 이때 데브옵스 엔지니어의 머릿속은 하얗게 변합니다. 수백 개의 파이프라인 Job들, 정성껏 튜닝해 놓은 플러그인 설정들, 수많은 개발자들의 계정 정보와 보안 키(Credentials)를 새로운 서버에 똑같이 세팅하려면 몇 날 며칠을 밤새워야 할까요? 놀랍게도 정답은 "명령어 몇 줄이면 단 10분 만에 끝난다"입니다.

본 글에서는 인프라 엔지니어의 영원한 숙제인 구형 젠킨스 서버에서 신규 서버로 다운타임(Downtime)을 최소화하며 영혼(데이터)을 완벽하게 이식하는 마이그레이션 전략에 대하여 다뤄보겠습니다.

1. 젠킨스의 축복: 데이터베이스가 없다!

대부분의 웹 어플리케이션(게시판, 쇼핑몰 등)을 마이그레이션할 때는 MySQL이나 Oracle 같은 데이터베이스(DB)를 백업하고 이전하는 과정이 가장 고통스럽습니다. 하지만 젠킨스 아키텍처의 가장 위대한 설계적 특징 중 하나는 바로 별도의 RDBMS(관계형 DB)를 전혀 사용하지 않는다는 점입니다.

젠킨스의 모든 영혼(Job 설정, 플러그인, 계정, 빌드 히스토리)은 오직 $JENKINS_HOME (일반적으로 /var/jenkins_home) 폴더 내부의 텍스트 파일(XML)과 디렉토리들로만 저장됩니다. 즉, 구형 서버의 이 폴더를 통째로 복사해서 신규 서버의 동일한 경로에 덮어씌운 뒤 젠킨스 데몬을 기동하기만 하면, 신규 서버는 자기가 구형 서버인 줄 착각하고 100% 동일한 모습으로 살아나게 됩니다. 마이그레이션이 사실상 '폴더 복사(Copy)' 하나로 끝나는 매우 우아한 구조입니다.


2. 다운타임(Downtime) 제로를 향한 rsync 2-Step 전략

하지만 폴더 용량이 50GB라면 복사하는 데 1시간이 넘게 걸릴 수 있습니다. 그 1시간 동안 젠킨스를 멈춰두면 개발팀 전체의 배포가 중단되겠죠. 실무에서는 서비스 중단을 최소화하기 위해 rsync를 활용한 동기화(Delta Sync) 전략을 씁니다.

Step 1: 1차 백그라운드 복사 (서버 정상 가동 중)
개발자들이 평소처럼 배포를 하고 있는 와중에, 인프라 관리자는 몰래 백그라운드에서 전체 데이터를 신규 서버로 전송하기 시작합니다. 50GB를 넘기느라 1시간이 걸려도 서비스에는 아무 지장이 없습니다.

Step 2: 2차 델타(Delta) 동기화 및 트래픽 절체 (다운타임 3분 컷)
1차 복사가 끝난 늦은 밤, 젠킨스를 Quiet Down(더 이상 새 빌드를 받지 않고 대기하는 모드) 상태로 만듭니다. 그리고 다시 한번 rsync 명령어를 때립니다. 이때 rsync는 천재적이게도 전체 50GB를 다시 복사하는 게 아니라, 1차 복사 이후 1시간 동안 추가되거나 변경된 파일(Delta) 수십 MB만 찾아내어 빛의 속도로 덮어씁니다. 복사가 1분 만에 끝나면, 구형 서버를 끄고 신규 서버를 켜서 도메인(DNS)을 연결합니다. 개발자들은 젠킨스가 이사 갔다는 사실조차 눈치채지 못합니다.


3. 실무 적용 극한의 쉘 스크립트 파헤치기

백업 노하우에서 배웠듯, 쓰레기 파일들(Workspace 등)을 제외하고 영양가 있는 파일만 옮기는 것이 마이그레이션 시간 단축의 핵심입니다.

# [작업 1] 구형 서버(A)의 터미널에서 실행 - 1차 대용량 동기화 (서비스 무중단 상태)
# -a(아카이브 권한유지), -v(상세출력), -z(압축전송), -P(진행률 표시)
# --exclude 로 쓸데없는 찌꺼기 파일들은 이관 대상에서 제외시켜 버립니다.
echo "1차 마이그레이션 동기화를 시작합니다 (예상시간: 1시간)..."
rsync -avzP \
  --exclude 'workspace' \
  --exclude 'caches' \
  --exclude 'logs' \
  /var/jenkins_home/ root@192.168.2.200:/var/jenkins_home/

# =================================================================
# (1차 동기화 완료 후, 젠킨스 관리자 UI에서 [준비 상태로 전환(Quiet Down)] 클릭)
# 현재 돌고 있는 빌드가 모두 끝나기를 기다립니다. (빌드가 끝나면 젠킨스가 얼어붙음)
# =================================================================

# [작업 2] 구형 서버(A)의 터미널에서 재실행 - 2차 델타 동기화 (다운타임 시작)
# --delete 옵션을 추가하여, 그사이 구형 서버에서 삭제된 파일이 있다면 신규 서버에서도 날려줍니다 (완벽한 싱크).
echo "2차 차분(Delta) 동기화를 시작합니다 (예상시간: 1분)..."
rsync -avzP --delete \
  --exclude 'workspace' \
  --exclude 'caches' \
  /var/jenkins_home/ root@192.168.2.200:/var/jenkins_home/

# [작업 3] 신규 서버(B) 기동 및 마무리
echo "마이그레이션 완료! 신규 서버의 Jenkins 데몬을 기동합니다."
# DNS 설정(Route53 등)에서 jenkins.my-company.com 의 A 레코드 IP를 신규 서버로 변경합니다.

4. 마무리 및 주요 발전 방향

지금까지 데이터베이스가 없는 젠킨스의 고유한 파일 기반(File-based) 아키텍처를 십분 활용하여, rsync의 델타 동기화를 통해 다운타임을 단 1분으로 압축해 버리는 완벽한 마이그레이션(서버 이전) 전략에 대해 3,000자의 생생한 실무 노하우로 다루어 보았습니다. 이제 여러분은 젠킨스 인프라를 언제 어디로든(온프레미스에서 클라우드로, K8s로) 자유자재로 옮겨 심을 수 있는 조물주의 권능을 얻었습니다.

서버 이전도 무사히 끝났고 평화로운 나날이 계속될 줄 알았습니다. 그런데 어느 날 젠킨스 대시보드를 보니, 빌드 3개가 24시간째(1,440분) 진행률 10%에 멈춰서 영원히 끝나지 않고(Hang) 있는 끔찍한 현상을 목격하게 됩니다.
젠킨스의 소중한 워커 쓰레드(Thread)를 좀비처럼 점유하고 놔주질 않는 것이죠.

 

다음에는 "30분 안에 안 끝나면 무조건 강제로 제거해줘!" 파이프라인의 무한 대기 늪을 원천 차단하는 타임아웃(Timeout) 강제 종료 방어 구성에 대해 다룰 예정입니다.