Wednesday, May 16, 2018

안드로이드 개발 자동화의 실전: GitLab CI/CD와 최신 Gradle 설정 가이드

현대적인 안드로이드 애플리케이션 개발에서 CI/CD(Continuous Integration/Continuous Deployment) 파이프라인의 구축은 선택이 아닌 필수적인 요소로 자리 잡았습니다. 코드의 변경사항을 자동으로 빌드하고, 테스트하며, 배포하는 이 일련의 과정은 개발 생산성을 극대화하고, 버그를 조기에 발견하며, 안정적인 서비스 운영을 가능하게 합니다.

시중에는 Jenkins, Travis CI, CircleCI 등 다양한 CI/CD 도구들이 존재합니다. 하지만 설치 및 설정의 복잡성, 높은 유지보수 비용, 별도의 서버 운영 부담 등으로 인해 소규모 팀이나 개인 개발자들이 도입하기에는 진입 장벽이 높은 것이 현실입니다. 이러한 고민에 대한 훌륭한 대안으로 GitLab CI/CD가 부상하고 있습니다. GitLab은 소스 코드 저장소와 CI/CD 도구를 완벽하게 통합하여, .gitlab-ci.yml이라는 단 하나의 설정 파일만으로 강력하고 유연한 자동화 파이프라인을 구축할 수 있는 환경을 제공합니다.

본격적인 CI/CD 파이프라인 구축에 앞서, 우리는 반드시 안정적이고 최적화된 빌드 환경을 갖추어야 합니다. 그 중심에는 바로 Gradle 빌드 시스템과 의존성 라이브러리를 가져오는 '저장소(Repository)' 설정이 있습니다. 이 글에서는 더 이상 사용되지 않는 JCenter의 시대를 지나 최신 안드로이드 개발 환경에 맞는 Gradle 저장소 설정법을 명확히 정리하고, 이를 바탕으로 GitLab CI/CD를 활용하여 안드로이드 앱의 빌드, 테스트, 서명, 배포 준비까지의 전 과정을 자동화하는 실전적인 방법을 상세하게 다룰 것입니다.


1. 최신 안드로이드 프로젝트를 위한 Gradle 저장소 설정

과거 안드로이드 프로젝트의 build.gradle 파일을 열어보면 jcenter()라는 코드를 흔하게 볼 수 있었습니다. 하지만 시대가 변하면서 Gradle 저장소 설정 전략도 크게 바뀌었습니다. 올바른 저장소 설정은 빌드 속도와 안정성에 직접적인 영향을 미칩니다.

1.1. JCenter의 시대, 그리고 그 이후: 왜 더 이상 사용하지 않는가?

JCenter는 Bintray에서 운영하던 자바 라이브러리 저장소로, 한때 MavenCentral의 대안이자 확장판으로 큰 인기를 누렸습니다. 사용이 간편하고, MavenCentral에 있는 대부분의 라이브러리를 포함하고 있어 많은 개발자가 기본 저장소로 채택했습니다.

하지만 운영사인 JFrog는 2022년 2월 1일부로 JCenter 서비스를 완전히 종료했습니다. 이는 더 이상 jcenter()를 통해 새로운 라이브러리를 다운로드하거나 기존 라이브러리를 업데이트할 수 없음을 의미합니다. 만약 아직도 프로젝트의 settings.gradle 또는 루트 build.gradle.kts 파일에 jcenter()가 남아있다면, 빌드 시 불필요한 네트워크 요청으로 인해 빌드 시간이 지연되거나, 특정 라이브러리를 찾지 못해 빌드 실패의 원인이 될 수 있습니다.

따라서 현재 진행하는 모든 안드로이드 프로젝트에서는 jcenter() 선언을 즉시 제거해야 합니다.

// 예전 방식 (settings.gradle 또는 루트 build.gradle) - 지금은 제거해야 합니다!
// DO NOT USE THIS CONFIGURATION
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        jcenter() // <-- 이 줄을 반드시 제거해야 합니다.
    }
}

1.2. 현대적인 저장소 구성: `google()`과 `mavenCentral()`

JCenter가 사라진 지금, 안드로이드 개발의 표준 저장소는 google()mavenCentral() 두 가지입니다.

  • google(): Google이 직접 제공하는 Maven 저장소입니다. AndroidX 라이브러리, 머티리얼 디자인 컴포넌트, Firebase, Google Play 서비스 등 안드로이드 개발에 필수적인 공식 라이브러리들이 모두 이곳을 통해 배포됩니다. 따라서 안드로이드 프로젝트라면 반드시 최우선으로 포함해야 하는 저장소입니다.
  • mavenCentral(): 수십만 개의 오픈소스 라이브러리가 등록된 중앙 저장소입니다. Kotlin 관련 라이브러리, Square사의 OkHttp, Retrofit, GSON 등 서드파티 라이브러리의 대부분이 MavenCentral을 통해 제공됩니다.

