⟡ 인트로
신입으로 입사해 처음 맡은 고도화 프로젝트에서 폐쇄망 환경을 접했다.
일반적인 개발 환경과 달리 외부 인터넷 연결이 차단된 상태에서 Gradle 빌드를 설정해야 했고,
처음에는 단순히 설정만 바꾸면 될 줄 알았으나 의존성 관리의 복잡성을 직접 체감했음😳
수동 다운로드부터 캐시 복사까지 여러 시행착오를 거쳐 최종적으로 로컬 Maven 저장소 기반의 안정적인 빌드 환경을 구축했는데
이 글은 그 과정에서 겪은 실패와 해결 방법을 기록한 것이다. .
⟡ 개발환경
| AS-IS | TO-BE |
|---|---|
| JDK 1.7 | JDK 17 |
| Maven 2.9 | Gradle 8.8 |
| Spring Framework 4.2.5 (XML 기반) | Spring Boot 3.3.2 |
| MyBatis 3.3.0 | MyBatis 3.5.13 |
| Tomcat 7 | Embedded Tomcat |
| MariaDB | |
| JSP, EL, JSTL 기반 SSR(Server Side Rendering) | |
| HTML, CSS, JavaScript, jQuery | |
| Eclipse | IntelliJ or VSCode |
| Linux(사내 Cloud) |
⟡ 문제 상황

고도화 프로젝트를 진행하던 중 폐쇄망 환경에서 Gradle 빌드를 해야 하는 상황이 발생했음.
일반적인 개발 환경에서는 mavenCentral()을 통해 필요한 의존성을 자동으로 다운로드받지만,
인터넷이 안되는 폐쇄망에서는 외부 저장소 접근이 원천 차단되어 있었음. . . 😭
빌드를 실행하면 의존성을 찾을 수 없다는 에러만 반복적으로 발생했고, 이를 로컬 환경에서만 해결해야 했ㄷㅏ. . . .
⟡ 첫 번째 시도: 무작정 mavenLocal() 설정

처음에는 간단하게 생각했다 . .
build.gradle에 mavenLocal()만 추가하면 로컬 Maven 저장소(~/.m2/repository)를 참조할 것이라 판단한
💩인지 된장인지 찍어봐야 아는 이 어리석은 인간 ㅎ. .ㅎ..
repositories {
mavenLocal()
}
문제점: 당연하게도 실패
로컬 Maven 저장소에는 아무것도 없었기 때문.
외부 인터넷이 되는 환경에서 한 번도 빌드를 실행한 적이 없었기에 의존성이 다운로드되지 않은 상태였음.
단순히 설정만 바꾼다고 해결될 문제가 아니었음.
배운 점: 폐쇄망 환경에서는 사전 준비가 필수라는 것을 깨달았음.
의존성을 미리 확보해두지 않으면 어떤 설정도 소용없음.
⟡ 두 번째 시도: 인터넷 환경에서 의존성 수동 다운로드

외부 인터넷이 되는 개발 환경에서 필요한 jar 파일들을 Maven Repository 사이트를 통해 직접 다운로드함. .
프로젝트에서 사용하는 Spring Boot, Lombok 등의 라이브러리를 하나씩 찾아서 다운받았다.
다운받은 jar 파일들을 프로젝트 내부 libs 폴더에 넣고, build.gradle에서 직접 참조하도록 설정했다.
dependencies {
implementation files('libs/spring-boot-starter-web-3.x.x.jar')
implementation files('libs/lombok-1.x.x.jar')
// ... 기타 등등
}
문제점: 이 방법은 의존성 개수가 적을 때만 가능했음.
실제 프로젝트에서는 직접 명시한 의존성 외에도 각 라이브러리가 의존하는
전이 의존성(transitive dependencies)이 수십 개씩 존재했음.
예를 들어 Spring Boot 하나만 추가해도 그것이 의존하는
Spring Core, Jackson, Tomcat 등 수많은 라이브러리를 모두 수동으로 다운받아야 했음.
의존성 버전 충돌 문제도 직접 해결해야 했고, 관리가 불가능한 수준이었음.
배운 점: Gradle이 의존성 트리를 자동으로 관리해주는 이유를 체감했음.

v수동 관리는 현실적으로 불가능이야. 퉤이💦
⟡ 세 번째 시도: Gradle 캐시 디렉토리 복사

