본문 바로가기
CI.CD/Jenkins

[Jenkins] 병렬(Parallel) 빌드로 파이프라인 속도 혁신하기

반응형

파이프라인을 성공적으로 구축하고 자동화의 기쁨을 누리는 것도 잠시, 프로젝트의 덩치가 커지면서 빌드 시간이 점점 늘어나기 시작합니다. 프론트엔드 빌드 5분, 백엔드 스프링 빌드 5분, 유닛 테스트 5분, E2E 테스트 10분... 코드를 하나 푸시하고 나서 무려 25분을 기다려야 배포가 완료된다면 개발팀의 생산성은 바닥을 칠 것입니다. 이번 [Jenkins 실무 완벽 가이드] 시리즈에서는 "시간이 돈"인 현대 소프트웨어 개발 환경에서 가장 적은 노력으로 빌드 시간을 드라마틱하게 반토막 내버리는 '병렬(Parallel) 처리 기법'에 대하여 3,000자 분량으로 아주 치밀하게 딥다이브 해보겠습니다.

⏳ 1. 직렬 빌드의 치명적인 병목 현상

기본적으로 젠킨스의 stages 블록 안에 적힌 stage들은 위에서 아래로 순차적(직렬)으로 실행됩니다. 앞의 작업이 끝나야만 뒤의 작업이 시작될 수 있죠. 그런데 곰곰이 생각해 봅시다. 프론트엔드 React 코드를 압축(Build)하는 작업과 백엔드 Java 코드를 테스트하는 작업은 서로 아무런 연관성(의존성)이 없습니다. 프론트 빌드가 끝날 때까지 백엔드가 얌전히 기다려줄 이유가 전혀 없다는 뜻입니다.

만약 서로 독립적인 3개의 작업이 각각 10분씩 걸린다면, 직렬 처리 시 총 30분이 걸립니다. 하지만 이 3개를 동시에(병렬로) 출발시킨다면? 가장 늦게 끝나는 10분 만에 3개의 작업이 모두 완료되는 마법이 펼쳐집니다. 이것이 바로 parallel 지시어의 위력입니다.


🚀 2. Parallel 블록의 기본 구조와 문법

Declarative Pipeline에서 병렬 처리를 구현하는 방법은 직관적입니다. stage 내부에서 steps 대신 parallel 블록을 선언하고, 그 안에 또 다른 stage들을 나란히 나열해 주면 됩니다.

pipeline {
    agent any
    stages {
        stage('Frontend & Backend Parallel Build') {
            // 이 블록 안의 stage들은 동시에(동시다발적으로) 실행됩니다.
            parallel {
                stage('React Build') {
                    steps {
                        echo "프론트엔드 빌드 시작..."
                        sh 'npm run build' // 3분 소요
                    }
                }
                stage('Spring Boot Test') {
                    steps {
                        echo "백엔드 단위 테스트 시작..."
                        sh './gradlew test' // 5분 소요
                    }
                }
                stage('Code Quality Check') {
                    steps {
                        echo "정적 분석 시작..."
                        sh 'sonar-scanner' // 2분 소요
                    }
                }
            }
        }
        
        stage('Deploy') {
            // 위의 3개 병렬 작업이 *모두* 끝난 5분 뒤에야 비로소 이 스테이지가 시작됩니다.
            steps {
                echo "🎉 병렬 작업 완료! 운영 배포 시작!"
            }
        }
    }
}

이전 18단계에서 다룬 Blue Ocean UI에서 위 코드를 돌려보면, 하나의 스테이지에서 3가닥으로 선이 갈라지며(Fork) 예쁜 병렬 노드 그래프가 그려지는 것을 확인할 수 있습니다.


⚡ 3. 실무 필수 옵션: failFast true (가장 빠른 손절)

병렬 처리를 할 때 초보자들이 놓치는 가장 중요한 옵션이 바로 failFast 입니다. 위의 예제에서 3개의 작업이 병렬로 돌고 있다고 가정해 봅시다. 그런데 시작하자마자 1분 만에 'React Build'에서 치명적인 에러가 발생해 빨간불이 떴습니다. 프론트엔드가 터졌으니 어차피 최종 배포(Deploy)는 불가능합니다. 그렇다면 나머지 'Spring Boot Test'와 'Code Quality Check'를 굳이 남은 4분 동안 끝까지 땀 뻘뻘 흘리며 돌릴 필요가 있을까요?

아닙니다. 즉시 나머지 병렬 작업들을 강제로 죽여버리고(Kill) 서버 리소스(CPU/Memory)를 반환해야 합니다. 이를 위해 failFast true 옵션을 선언합니다.

stage('Parallel Tests') {
    // 병렬 작업 중 단 하나라도 실패하면, 나머지 돌고 있는 작업들을 즉시 Abort 시킵니다.
    failFast true 
    
    parallel {
        stage('Unit Test') { ... }
        stage('E2E Test') { ... }
        stage('Load Test') { ... }
    }
}

⚠️ 4. 주의사항: 리소스 폭발(OOM) 방어 전략

"우와! 그럼 10개 작업을 전부 병렬로 돌리면 1분 만에 빌드가 다 끝나겠네요?"라고 생각하실 수 있습니다. 하지만 현실은 냉혹합니다. 병렬 처리는 시간을 줄이는 대신 서버의 CPU와 메모리를 그만큼 동시에 갉아먹습니다.

만약 메모리가 4GB밖에 안 되는 젠킨스 서버에서 무거운 Spring Boot 컴파일 3개를 병렬로 돌려버리면 어떻게 될까요? 서버가 버티지 못하고 OOM(Out Of Memory) 에러를 뿜으며 즉시 뻗어버립니다. 따라서 병렬 처리를 도입할 때는 반드시 젠킨스 워커(Worker) 노드의 스펙이 넉넉한지 모니터링해야 합니다. 실무에서는 이러한 리소스 부족 문제를 해결하기 위해, 아예 병렬 스테이지마다 agent 지시어를 별도로 선언하여 서로 다른 물리적 서버(또는 다른 K8s 파드)로 작업을 분산 할당(Routing)시키는 고급 기법을 사용하기도 합니다.


🎯 5. 마무리 및 다음 단계

지금까지 파이프라인의 숨겨진 시간 단축 치트키인 `parallel` 블록의 구조와 리소스 낭비를 막는 `failFast` 옵션의 100% 활용법에 대해 3,000자 분량으로 밀도 있게 다루어 보았습니다. 이제 여러분의 파이프라인은 앞만 보고 달리던 경주마에서, 양손 양발을 모두 사용하여 작업을 순식간에 끝내버리는 문어발로 진화했습니다.

하지만 속도만 빠르다고 장땡이 아닙니다. 만약 병렬로 돌고 있는 쉘 스크립트 하나가 무한 루프(Infinite Loop) 버그에 빠지면 어떻게 될까요? 파이프라인은 성공도 실패도 하지 않은 채 몇 시간이고 멈춰서(Hang) 서버 리소스를 갉아먹게 됩니다. 또는 외부 API가 잠깐 끊겼다고 바로 실패(Fail) 처리를 해버리면 너무 억울하겠죠. 이어지는 20단계 포스팅에서는 "30분 지나면 강제 종료해라!", "실패하면 3번까지만 다시 시도해봐라!"라고 젠킨스에게 명령하는 극강의 안정성 확보 기법, 'Timeout과 Retry 제어'에 대해 아주 치밀하게 파헤쳐 보겠습니다!

반응형