따라서 현대적인 안드로이드 프로젝트의 settings.gradle 파일 내 저장소 설정은 아래와 같이 구성하는 것이 가장 이상적입니다. Gradle은 선언된 순서대로 저장소에서 라이브러리를 찾으므로, 안드로이드 공식 라이브러리를 더 빨리 찾기 위해 google()mavenCentral()보다 먼저 선언하는 것이 좋습니다.

// 최신 권장 방식 (settings.gradle)
// LATEST RECOMMENDED CONFIGURATION
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()       // 1순위: 안드로이드 공식 라이브러리
        mavenCentral() // 2순위: 대부분의 서드파티 오픈소스 라이브러리
        // 필요한 경우, 특정 라이브러리를 위한 별도 저장소 추가 (예: JitPack)
        maven { url 'https://jitpack.io' }
    }
}

때때로 일부 라이브러리는 JitPack과 같은 특정 저장소를 통해서만 배포되기도 합니다. 이런 경우에는 위 예시처럼 maven { url '...' } 형식을 사용하여 필요한 저장소를 추가할 수 있습니다.


2. GitLab CI/CD를 활용한 안드로이드 빌드 자동화 구축

안정적인 Gradle 설정이 준비되었다면, 이제 GitLab CI/CD를 통해 개발 워크플로우를 자동화할 차례입니다. 우리는 프로젝트 루트 디렉터리에 .gitlab-ci.yml 파일을 생성하여 파이프라인의 모든 동작을 코드로 정의할 것입니다.

2.1. 왜 GitLab CI/CD를 선택해야 하는가?

  • 완벽한 통합: 소스 코드 관리와 CI/CD가 하나의 플랫폼에서 이루어지므로 별도의 연동 작업이 필요 없습니다. Merge Request, 브랜치, 커밋 등 GitLab의 모든 기능과 긴밀하게 연동됩니다.
  • 쉬운 시작: .gitlab-ci.yml 파일 하나만 프로젝트에 추가하면 즉시 CI/CD 파이프라인이 동작합니다. 직관적인 YAML 문법으로 복잡한 워크플로우도 쉽게 정의할 수 있습니다.
  • 비용 효율성: GitLab.com은 공개 프로젝트는 물론 비공개 프로젝트에도 매월 상당한 양의 무료 CI/CD 실행 시간(CI/CD minutes)을 제공하여, 대부분의 중소규모 프로젝트는 추가 비용 없이 사용할 수 있습니다.
  • 유연한 실행 환경(Runner): GitLab이 제공하는 공유 Runner를 사용하거나, 특수한 환경이 필요할 경우 직접 서버를 구성하여 특정 Runner(Self-hosted Runner)를 연결할 수도 있습니다. 안드로이드 빌드는 리소스를 많이 소모하므로, 필요에 따라 강력한 성능의 자체 Runner를 구축하는 것도 좋은 전략입니다.

2.2. `.gitlab-ci.yml` 핵심 구조와 안드로이드 빌드 환경 설정

안드로이드 프로젝트를 위한 GitLab CI/CD 파이프라인은 크게 '정적 분석(Lint)', '유닛 테스트(Unit Test)', 'APK 빌드(Build)'의 3단계로 구성하는 것이 일반적입니다. 아래는 이를 구현한 전체 .gitlab-ci.yml 파일의 예시이며, 각 섹션을 상세히 분석하겠습니다.


# 사용할 Docker 이미지를 지정합니다. 안드로이드 빌드에 필요한 도구가 설치된 이미지를 사용하면 편리합니다.
# https://github.com/mobile-dev-tools/android-sdk-image
image: mobiledevops/android-sdk-image:34.0.0

# 파이프라인의 전체적인 실행 단계를 정의합니다. 선언된 순서대로 Job이 실행됩니다.
stages:
  - lint
  - test
  - build
  - signed_build # 릴리즈 빌드를 위한 추가 단계