인터넷 환경에서 프로젝트를 한 번 빌드하면
~/.gradle/caches 디렉토리에 모든 의존성이 캐싱된다는 정보를 찾았음.
그래서 인터넷 되는 환경에서 프로젝트를 빌드한 후, ~/.gradle/caches 전체를 폐쇄망 환경으로 복사했다.
결과: 이 방법이 실질적으로 작동했음. !!!!!!!!!!!!
Gradle은 빌드 시 먼저 로컬 캐시를 확인하고, 없을 때만 원격 저장소에 접근하는 구조이기 때문.
캐시에 필요한 의존성이 모두 있으면 네트워크 연결 없이도 빌드가 가능했음.
하.지.만 이 방법도 한계가 있었음.
새로운 의존성을 추가하거나 버전을 업데이트할 때마다
인터넷 환경에서 빌드하고 캐시를 다시 복사해야 했음.

일회성 해결책에 가까웠음.
⟡ 최종 해결: 로컬 Maven 저장소 구축 + 체계적 관리

결국 가장 안정적인 방법은 로컬 Maven 저장소를 제.대.로 구축하는 것이었음.
(사실 모르는건 아니잖아. . . .😭)
- 인터넷 환경에서 의존성 준비
먼저 인터넷이 되는 환경에서 프로젝트를 빌드하면서~/.m2/repository에 모든 의존성을 다운로드 받았음
./gradlew build --refresh-dependencies
--refresh-dependencies 옵션을 사용해 최신 의존성을 강제로 다운로드했음.
이렇게 하면 Gradle이 의존성을 다운로드하면서 자동으로 Maven 로컬 저장소 형식으로 저장함.
- 로컬 저장소 복사 및 설정
~/.m2/repository디렉토리를 폐쇄망 환경으로 복사한 후,build.gradle에서 로컬 저장소를 최우선으로 참조하도록 설정했음.
repositories {
mavenLocal() // ~/.m2/repository 참조
// 폐쇄망 내부에 구축된 Nexus가 있다면 추가
maven {
url uri("http://internal-nexus/repository/maven-releases/")
}
}
저장소 순서가 중요했음. mavenLocal()을 가장 위에 두면 로컬부터 찾고, 없을 때만 다음 저장소를 확인함.
- 추가 의존성 관리 프로세스
새로운 의존성이 필요할 때는 다음 프로세스를 따랐음
- 개발 환경(인터넷 가능)에서
build.gradle에 의존성 추가
./gradlew build --refresh-dependencies실행
- 업데이트된
~/.m2/repository디렉토리를 폐쇄망으로 복사
- 폐쇄망 환경에서 빌드 확인
⟡ 번외: 그래도 안되면 ? 전이의존성 문제
전이 의존성 때문에 최종 해결 방법으로도 안된다면 ?
그냥 전이 의존성까지 무식하게 다 ~ ~ ~ ~ 긁어오는 task를 만들어서 긁어오면 된다.
이 방법은 정말로 최최최최최종 느낌이다.
전이 의존성 (구글에서 긁어옴)
Gradle 전이 의존성(Transitive Dependency)이란,
내가 직접 추가하지 않았지만, 내가 추가한 라이브러리가 의존하고 있는 다른 라이브러리(하위 라이브러리)까지 Gradle이 자동으로 가져와 빌드 및 실행에 포함시켜주는 현상을 말하며, 이는 의존성 관리를 단순화하지만 때로는 불필요한 라이브러리 포함으로 인해 빌드 크기나 충돌을 유발할 수도 있어 implementation과 api 설정으로 제어할 수 있습니다.
주요 개념
의존성(Dependency): 내 프로젝트가 다른 라이브러리나 모듈의 기능을 사용하기 위해 필요로 하는 관계.
전이 의존성: 라이브러리 A가 라이브러리 B를 의존하고, B가 다시 C를 의존할 때, A를 추가하면 Gradle이 자동으로 B와 C까지 가져오는 것.
Gradle에서의 관리
implementation: 의존성을 현재 모듈 내부에서만 사용하고, 해당 의존성을 사용하는 다른 모듈로 전이하지 않습니다 (전파하지 않음). 컴파일 시점에만 필요하고, 런타임에 노출될 필요가 없는 경우에 사용합니다.
api (과거 compile): 의존성을 현재 모듈뿐만 아니라, 이 모듈을 사용하는 다른 모듈까지 전이합니다 (전파함). API로 공개되는 클래스를 사용하는 라이브러리에 적합합니다.
runtimeOnly (과거 runtime): 컴파일 시점에는 필요 없고 런타임에만 필요한 의존성.
testImplementation, testRuntimeOnly: 테스트 코드 컴파일 및 실행 시에만 적용되는 의존성.
왜 중요한가?
편의성: 복잡한 의존성 그래프를 직접 관리할 필요 없이 Gradle이 알아서 해결해 줌.
성능/안정성: implementation을 적절히 사용하여 불필요한 전이를 막으면 빌드 속도를 높이고 의존성 충돌 가능성을 줄일 수 있음.
멀티모듈 프로젝트: 모듈 간 의존성 전파를 제어하여 컴파일 범위를 정확히 설정하는 데 필수적.
제미니랑 엎치락 뒤치락 싸워가며 전이 의존성까지 긁어오는 task를 만들었다.
나 처럼 질하는 다른 분들을 위해 질 덜하시라고 공유해본다 . . .
task populateLocalMavenRepo {
description = "Populates 'gradle-lib' with ALL dependencies AND BOMs (Transitively), including plugin artifacts."
group = "build"
outputs.dir(localMavenRepoDir)
doFirst {
println "Cleaning 'gradle-lib' directory..."
delete(localMavenRepoDir)
}
doLast {
if (!localMavenRepoDir.asFile.exists()) {
localMavenRepoDir.asFile.mkdirs()
}
println "Scanning for ALL resolvable dependencies (including BOMs and pluginClasspath)..."
def allResolvedArtifacts = []
// 0) pluginClasspath artifacts (플러그인 JAR 수집)
if (configurations.findByName('pluginClasspath') != null && configurations.pluginClasspath.canBeResolved) {
println "Scanning: ROOT:pluginClasspath"
configurations.pluginClasspath.resolvedConfiguration.lenientConfiguration.getArtifacts().each { artifact ->
allResolvedArtifacts << artifact
}
}
// 1) 루트 프로젝트 구성 스캔
project.configurations.each { config ->
if (config.canBeResolved) {
println "Scanning: ROOT:${config.name}"
config.resolvedConfiguration.lenientConfiguration.getArtifacts().each { artifact ->
allResolvedArtifacts << artifact
}
}
}
// 2) 하위 프로젝트 구성 스캔
subprojects.each { subproject ->
subproject.configurations.each { config ->
if (config.canBeResolved) {
println "Scanning: ${subproject.name}:${config.name}"
config.resolvedConfiguration.lenientConfiguration.getArtifacts().each { artifact ->
allResolvedArtifacts << artifact
}
}
}
}
def uniqueArtifacts = allResolvedArtifacts.unique { it.file.absolutePath }
println "\nFound ${uniqueArtifacts.size()} unique artifacts. Copying to 'gradle-lib'..."
uniqueArtifacts.each { artifact ->
def moduleVersion = artifact.moduleVersion.id
def groupPath = moduleVersion.group.replace('.', '/')
def artifactPath = moduleVersion.name
def versionPath = moduleVersion.version
def targetDir = new File(localMavenRepoDir.asFile, "$groupPath/$artifactPath/$versionPath")
targetDir.mkdirs()
def targetFileName = "${artifactPath}-${versionPath}${artifact.classifier ? "-${artifact.classifier}" : ""}.${artifact.extension}"
copy { from artifact.file; into targetDir; rename { fileName -> targetFileName } }
}
println "\nSUCCESS: Local Maven repository 'gradle-lib' populated."
println "You can now zip the project and move it to the closed network."
}
}
사용하는 방법은
1. build.gradle 파일에 task 소스를 추가한다.
2. 외부망 통신이 되는 PC에서 task를 돌려 프로젝트 안에 라이브러리를 구성한다.
명령어 : ./gradlew populateLocalMavenRepo
3. 해당 폴더 안에는 전이 의존성 라이브러리가 보통 들어가 있다.
그럼 이걸 압축시켜 폐쇄망 PC로 옮긴 다음 폐쇄망 넥서스에 등록을 해준다. 그럼 끗-
⟡ 배운 점
- Gradle 의존성 관리 구조 이해:
Gradle이 로컬 캐시 → 로컬 Maven → 원격 저장소 순서로 의존성을 찾는다는 것을 알게 되었음.
이 구조를 이해하니 폐쇄망 환경에서도 효율적으로 대응할 수 있었음.
- 전이 의존성의 복잡성: 한 개의 라이브러리가 수십 개의 다른 라이브러리에 의존한다는 것을 체감했음.
수동 관리가 불가능한 이유를 직접 경험했음.
- 사전 준비의 중요성: 폐쇄망 환경에서는 무엇이든 미리 준비해야 함.
"그때 가서 해결하면 되지"라는 생각은 통하지 않았음.
의존성뿐만 아니라 플러그인, Gradle Wrapper까지 사전에 확보해야 함.
- 회사 내부 저장소 구축 필요성: 장기적으로는 Nexus나 Artifactory 같은 내부 저장소를 구축하는 것이 가장 효율적임.
개인이 매번 파일을 복사하는 것보다 중앙화된 저장소를 통해 팀 전체가 의존성을 공유하는 것이 바람직함.