InfoGrab
InfoGrab

Maven 빌드가 자꾸 429를 만난다면 - GitLab Virtual Registry로 의존성 에러 해결하기

Maven 빌드가 자꾸 429를 만난다면 - GitLab Virtual Registry로 의존성 에러 해결하기

··28 min read
Maven 빌드가 자꾸 429를 만난다면 - GitLab Virtual Registry로 의존성 에러 해결하기

폐쇄망 CI 환경에서 NAT 게이트웨이 뒤에 Maven 빌드를 돌리다 보면, 다른 빌드 도구에 비해 유독 자주 멈추는 패턴을 마주합니다. 빌드 로그를 확인하면 다음과 같은 에러가 반복적으로 표시됩니다.

javascript
[ERROR] Failed to transfer artifact org.springframework:spring-core:jar:6.1.10
  from/to central (https://repo1.maven.org/maven2):
  status code: 429, reason phrase: Too Many Requests (429)
[ERROR] 429 Your IP: [x.x.x.x] has hit the rate limit with Maven Central

여기서 429 Too Many Requests는 Maven Central이 클라이언트 IP에 rate limit을 적용했다는 신호입니다. NAT로 인한 IP 공유, Maven의 기본 병렬 다운로드, Maven Central의 점진적 consumption limits 도입이 맞물려 발생하는 증상이죠. 빌드를 재시도하면 통과되는 경우도 있어 원인을 곧바로 짚어내기는 쉽지 않습니다.

이 글에서는 폐쇄망 빌드의 의존성 수급 단계에서 발생하는 Maven 빌드 중단 문제를 GitLab Maven Virtual Registry로 해결한 과정과 실전 팁을 정리했습니다. PoC 환경에서 1주 안에 검증을 마쳤습니다.

폐쇄망에서 Maven 빌드가 429에 자주 막히는 이유

먼저 폐쇄망 CI 환경에서 NAT 게이트웨이 뒤에 Maven 빌드를 돌릴 때, 429 에러가 자주 발생하는 이유를 짚고 가겠습니다. Maven Central은 자원 보호를 위해 consumption limits 정책을 단계적으로 도입하고 있으며, 이 정책은 IP별 rate limit 형태로 구현돼 한도 초과 시 429 응답을 돌려줍니다. 원인은 세 가지로 정리할 수 있습니다.

1. NAT 게이트웨이가 외부 IP를 단일 IP로 묶음

NAT 게이트웨이가 외부에서 본 IP를 단일 IP로 묶는 점이 영향을 미칩니다. 회사 IDE와 CI 서버는 각자 다른 내부 IP를 쓰더라도, 외부 네트워크로 나갈 때는 모두 같은 외부 IP를 거칩니다.

이에 대해 Sonatype 공식 FAQ는 다음과 같이 설명합니다. 기업 NAT, VPN, 호스팅 CI, 클라우드 인프라처럼 여러 사용자나 조직이 같은 외부 IP를 공유하는 환경에서는 각자의 트래픽이 하나의 고용량 소비자처럼 묶여 보일 수 있습니다. 따라서 우리 프로젝트의 사용량 자체는 합리적 수준이더라도, 같은 외부 IP를 통해 나가는 다른 트래픽이 누적돼 rate limit 임계값을 초과할 수 있죠.

이 때문에 우리 프로젝트가 빌드 빈도를 조절하더라도, 같은 외부 IP를 공유하는 다른 팀의 트래픽 때문에 빌드가 갑작스럽게 429 에러로 중단될 수 있습니다.

2. Maven이 의존성 다운로드를 병렬 처리함

Maven 기본 설정이 의존성 다운로드를 여러 스레드로 병렬 처리하는 영향도 있습니다. Maven Resolver의 다운로드 스레드 수(aether.connector.basic.downstreamThreads, Resolver 2.0 이전 및 공용 설정은 aether.connector.basic.threads) 기본값은 5로, 한 빌드가 시작되면 최대 5개의 의존성 파일을 동시에 요청합니다.

따라서 폐쇄망 환경에서 개발자 한 명이 빌드를 한 번 실행하더라도, 같은 외부 IP에서 짧은 시간 안에 다수의 요청이 발생해 429에 막힐 가능성이 커집니다.

3. Maven Central의 rate limit이 동적 정책임

Maven Central의 rate limit이 동적 정책인 점도 영향을 줍니다. Maven Central은 IP별 부하에 따라 rate limit을 가변적으로 적용하므로, 예측 가능한 한도가 존재하지 않습니다. Docker Hub가 비인증 100회/6시간, 무료 Personal 계정(인증) 200회/6시간처럼 명문화된 등급별 pull quota를 적용하는 것과 대비됩니다.

이에 평소에는 정상적으로 통과되던 빌드가 어느 날 갑자기 429 에러로 중단되는 일이 발생합니다.

이러한 세 가지 원인은 폐쇄망 CI 환경에서 동시에 적용되기에 응급 대처보다 구조적인 해결책이 필요합니다. Sonatype은 공식 권고 해법을 다음과 같이 안내합니다. 저장소 관리자(repository manager)를 개발 환경과 Maven Central 사이에 두면 첫 요청만 공용 레지스트리에서 가져오고 이후 동일 요청은 캐시에서 제공할 수 있어 Maven Central이 보는 요청 수를 줄이고 향후 rate limit 차단까지 피할 수 있다는 것입니다. Sonatype은 자사의 Nexus Repository를 이 패턴의 구현 사례로 제시합니다.

정리하면, '캐싱 프록시(repository proxy) 도입'이 표준 처방입니다. 다만 새 솔루션을 도입하려면 비용과 운영 부담이 들고, 준비 일정도 필요하죠. 그러나 GitLab Premium 이상을 이미 사용하는 조직이라면, 첫 수를 다른 옵션으로 시도해 볼 여지가 있습니다. 지금부터 설명할 GitLab Maven Virtual Registry가 그 선택지입니다.

GitLab Maven Virtual Registry로 우선 해결하기

이어서 GitLab Maven Virtual Registry의 개념과 Nexus와의 비교, 폐쇄망 권장 아키텍처, Web UI 기반 설정 단계, 실제 PoC에서 확인한 결과를 차례로 다루겠습니다.

GitLab Maven Virtual Registry란 무엇인가

GitLab Maven Virtual Registry는 N개의 외부 Maven 레지스트리(upstream)를 단일 엔드포인트 뒤에 묶고, 한 번 받은 패키지를 캐싱하는 기능입니다. Maven Central, Apache Releases, 사내 비공개 저장소를 하나의 미러 URL로 통합할 수 있습니다.

이 기능은 Sonatype이 권고한 캐싱 프록시 패턴에 부합하는 GitLab 내장 기능입니다. 폐쇄망 CI가 Maven Central에 직접 요청하던 구조를 GitLab Virtual Registry 경유 구조로 바꾸면, 두 번째 요청부터는 GitLab 캐시가 응답을 제공합니다. 그 결과, 앞서 짚은 세 가지 429 원인(NAT IP 공유로 인한 트래픽 합산, Maven의 병렬 다운로드, 동적 rate limit)이 구조적으로 회피됩니다. 캐시 적중 시에는 외부망으로 요청이 나가지 않아 Maven Central의 IP별 한도와 무관해집니다.

현재 상태는 다음과 같이 정리할 수 있습니다.

항목상태
Virtual Registry 전체베타 (GitLab 18.11 기준, Premium/Ultimate)
지원 패키지 형식Maven, Container (npm/NuGet 등 미지원)
Cleanup PoliciesGA (18.6 도입 → 18.10 GA)
인증 토큰 스코프api 또는 read_virtual_registry
가상 레지스트리·upstream 최대 개수그룹당 Maven 가상 레지스트리 20개, 가상 레지스트리당 upstream 20개
관리 UI(Web)GitLab 18.5부터 제공 (이전 버전은 API 사용)

Nexus와의 비교

캐싱 프록시 패턴의 대표적인 외부 솔루션은 Sonatype Nexus Repository입니다. Nexus는 Maven Central을 비롯한 여러 패키지 레지스트리를 프록시·캐싱하는 저장소 관리자 제품군으로, Community Edition(무료)과 Pro(상용) 두 에디션이 있습니다. GitLab Premium 이상 환경에서 GitLab Maven Virtual Registry와 Nexus 중 어느 쪽이 더 적합한지는 도입 형식과 운영 부담을 함께 고려하고 판단하는 것이 합리적입니다.

다음 표는 두 솔루션을 동일 항목으로 비교한 결과입니다.

항목GitLab Maven Virtual RegistryNexus
추가 라이선스 비용0원 (Premium/Ultimate 보유 시)Community Edition 무료 (EPL 라이선스) / Pro 유상
신규 VM불필요최소 1대 권장 (small profile 기준 4 vCPU / 8GB RAM, 디스크는 아티팩트 양에 따라 가변)
권한·인증 연동기존 GitLab 그룹·토큰 상속LDAP 연동 가능 / SAML·SSO는 Pro 전용
도입 일정1주 안에 PoC 시작·검증 가능 (본 글의 검증 사례 기준)도입·연동까지 일반적으로 2~4주 소요
운영 부담기존 GitLab 운영에 흡수OS·런타임 패치, 백업 정책 별도
다중 형식 지원Maven, ContainerMaven, npm, NuGet, Docker, PyPI 등 다수

GitLab Premium 이상을 이미 사용하고 도입 대상이 Maven, Gradle, 또는 외부 컨테이너 레지스트리 캐싱 중심이며 사내 라이선스 게이팅까지 필요하지 않은 조직이라면, GitLab Maven Virtual Registry는 비용 0원·신규 VM 0대로 시작할 수 있는 합리적인 첫 선택입니다.

반면에 도입 형식이 Maven 외 다수에 걸쳐 있거나 베타 기능을 운영 환경에서 적용하기 어려운 조직이라면 처음부터 Nexus 검토가 합리적입니다. 후자에 해당하는 상세 조건은 글 말미의 부록에서 다루겠습니다.

폐쇄망 권장 아키텍처

폐쇄망에서 GitLab Maven Virtual Registry를 사용할 때는 다음 아키텍처 활용을 권장합니다. 외부망과 폐쇄망 사이에 DMZ를 두고, 외부와 통신할 수 있는 주체를 GitLab 인스턴스로만 제한하는 구조입니다. 이렇게 하면 보안 경계가 단순해지고, 폐쇄망 내부 클라이언트가 호출하는 엔드포인트도 GitLab Virtual Registry URL 하나로 통일됩니다.

다이어그램 로딩 중...

다이어그램은 세 계층으로 구성됩니다. 외부망에는 Maven Central, Apache Releases, Spring Repo 같은 공용 레지스트리와 사내 비공개 저장소가 위치합니다. DMZ에 둔 GitLab 인스턴스가 이들과의 outbound 통신을 전담하며, 내부에 Virtual Registry를 호스팅합니다. 폐쇄망의 CI Runner와 개발자 워크스테이션은 GitLab Virtual Registry URL만 호출합니다.

동작 흐름은 다음과 같습니다. 폐쇄망 클라이언트가 의존성을 요청하면, Virtual Registry는 등록된 upstream 목록을 우선순위 순서대로 탐색합니다. 캐시에 응답이 있으면 즉시 반환하고, 없으면 해당 upstream에서 가져와 캐싱한 후 반환합니다. 한 번 캐싱된 의존성은 외부망이 끊긴 상황에서도 빌드에 그대로 사용할 수 있습니다.

upstream 순서는 GitLab 공식 문서가 권고하는 우선순위 규칙(사내 비공개 레지스트리 우선, 빠르고 신뢰성 높은 레지스트리 상위 배치, 공용 레지스트리는 fallback 용도로 마지막 배치)에 따라 사내 비공개 → Apache Releases·Spring 등 → Maven Central(마지막) 순서로 정렬합니다. 우선순위가 높은 upstream에서 의존성이 누락된 경우 다음 upstream으로 탐색이 진행되므로, Maven Central을 마지막에 두지 않으면 사내 비공개 라이브러리보다 공용 라이브러리가 먼저 선택되는 사태가 발생할 수 있습니다.

Web UI로 설정하기

GitLab 18.5부터 제공되는 Web UI를 기준으로 설정 단계를 설명합니다. 이전 버전에서는 API로 동일 작업을 수행할 수 있으며, 전체 API 엔드포인트는 Maven 가상 레지스트리 API GitLab 한글 문서에서 확인할 수 있습니다.

  1. 레지스트리 생성. Top-level group에서 Deploy → Virtual registry 메뉴로 진입합니다. 처음에는 다음과 같이 빈 상태로 Create registry 버튼이 표시됩니다.

    Deploy → Virtual registry 메뉴 진입 시 빈 상태 화면 Deploy → Virtual registry 메뉴 진입 시 빈 상태 화면

    Maven을 선택한 후 Create registry 버튼을 누르면 신규 레지스트리 생성 폼으로 이동합니다. 이름과 설명(선택)을 입력한 후 레지스트리를 생성합니다.

    New maven virtual registry 폼 New maven virtual registry 폼

  2. Upstream 추가. 생성된 레지스트리 상세 페이지에서 Add upstream 버튼을 누릅니다.

    레지스트리 상세 페이지 레지스트리 상세 페이지

    Upstream 추가 폼에서 이름과 외부 레지스트리 URL(예: https://repo1.maven.org/maven2)을 입력합니다. 위 아키텍처에서 정리한 순서(사내 비공개 → Apache Releases·Spring 등 → Maven Central)대로 차례로 등록합니다.

    Upstream 추가 폼 Upstream 추가 폼

  3. 클라이언트 설정. Maven은 settings.xml에서 mirror로 설정합니다.

    xml
    <mirrors>
      <mirror>
        &lt;id&gt;central-proxy</id>
        &lt;name&gt;GitLab proxy of central repo</name>
        &lt;url&gt;https://gitlab.example.com/api/v4/virtual_registries/packages/maven/&lt;registry_id&gt;</url>
        &lt;mirrorOf&gt;central</mirrorOf>
      </mirror>
    </mirrors>
    

    인증은 api 또는 read_virtual_registry 스코프를 가진 토큰(개인 액세스 토큰, 그룹 액세스 토큰, 그룹 배포 토큰, CI/CD Job 토큰)으로 처리합니다. CI 환경에서는 CI Job 토큰이 가장 편리합니다.

실제 검증에서 확인한 내용

위 설정에 따라 구성한 폐쇄망 PoC 환경에서 다음 사항을 확인했습니다.

  • 전이 의존성(transitive dependency) 5단계까지 정상 해소. BUILD SUCCESS 결과를 확보했습니다. 일부 환경에서 보고되는 ‘Dependency Proxy 단독 사용 시 전이 의존성 미해소’ 이슈는 본 검증에서 재현되지 않았습니다. Dependency Proxy와 Virtual Registry는 캐시 저장 방식과 upstream 탐색 로직이 다른 별개 기능이므로, 동일 증상으로 묶어 판단해서는 안 됩니다.
  • 최소 구성으로도 일반적인 의존성 해소 가능. upstream 2개(Maven Central + Apache Releases) 구성만으로 일반적인 Spring·Gradle 의존성까지 모두 해소됐습니다.
  • 외부 장애에 대한 회복력 확보. 한 번 캐싱된 의존성은 Maven Central이 일시 정지되거나 CDN 장애를 겪는 상황에서도 빌드에 그대로 사용할 수 있습니다. Maven Central의 단발성 incident는 드물지 않으므로, 폐쇄망에서 외부 의존성을 직접 호출하는 구조는 그 자체로 위험 자산이라는 점은 짚어 둘 만합니다.
  • 초기 설정 소요 시간 약 2~3시간. 그룹 생성, upstream 2개 등록, 토큰 발급, 클라이언트 mirror 적용까지 포함한 수치입니다.
💡 전이 의존성(transitive dependency)이란?

내 프로젝트가 pom.xml이나 build.gradle에 직접 선언하지 않았지만, 선언한 라이브러리가 내부적으로 필요로 해서 자동으로 따라오는 의존성을 말합니다. 예를 들어, spring-boot-starter-web 하나만 선언해도 Tomcat, Jackson, Logback 등 수십 개의 라이브러리가 함께 끌려옵니다. 의존성 트리는 보통 3~5단계 깊이로 이어지며, 깊어질수록 누락된 한 개가 빌드 전체를 멈추는 위험도 커집니다.

📝 Maven Central의 가동 상태

Maven Central의 가동 상태는 공식 status 페이지에서 확인할 수 있습니다. 외부 모니터링 서비스 IsDown 기준 2024년 11월 이후 491건의 incident가 추적되며, 월평균 27.2건·평균 해결 시간 35분으로 보고됐습니다(2026년 5월 조회 기준).

GitLab Maven Virtual Registry 안정적으로 운영하기

GitLab Maven Virtual Registry를 도입·운영할 때 사전에 점검해야 할 사항이 있습니다. 공식 문서에 명시돼 있거나 PoC 운영에서 누락될 수 있는 포인트 위주로, 도입 단계에서 우선 확인해야 할 항목부터 순서대로 다루겠습니다.

  1. Virtual Registry와 Dependency Proxy 설정 활성화 여부를 함께 확인합니다. Virtual Registry의 캐시는 내부적으로 dependency_proxy 객체 스토리지 버킷을 사용하므로, 두 설정 모두 활성 상태여야 정상 동작합니다. GitLab 공식 문서는 두 설정 모두 기본 활성화 상태이지만 관리자가 비활성화할 수 있다고 명시하므로, 사용 전 두 항목의 활성화 여부를 반드시 확인합니다.
  2. Maven Central은 다른 upstream과 함께 구성하고, Virtual Registry 안에서 마지막 upstream에 배치합니다. Virtual Registry를 기본 레지스트리(Maven Central)의 대체(replacement) 방식으로 구성하는 경우, GitLab 공식 문서는 누락된 공용 의존성을 보완하기 위해 Maven Central을 Virtual Registry의 마지막 upstream으로 둘 것을 권고합니다. 본 PoC에서도 일부 Maven 플러그인이 Apache Releases에만 존재해 Maven Central 단독 구성 시 404 응답으로 빌드가 중단되는 사례를 확인했습니다. 공용 환경이라면 최소 2~3개 upstream을 함께 구성하는 편이 안정적입니다.
  3. Gradle을 사용하는 경우, 사내 CA 인증서를 빌드 환경에 사전 설치합니다. 사내 CA가 빌드 환경에 설치되지 않은 폐쇄망에서는 HTTPS 핸드셰이크 단계에서 PKIX path building failed 오류가 발생할 수 있습니다. 사내 CA 인증서를 Java truststore(cacerts)에 추가하거나 빌드 이미지에 사전 설치하는 방식이 표준 해법입니다. GitLab 공식 문서가 다루는 항목은 아니므로, 사내 인프라 정책에 맞춰 적용합니다.
  4. SBT(Scala 진영의 표준 빌드 도구)를 사용하는 경우, 인증 realm 이름이 정확히 일치해야 합니다. GitLab 공식 문서는 SBT 클라이언트에서 Credentials의 첫 번째 인자를 반드시 "GitLab Virtual Registry"로 지정해야 하며, Maven Virtual Registry가 보내는 Basic Auth realm 이름과 문자 단위로 일치해야 한다고 명시합니다. 한 글자라도 다르면 basic auth가 통과되지 않으므로 정확한 표기에 주의합니다.
  5. 추가로 인지해야 할 제약 사항이 있습니다. Geo 지원은 현재 미구현(추적 이슈: gitlab-org/gitlab#473033) 단계이며, proxy_download는 객체 스토리지 설정과 무관하게 강제 활성됩니다.

부록: Nexus가 더 적합한 경우

앞서 GitLab Premium 이상 환경에서 GitLab Maven Virtual Registry를 사용하는 게 합리적인 상황을 다뤘습니다. 다만 조직의 의존성 형식, 라이선스 게이팅 요구, 운영 SLA 등 조건에 따라 처음부터 Nexus(또는 Artifactory) 도입이 더 적합한 시나리오도 있죠. 다음 항목 중 하나라도 해당하면, Nexus를 사용하는 걸 고려해 볼 수 있습니다.

  • 외부 Docker 레지스트리 캐싱을 안정 단계의 GA 도구로 운영해야 하는 경우. GitLab 18.10 이상에서는 GitLab Container Virtual Registry가 gcr.io, quay.io, nvcr.io 같은 외부 컨테이너 레지스트리의 pull-through 캐싱을 지원하지만, 현재 베타 단계이며 그룹당 가상 레지스트리 5개, 가상 레지스트리당 upstream 5개라는 제약이 있습니다. AI·GPU 스택처럼 다수의 외부 컨테이너 레지스트리를 통합하거나, 베타 기능 운영 부담을 감수하기 어려운 환경에서는 Nexus가 더 적합합니다.
  • npm, NuGet, PyPI 같은 비-Maven·비-Container 폐쇄망 의존성을 캐싱해야 하는 경우. GitLab Virtual Registry는 현재 Maven과 Container만 지원하므로, 이들 형식은 지원 범위 밖이며 Nexus가 합리적인 선택입니다.
  • 라이선스·CVE 자동 게이팅이 즉시 필요한 경우. Sonatype Repository Firewall(Nexus 생태계, IQ Server 기반)과 JFrog Xray(Artifactory 생태계) 같은 전용 제품이 이 영역에서 성숙한 기능을 제공합니다.
  • 엄격한 운영 SLA로 베타 기능을 수용하기 어려운 경우. GA 단계의 도구가 안전한 선택입니다.

맺음말

폐쇄망 환경에서 Maven 429는 NAT 구조상 사실상 피할 수 없는 문제이므로, 캐싱 프록시는 선택이 아니라 필수에 가깝습니다. 다만, 그 프록시가 반드시 Nexus여야 한다는 법은 없습니다. GitLab Premium 이상을 사용한다면 1주짜리 PoC로 검증을 마치고 신규 VM 없이 시작할 수 있는 GitLab Maven Virtual Registry를 추천합니다.

한계는 분명합니다. 베타 단계이며 Maven과 Container만 지원하고, 관리 UI는 18.5 이후에 추가되었습니다. 위에서 정리한 Nexus 트리거 조건이 명확한 조직이라면 처음부터 Nexus 도입이 더 적합할 수 있습니다.

여러분도 환경에 맞춰 의존성 수급 구조를 점검하고, 가장 작은 비용으로 시작할 수 있는 옵션부터 검증해 보시길 바랍니다. 이 글이 폐쇄망 빌드의 안정성과 개발 생산성을 함께 끌어올리는 데 도움이 되길 기대합니다.

참고 자료

  1. Sonatype, "Maven Central 429 FAQ", https://central.sonatype.org/faq/429-error/
  2. Sonatype, "Maven Central and the Tragedy of the Commons", https://www.sonatype.com/blog/maven-central-and-the-tragedy-of-the-commons
  3. GitLab Docs, "Virtual registry", https://docs.gitlab.com/user/packages/virtual_registry/
  4. GitLab Docs, "Maven virtual registry", https://docs.gitlab.com/user/packages/virtual_registry/maven/
  5. 인포그랩, "Maven 가상 레지스트리 API (한글)", https://docs.infograb.net/docs/gitlab/18.10/api/maven_virtual_registries/
  6. GitLab Docs, "GitLab 18.11 released", https://docs.gitlab.com/releases/18/gitlab-18-11-released/
  7. Maven Central, "Incident History", https://status.maven.org/history
  8. IsDown, "Maven Central Status", https://isdown.app/status/maven-central
⚠️
해당 콘텐츠는 저작권법에 의하여 보호 받는 저작물로 기고자에게 저작권이 있습니다.
사전 동의 없이 2차 가공 및 영리적인 이용을 금하며, 온·오프라인에 무단 전재 또는 유포할 수 없습니다.

이 글이 도움이 되셨나요?

인포그랩 전문가가 맞춤 상담을 도와드립니다.

InfoGrab Infoletter

최신 DevOps·AI 트렌드를 매달 받아보세요