# 전역 변수를 선언합니다. 모든 Job에서 이 변수를 사용할 수 있습니다.
variables:
  # Gradle이 사용하는 메모리 옵션 설정. CI 환경에 맞게 조절합니다.
  GRADLE_OPTS: "-Dorg.gradle.daemon=false -Xmx4g"
  # ANDROID_SDK_ROOT와 ANDROID_HOME 환경 변수를 이미지에 맞게 설정합니다.
  ANDROID_SDK_ROOT: "/opt/android-sdk"
  ANDROID_HOME: "/opt/android-sdk"

# 모든 Job이 실행되기 전에 공통으로 수행할 스크립트를 정의합니다.
before_script:
  # Gradle Wrapper를 실행 가능하게 권한을 부여합니다.
  - chmod +x ./gradlew
  # CI 환경에서 불필요한 Gradle 데몬을 비활성화하는 설정을 ~/.gradle/gradle.properties에 추가합니다.
  - mkdir -p ~/.gradle
  - echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties

# 파이프라인 속도 향상을 위한 캐시 설정
cache:
  # 캐시를 식별하는 고유 키. `CI_COMMIT_REF_SLUG`는 브랜치명을 나타냅니다.
  key: "$CI_COMMIT_REF_SLUG"
  # 여러 Job에서 캐시를 다운로드하고, 특정 Job에서만 캐시를 업데이트하도록 설정 (pull-push)
  # 기본 정책은 pull-push 이지만, 명시적으로 pull 로 설정하여 모든 job이 캐시를 사용하도록 합니다.
  policy: pull
  # 캐시할 경로를 지정합니다. Gradle 관련 파일들을 캐시하여 의존성 재다운로드를 방지합니다.
  paths:
    - .gradle/wrapper
    - .gradle/caches
    - app/build # 빌드 결과물을 캐시하여 후속 작업 속도를 높일 수 있습니다.

# 'lint' 스테이지에 속하는 Job 정의
lintDebug:
  stage: lint
  # lint job 에서는 gradle 캐시를 업데이트 하지 않고, 다운로드만 받도록 policy 를 설정합니다.
  cache:
    <<: *global_cache # 전역 캐시 설정을 상속받아 사용
    policy: pull
  script:
    - ./gradlew lintDebug
  # 이 Job이 실패하더라도 다음 스테이지를 계속 진행할지 여부. lint는 경고이므로 실패해도 괜찮다면 true로 설정합니다.
  allow_failure: true

# 'test' 스테이지에 속하는 Job 정의
unitTests:
  stage: test
  # test job 에서는 gradle 캐시를 업데이트 하지 않고, 다운로드만 받도록 policy 를 설정합니다.
  cache:
    <<: *global_cache # 전역 캐시 설정을 상속
    policy: pull
  script:
    - ./gradlew testDebugUnitTest

# 'build' 스테이지에 속하는 Job 정의
buildDebug:
  stage: build
  # build job에서는 다운로드 받은 캐시를 업데이트해야하므로, policy를 push-pull로 설정합니다.
  cache:
    <<: *global_cache
    policy: pull-push
  script:
    # 디버그용 APK를 빌드합니다.
    - ./gradlew assembleDebug
  artifacts:
    # Job이 성공하면 생성될 파일을 보관(Artifact)합니다.
    name: "debug-apk-$CI_COMMIT_SHORT_SHA" # 아티팩트 파일 이름
    paths:
      - app/build/outputs/apk/debug/app-debug.apk # 보관할 파일 경로
    expire_in: 1 week # 보관 기간

# 'signed_build' 스테이지에 속하며, develop 브랜치에서만 실행되는 Job 정의
buildReleaseSigned:
  stage: signed_build
  script:
    # 1. GitLab CI/CD 변수에서 Base64로 인코딩된 Keystore 파일을 디코딩하여 실제 파일로 저장합니다.
    - echo $RELEASE_KEYSTORE_BASE64 | base64 -d > app/release-keystore.jks
    # 2. 릴리즈용 앱 번들(AAB)을 빌드합니다. Keystore 관련 정보는 변수에서 읽어옵니다.
    - ./gradlew bundleRelease -Pandroid.injected.signing.store.file=$(pwd)/app/release-keystore.jks -Pandroid.injected.signing.store.password=$RELEASE_STORE_PASSWORD -Pandroid.injected.signing.key.alias=$RELEASE_KEY_ALIAS -Pandroid.injected.signing.key.password=$RELEASE_KEY_PASSWORD
  artifacts:
    name: "release-aab-$CI_COMMIT_SHORT_SHA"
    paths:
      - app/build/outputs/bundle/release/app-release.aab
    expire_in: 1 month
  only:
    - develop # 'develop' 브랜치에 푸시될 때만 이 Job을 실행합니다.

