폐쇄망 Nexus에 의존성을 올렸는데도 빌드가 실패했다. 원인은 mvn dependency:list가 parent POM을 출력하지 않는다는 점이었다. 이 함정과 해결 방법을 정리한다.
1. 증상
로컬 .m2에서 의존성을 수집해 Nexus에 올리고 폐쇄망 TeamCity 빌드를 돌렸더니 아래 에러가 떴다.
Could not resolve dependencies for project ...
Could not find artifact com.github.librepdf:openpdf-parent:pom:1.3.30.jaspersoft.2
in nexus (http://nexus.internal:8081/repository/maven-public/)
openpdf는 분명히 올렸는데 왜 openpdf-parent가 없다고 할까.
2. 원인 — mvn dependency:list의 함정
Maven 빌드 시 각 아티팩트의 POM을 읽어서 <parent> 태그를 따라 부모 POM도 함께 다운로드한다. 그런데 mvn dependency:list는 실제 빌드에 사용되는 JAR만 출력하고, 모델 해석에 필요한 parent POM은 목록에 포함시키지 않는다.
예를 들어 openpdf JAR를 올렸더라도, Maven이 openpdf.pom을 읽어보면 아래와 같다.
<parent>
<groupId>com.github.librepdf</groupId>
<artifactId>openpdf-parent</artifactId>
<version>1.3.30.jaspersoft.2</version>
</parent>
이 parent POM까지 Nexus에 있어야 빌드가 성공한다. 하지만 mvn dependency:list에는 openpdf-parent가 전혀 나타나지 않는다.
3. 해결 — POM의 parent 체인을 재귀적으로 탐색
수집 스크립트에 Step 4를 추가했다. 이미 수집된 모든 POM 파일을 파싱해서 <parent> 요소를 찾고, 해당 parent POM도 .m2에서 복사한다. 복사한 parent POM에서 또 parent를 찾는 식으로 BFS(너비 우선 탐색)로 체인 전체를 추적했다.
# POM에서 <parent> 정보 추출
function Get-PomParent($pomPath) {
try {
$doc = New-Object System.Xml.XmlDocument
$doc.Load($pomPath) # [xml] 캐스트 대신 XmlDocument.Load() 사용 (인코딩 안전)
$node = $doc.DocumentElement.SelectSingleNode("*[local-name()='parent']")
if (-not $node) { return $null }
$g = $node.SelectSingleNode("*[local-name()='groupId']").InnerText.Trim()
$a = $node.SelectSingleNode("*[local-name()='artifactId']").InnerText.Trim()
$v = $node.SelectSingleNode("*[local-name()='version']").InnerText.Trim()
if ($g -and $a -and $v) { return @($g, $a, $v) }
} catch {}
return $null
}
# BFS로 parent 체인 전체 수집
$seenParents = New-Object 'System.Collections.Generic.HashSet[string]'
$parentQueue = New-Object 'System.Collections.Generic.Queue[object]'
# 이미 수집된 POM에서 parent 목록 추출해서 큐에 넣기
$pomFiles = Get-ChildItem -Path $DEST_DIR -Recurse -Filter "*.pom"
foreach ($pf in $pomFiles) {
$par = Get-PomParent $pf.FullName
if ($par) {
$key = "$($par[0]):$($par[1]):$($par[2])"
if ($seenParents.Add($key)) {
$parentQueue.Enqueue([PSCustomObject]@{ GroupId=$par[0]; ArtifactId=$par[1]; Version=$par[2] })
}
}
}
# BFS 실행
while ($parentQueue.Count -gt 0) {
$p = $parentQueue.Dequeue()
$srcPath = Find-M2Path -base "$M2_DIR\$groupPath\$($p.ArtifactId)" -version $p.Version
if ($srcPath) {
Copy-Item -Path "$srcPath\*" -Destination $dstDir -Recurse -Force
Write-Host "[PARENT] $($p.GroupId):$($p.ArtifactId):$($p.Version)"
# 복사한 POM의 parent도 큐에 추가
$copiedPom = Get-ChildItem -Path $dstDir -Filter "*.pom" | Select-Object -First 1
$par = Get-PomParent $copiedPom.FullName
if ($par -and $seenParents.Add("$($par[0]):$($par[1]):$($par[2])")) {
$parentQueue.Enqueue(...)
}
}
}
4. 삽질 포인트 — [xml] 캐스트 vs XmlDocument.Load()
처음에는 PowerShell의 [xml]$doc = Get-Content $pomPath로 파싱했는데 Parent POMs copied: 0이라는 결과가 계속 나왔다.
원인은 두 가지였다.
첫째, Get-Content는 파일 내용을 라인 단위 배열로 반환한다. [xml] 캐스트에 넣으면 첫 번째 라인만 파싱되거나 인코딩 문제로 파싱 자체가 실패할 수 있다.
둘째, POM 파일 인코딩이 UTF-8 BOM 없이 저장된 경우 Get-Content의 기본 인코딩(UTF-16 LE)과 충돌한다.
XmlDocument.Load()는 파일 경로를 직접 받아서 XML 선언의 인코딩(<?xml ... encoding="UTF-8"?>)을 자동으로 처리하므로 훨씬 안전하다.
# 위험 (인코딩 문제 가능)
[xml]$doc = Get-Content $pomPath
# 안전
$doc = New-Object System.Xml.XmlDocument
$doc.Load($pomPath)
5. 결과
| 상태 | 수 |
| JAR 의존성 (COPY) | 169 |
| system scope (LOCAL) | 3 |
| Parent POM 수집 | 55 |
총 55개의 parent POM이 누락되어 있었다.
이걸 올리고 나서야 폐쇄망 빌드가 정상적으로 진행됐다.
mvn dependency:list만 믿으면 parent POM은 반드시 누락된다. JAR 수집 후 POM을 재귀적으로 파싱해서 parent 체인까지 챙겨야 한다.
'Java > Maven' 카테고리의 다른 글
| [Troubleshooting] Maven "was cached in the local repository" — 폐쇄망 빌드 서버에서 캐시된 실패 처리 (0) | 2026.06.03 |
|---|---|
| [폐쇄망 CI/CD] Maven 의존성을 Nexus에 자동 업로드하기 (0) | 2026.06.03 |
| [Troubleshooting] Spring Boot 2.3 + JEUS 로그 라이브러리 충돌 해결 (0) | 2026.05.05 |
| [Maven] 외부망에서 .m2 복사했는데 내부망 빌드 실패할 때 (0) | 2026.05.05 |
| [Maven] clean / package / --offline / -U 실무 정리 (0) | 2026.05.05 |