수동으로 APK나 AAB(Android App Bundle)를 빌드하고 배포하는 과정은 인적 오류(Human Error)가 발생하기 쉬운 병목 구간입니다. 로컬 머신의 환경 차이로 인해 "내 PC에서는 되는데 빌드 서버에서는 안 되는" 문제는 엔지니어링 팀의 리소스를 낭비하는 주범입니다. Jenkins와 같은 설치형 CI 도구는 높은 커스터마이징 자유도를 제공하지만, 유지보수 비용과 인프라 관리라는 트레이드오프가 존재합니다. 반면 GitLab CI/CD는 저장소와 통합된 파이프라인을 제공하여 인프라 관리 부담을 줄이면서도 컨테이너 기반의 격리된 빌드 환경을 보장합니다. 본 글에서는 JCenter 중단에 따른 최신 Gradle 저장소 설정 전략과 GitLab CI/CD를 이용한 안드로이드 프로덕션 레벨의 파이프라인 구축 방법을 다룹니다.
1. 레거시 청산과 Gradle 저장소 전략 재수립
CI/CD 파이프라인을 구축하기 전, 빌드 스크립트의 안정성을 확보해야 합니다. 과거 안드로이드 생태계에서 널리 쓰이던 JCenter(Bintray)는 서비스가 종료되었으며, 읽기 전용 모드조차 불안정한 상태입니다. 여전히 레거시 프로젝트에서는 jcenter()를 참조하고 있어 빌드 실패의 원인이 됩니다. 최신 Android Gradle Plugin(AGP) 7.0 이상에서는 settings.gradle (또는 settings.gradle.kts)에서 의존성 저장소를 중앙 관리하는 방식을 권장합니다.
build.gradle 파일 내의 allprojects { repositories { ... } } 블록은 권장되지 않으며, 최신 프로젝트 템플릿에서는 제거되었습니다. 의존성 해결 규칙은 dependencyResolutionManagement로 이관해야 합니다.
아래는 JCenter를 완전히 배제하고 Maven Central과 Google 저장소를 우선순위로 설정한 settings.gradle.kts의 구성 예시입니다. 이 설정은 CI 환경에서 불필요한 네트워크 타임아웃을 방지하고 빌드 신뢰성을 높입니다.
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
// 필요한 경우 사설 저장소(Nexus, Artifactory) 추가
// maven { url = uri("https://your-private-repo.com") }
}
}
rootProject.name = "AndroidArchitecture"
include(":app")
2. GitLab CI 아키텍처 및 Docker 환경 구성
GitLab CI/CD는 .gitlab-ci.yml 파일을 통해 파이프라인을 정의합니다. 안드로이드 빌드를 위해서는 Java Development Kit(JDK)와 Android SDK가 포함된 환경이 필요합니다. 이를 위해 GitLab Runner의 Executor로 Docker를 사용하는 것이 업계 표준입니다. VM 기반 방식보다 가볍고, 매 빌드마다 격리된 환경(Clean State)을 제공하기 때문입니다.
Docker 이미지 선정 전략
매 빌드마다 Android SDK를 다운로드하고 설치하는 것은 막대한 시간과 대역폭을 소모합니다. 따라서 필요한 도구들이 사전 설치된 Docker 이미지를 사용해야 합니다. 직접 Dockerfile을 작성하여 이미지를 관리할 수도 있지만, 유지보수 비용을 고려할 때 커뮤니티에서 검증된 이미지를 사용하는 것이 효율적입니다.
| 이미지 소스 | 장점 | 단점 | 추천 상황 |
|---|---|---|---|
| Jangrewe/gitlab-ci-android | 가장 널리 사용됨, 주기적 업데이트 | 이미지 크기가 큼 | 빠른 초기 구축 |
| CircleCI Android Image | 안정적임, 다양한 태그 제공 | CircleCI 의존적 설정 일부 존재 | 특정 SDK 버전 필요 시 |
| Custom Dockerfile | 필요한 SDK만 설치하여 최적화 가능 | 지속적인 유지보수(SDK 업데이트 등) 필요 | 대규모 조직, 보안 규정 엄격 |
3. 파이프라인 구현 및 캐싱 전략
CI 파이프라인의 핵심은 속도입니다. Gradle은 증분 빌드(Incremental Build)에 강력하지만, CI 환경은 매번 초기화되므로 캐싱(Caching) 설정이 필수적입니다. .gradle/wrapper와 .gradle/caches를 캐싱하여 의존성 다운로드 시간을 단축해야 합니다.
build.gradle이나 gradle-wrapper.properties 파일의 체크섬을 기반으로 생성합니다. 파일이 변경되었을 때만 캐시를 갱신하도록 설계하여 효율을 극대화합니다.
다음은 Lint 검사, 단위 테스트, Release 빌드를 수행하는 .gitlab-ci.yml의 전체 구성입니다.
image: jangrewe/gitlab-ci-android:latest
variables:
# Gradle의 사용자 홈 디렉토리를 프로젝트 내부로 지정하여 캐싱 용이성 확보
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
# JVM 메모리 설정 (OOM 방지)
JAVA_OPTS: "-Xmx2048m -Dfile.encoding=UTF-8"
cache:
key: ${CI_COMMIT_REF_SLUG} # 브랜치별 캐시 분리
paths:
- .gradle/wrapper
- .gradle/caches
stages:
- check
- build
- deploy
before_script:
- chmod +x ./gradlew
lintDebug:
stage: check
script:
- ./gradlew lintDebug
artifacts:
paths:
- app/build/reports/lint-results-debug.html
expire_in: 1 week
testDebug:
stage: check
script:
- ./gradlew testDebugUnitTest
artifacts:
reports:
junit: app/build/test-results/testDebugUnitTest/TEST-*.xml
assembleRelease:
stage: build
script:
# 키스토어 디코딩 및 빌드 (보안 섹션 참조)
- echo "$KEYSTORE_FILE_BASE64" | base64 -d > ./my-release-key.jks
- ./gradlew assembleRelease
artifacts:
paths:
- app/build/outputs/apk/release/app-release.apk
expire_in: 1 month
only:
- main
- tags
4. 보안: Keystore 서명 자동화
프로덕션 앱 배포를 위해서는 서명 키(Keystore)가 필요합니다. 하지만 Keystore 파일(.jks)과 비밀번호를 Git 저장소에 커밋하는 것은 심각한 보안 위반입니다. 이를 해결하기 위해 GitLab CI/CD의 CI/CD Variables 기능을 사용해야 합니다.
설정 방법
- GitLab Settings > CI/CD > Variables로 이동합니다.
- 다음 변수들을 Masked 및 Protected(필요시) 옵션과 함께 등록합니다.
KEYSTORE_PASSWORD: 키스토어 비밀번호KEY_ALIAS: 키 별칭KEY_PASSWORD: 키 비밀번호KEYSTORE_FILE_BASE64:base64로 인코딩된 .jks 파일 내용
로컬에서 .jks 파일을 base64로 인코딩하는 명령어는 다음과 같습니다 (Linux/Mac 기준):
base64 -i my-release-key.jks | pbcopy # Mac
base64 -w 0 my-release-key.jks # Linux
Gradle 설정(app/build.gradle.kts)에서는 환경 변수가 존재할 때만 서명 설정을 적용하도록 방어 로직을 작성합니다.
android {
signingConfigs {
create("release") {
// CI 환경 변수 또는 로컬 속성 사용
storeFile = file(System.getenv("KEYSTORE_LOCATION") ?: "local-keystore.jks")
storePassword = System.getenv("KEYSTORE_PASSWORD")
keyAlias = System.getenv("KEY_ALIAS")
keyPassword = System.getenv("KEY_PASSWORD")
}
}
buildTypes {
getByName("release") {
// 서명 설정이 유효한 경우에만 적용
if (System.getenv("KEYSTORE_PASSWORD") != null) {
signingConfig = signingConfigs.getByName("release")
}
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}
KEYSTORE_FILE 변수 타입(File Type)을 사용할 수도 있지만, 일부 Runner 환경에서 파일 권한 문제나 경로 인식 오류가 발생할 수 있습니다. Base64 인코딩 방식을 사용하면 문자열 형태로 안전하게 주입하고, 빌드 스크립트 실행 시점에만 일시적으로 파일을 생성하여 사용 후 삭제(Docker 컨테이너 소멸 시 자동 삭제)되도록 처리하는 것이 더 명시적입니다.
결론 및 제언
GitLab CI/CD를 활용한 안드로이드 파이프라인 구축은 초기 설정에 다소 시간이 소요되지만, 장기적으로 개발팀의 생산성을 비약적으로 향상시킵니다. 특히 Docker 기반의 격리된 빌드 환경은 "환경 불일치" 문제를 원천 차단하며, 코드 리뷰 단계에서 Lint와 유닛 테스트를 강제함으로써 코드 품질(Quality Assurance)을 높이는 게이트키퍼 역할을 수행합니다. 팀 규모가 커질수록 Gradle 캐싱 최적화와 Docker 이미지 경량화에 대한 지속적인 튜닝이 필요하며, 향후에는 Fastlane과 연동하여 Play Store 배포까지 완전 자동화하는 단계로 확장하는 것을 권장합니다.
Post a Comment