분석 1: `image`

CI/CD Job은 격리된 Docker 컨테이너 환경에서 실행됩니다. image는 이 컨테이너를 생성할 기반 Docker 이미지를 지정합니다. 안드로이드 빌드를 위해서는 Java(JDK), 안드로이드 SDK, 빌드 도구 등이 설치된 환경이 필요합니다. 처음부터 Ubuntu 같은 기본 이미지에서 모든 것을 설치할 수도 있지만, 매우 번거롭고 매번 실행될 때마다 많은 시간이 소요됩니다.

가장 효율적인 방법은 안드로이드 빌드 환경이 미리 구성된 Docker 이미지를 사용하는 것입니다. 위 예시의 `mobiledevops/android-sdk-image:34.0.0`는 안드로이드 SDK 34 버전이 설치된 공개 이미지로, 별도의 SDK 설치 과정 없이 바로 Gradle 명령어를 실행할 수 있게 해줍니다.

분석 2: `stages`와 Job

  • stages는 파이프라인의 전체적인 흐름을 정의합니다. 위 예시에서는 `lint` -> `test` -> `build` -> `signed_build` 순으로 단계를 나누었습니다. 같은 스테이지에 속한 Job들은 병렬로 실행되며, 이전 스테이지의 모든 Job이 성공해야만 다음 스테이지가 시작됩니다.
  • lintDebug, unitTests, buildDebug는 각각의 스테이지에 속한 구체적인 작업 단위인 'Job'입니다. 각 Job은 `script` 섹션에 정의된 셸 스크립트를 실행합니다. ./gradlew lintDebug와 같이 우리는 익숙한 Gradle Wrapper 명령어를 그대로 사용하면 됩니다.

분석 3: `cache` (빌드 속도 최적화의 핵심)

CI/CD 파이프라인은 매번 깨끗한 환경에서 시작되므로, 캐시를 사용하지 않으면 실행할 때마다 모든 의존성 라이브러리를 새로 다운로드해야 합니다. 이는 엄청난 시간 낭비입니다. cache 설정은 Job 실행 사이에 특정 파일이나 디렉터리를 보존하여 다음 Job에서 재사용하게 해주는 강력한 기능입니다.

  • key: 캐시를 구분하는 고유 식별자입니다. $CI_COMMIT_REF_SLUG는 GitLab이 제공하는 사전 정의 변수로, 현재 브랜치 이름(e.g., `main`, `feature-login`)으로 치환됩니다. 이렇게 하면 브랜치별로 캐시가 분리되어 관리됩니다.
  • paths: 캐시할 대상을 지정합니다. 안드로이드 프로젝트에서는 Gradle이 라이브러리를 저장하는 .gradle/caches와 Gradle Wrapper 관련 파일이 있는 .gradle/wrapper를 캐시하는 것이 필수적입니다.
  • policy: 캐시 사용 전략을 정의합니다.
    • pull: 기존 캐시가 있으면 가져와서 사용만 하고, Job이 끝나도 캐시를 업데이트하지 않습니다. (예: lint, test Job)
    • push: Job이 성공적으로 끝나면 paths에 지정된 파일들을 캐시에 업로드만 합니다. 기존 캐시는 가져오지 않습니다.
    • pull-push: 기본값으로, Job 시작 시 캐시를 가져오고, Job 성공 시 캐시를 업데이트합니다. (예: build Job)

분석 4: `artifacts` (결과물 저장)

Job이 실행되는 컨테이너는 작업이 끝나면 사라집니다. 이때 빌드를 통해 생성된 APK나 AAB 파일과 같은 결과물을 보존하고 싶을 때 `artifacts`를 사용합니다. `artifacts`로 지정된 파일은 Job이 성공하면 GitLab 서버에 업로드되어, 파이프라인 상세 페이지에서 다운로드하거나 다음 Job에서 전달받아 사용할 수 있습니다.

2.3. 고급 과정: 릴리즈 빌드 서명 및 배포 자동화

디버그 빌드를 넘어 실제 배포를 위한 릴리즈 빌드를 자동화하려면 '코드 서명(Code Signing)' 과정을 파이프라인에 포함해야 합니다. 서명에 필요한 Keystore 파일과 비밀번호는 민감한 정보이므로, 절대로 소스 코드 저장소에 직접 커밋해서는 안 됩니다.

안전한 키 관리를 위한 GitLab CI/CD Variables

GitLab은 이러한 민감 정보를 안전하게 관리하기 위해 CI/CD Variables 기능을 제공합니다. [프로젝트] > [Settings] > [CI/CD] > [Variables] 메뉴에서 변수를 등록할 수 있습니다.

  1. Keystore 파일 등록: Keystore 파일(.jks)은 바이너리 파일이므로 텍스트 기반 변수에 직접 넣을 수 없습니다. 대신, 파일을 Base64로 인코딩한 문자열을 변수 값으로 저장합니다.
    
    # 로컬 터미널에서 실행하여 Base64 인코딩된 문자열을 복사합니다.
    # macOS
    base64 -i my-release-key.jks
    # Linux
    base64 my-release-key.jks | tr -d '\n'
        

    생성된 긴 문자열을 복사하여 GitLab Variables에 RELEASE_KEYSTORE_BASE64라는 이름의 변수를 만들고 값으로 붙여넣습니다. 'Protect variable' 옵션을 활성화하면 보호된 브랜치(Protected Branch, 예: `main`, `develop`)에서만 이 변수를 사용할 수 있어 보안이 강화됩니다.
  2. 비밀번호 및 별칭 등록: Keystore 비밀번호, 키 별칭(alias), 키 비밀번호를 각각 RELEASE_STORE_PASSWORD, RELEASE_KEY_ALIAS, RELEASE_KEY_PASSWORD와 같은 이름의 변수로 등록합니다. 이 변수들은 'Mask variable' 옵션을 활성화하여 Job 로그에 값이 노출되지 않도록 설정하는 것이 좋습니다.

모든 변수가 등록되었다면, 위 .gitlab-ci.yml 예시의 buildReleaseSigned Job이 동작할 준비가 된 것입니다. 이 Job은 다음과 같이 동작합니다:

  • echo $RELEASE_KEYSTORE_BASE64 | base64 -d > app/release-keystore.jks: 환경 변수로 주입된 Base64 문자열을 다시 바이너리 Keystore 파일로 디코딩하여 컨테이너 내부에 생성합니다.
  • ./gradlew bundleRelease -P...: 릴리즈용 앱 번들을 빌드합니다. 이때 -P 플래그를 사용하여 서명에 필요한 파일 경로와 비밀번호들을 GitLab CI/CD 변수에서 읽어와 Gradle에 전달합니다.

2.4. 다음 단계: 배포 자동화와 알림

서명된 AAB 파일이 성공적으로 빌드되었다면, 이제 파이프라인의 마지막 단계인 '배포'를 자동화할 수 있습니다.

  • Firebase App Distribution: Firebase CLI를 사용하여 새로 빌드된 앱을 테스터들에게 자동으로 배포할 수 있습니다.
  • Google Play Console: Gradle Play Publisher와 같은 플러그인을 사용하면 내부 테스트, 알파, 베타 트랙 또는 프로덕션으로 직접 업로드하는 Job을 추가할 수 있습니다.
  • Slack 알림: 파이프라인의 각 단계(성공, 실패)에 대한 알림을 Slack으로 보내 팀원들이 빌드 상태를 즉시 알 수 있도록 구성할 수 있습니다. GitLab의 기본 통합 기능을 사용하거나, curl 명령어로 Slack Webhook을 호출하는 스크립트를 Job에 추가하면 됩니다.

결론

우리는 이 글을 통해 JCenter의 종료에 따른 최신 Gradle 저장소 설정법부터 GitLab CI/CD를 활용하여 안드로이드 애플리케이션의 빌드와 테스트, 서명까지 자동화하는 구체적이고 실전적인 방법을 단계별로 살펴보았습니다. .gitlab-ci.yml 파일을 통한 'Configuration as Code' 방식의 접근은 CI/CD 파이프라인을 명확하게 관리하고, 팀원 누구나 이해하고 개선할 수 있는 환경을 만들어줍니다.

처음에는 낯설고 복잡해 보일 수 있지만, 한번 안정적인 파이프라인을 구축하고 나면 반복적인 수동 빌드와 테스트에서 해방되어 개발자는 오롯이 가치 있는 코드를 작성하는 데에만 집중할 수 있게 됩니다. 이는 단순히 시간을 절약하는 것을 넘어, 소프트웨어의 품질을 한 단계 끌어올리는 중요한 원동력이 될 것입니다. 지금 바로 여러분의 안드로이드 프로젝트에 GitLab CI/CD를 적용하여 개발 경험의 혁신을 시작해 보시길 바랍니다.


0 개의 댓글:

Post a Comment