기존 프로젝트에서는 기획자의 QA를 받기 위해 수정사항마다 파일을 구글 콘솔의 내부 공유에 직접 올리는 반복적인 업무를 수행했습니다.
이에 QA용 파일을 자동으로 배포하기 위해, CI/CD 구현을 활용해보았습니다.
앱 개발 과정에서 빌드 자동화와 배포 자동화를 구현하면, 코드 변경이 있을 때마다 수동 작업 없이 안정적으로 APK를 생성하고 팀원에게 배포할 수 있습니다. 이 글에서는 GitHub Actions를 활용해
- 릴리즈 키(keystore)를 안전하게 관리하는 방법
- CI(Pull Request 환경)에서 디버그 APK를 빌드해 검증하는 방법
- CD(Production 브랜치 푸시)에서 릴리즈 APK를 빌드해 Firebase App Distribution으로 배포하는 방법
을 단계별로 상세히 살펴봅니다.
Github Action
깃허브에서 이벤트(커밋, 코멘트, 이슈, PR 등) 또는 cronJob이 트리거되는 상황에서 특정 작업을 수행하도록 구현하는 플랫폼입니다.
.github/workflows/ 경로에 YAML 파일을 작성해, 이벤트에 반응하도록 구성합니다.
CI (Continuous Integration)
개발자가 여러 명이 동시에 작업하는 중에도 “항상 통합 가능한” 상태를 유지하도록, 코드 변경이 생길 때마다 자동 빌드·테스트를 수행하는 방식입니다.
CD (Continuous Deployment/Delivery)
CI를 통해 자동으로 빌드된 아티팩트(APK, Docker 이미지 등)를 대상 환경(테스트, 스테이징, 실제 배포 등)에 자동으로 배포하는 과정입니다.
구현된 코드 :
Genti-Android/.github/workflows/firebase_distribution_builder.yml at develop · Genti2024/Genti-Android
내 마음대로 표현하는 하나뿐인 AI 사진, Genti. Contribute to Genti2024/Genti-Android development by creating an account on GitHub.
github.com
1. keystore.properties로 릴리즈 키 저장
(1) 릴리즈 키(Keystore)
Android 앱을 서명하기 위한 비밀 키를 담고 있는 .jks 파일과 비밀번호 집합입니다.
keystore.properties 파일에 경로와 패스워드를 저장하고, 빌드 스크립트에서 참조합니다.
개발, 스테이징, 프로덕션 등 환경별로 서로 다른 키스토어를 사용해야 할 때, keystore.properties를 교체하는 것으로 손쉽게 전환할 수 있습니다.
이 파일은 절대로 소스에 노출되면 안 되며, GitHub Secrets에 암호화해 관리해야 합니다.
빌드에 필요한 Keystore path, Keystore password, Key alias, Key password를 필요로 합니다.
(2) keystore.properties 생성
// gitIgnore
keystore.properties
- keystore.properties를 .gitignore에 추가해 Git 추적에서 제외해야 합니다.
// keystore.properties
storeFile=/Users/sangho/Desktop/santaKeyStore.jks
storePassword=xxxxxx
keyPassword=xxxxxx
keyAlias=santaKeyStore
- local.properties의 형태처럼, 추가적인 파일을 만들어 값을 입력해줍니다.
(3) build.gradle에서 참조
// build.gradle.kts(:app)
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
keystoreProperties.load(keystorePropertiesFile.inputStream())
signingConfigs {
create("release") {
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
- rootProject.file로 프로젝트 루트 디렉터리에 있는 keystore.properties 파일을 가리키는 File 객체를 만듭니다.
- java.util.Properties 인스턴스에, 위에서 가져온 파일을 InputStream 으로 읽어들여 Key–Value 쌍으로 로드합니다.
- Gradle의 android.signingConfigs DSL을 통해, release라는 이름의 새로운 서명 설정을 생성합니다.
- 릴리즈 빌드 타입에 위에서 만든 release 서명 설정을 연결합니다.
2. Github Secrets 설정
GitHub 레포지토리의 Settings → Secrets and variables → Actions에서 다음 값을 등록합니다.
이때, KEYSTORE_FILE에 해당하는 jks 파일은 기존의 path가 아닌, 파일을 Base64 텍스트로 인코딩하여 붙여넣습니다.
base64 -i gentiKeyStore.jks -o gentiKeyStore.jks.base64
3. CI - Pull Request 빌드 설정
(1) 워크플로우 설정
name: PR Checker
on:
pull_request:
branches: [ develop ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- on.pull_request: develop 브랜치에 대한 PR이 열리거나 업데이트될 때 실행되도록 트리거를 설정합니다.
- jobs.build: “build”라는 하나의 Job을 정의하고, steps 뒤에 이어지는 단계들을 적용합니다.
- runs-on: 이 Job을 실행할 환경으로 GitHub가 제공하는 최신 우분투 환경(ubuntu-latest)을 사용합니다.
(2) 코드 체크아웃
- name: Checkout
uses: actions/checkout@v3
- actions/checkout@v3 액션을 통해, 현재 Pull Request 대상 커밋의 코드를 워크스페이스에 clone합니다.
- 이후 모든 빌드·스크립트가 이 복제된 코드를 기준으로 실행됩니다.
(3) Gradle 캐시 활용
- name: Gradle cache
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: |
${{ runner.os }}-gradle-
- 의존성 다운로드 시간을 대폭 단축하기 위해, 한 번 내려받은 라이브러리 파일들을 GitHub 호스트 러너에 저장해 두었다가 이후 빌드에서 재사용하도록 하는 역할을 합니다.
- 두 번째 워크플로우부터는 key를 기준으로 이전에 저장해 둔 아티팩트를 찾아 자동으로 해당 디렉터리에서 활용합니다.
Key 전략
1. key : 워크플로우 실행 컨텍스트에 딱 맞는 캐시를 식별하는 고유 키입니다.
- runner.os + gradle-file-hash 형태로, build.gradle이나 의존성이 바뀌면 해시가 달라져 새 캐시를 생성하게 됩니다.
- ex. ubuntu-latest-gradle-aaa111
2. restore-keys : 완전히 일치하는 캐시가 없을 때, 키의 prefix를 가진 최근 캐시를 찾아 복원하도록 지정합니다.
➡️ 해시가 달라져도 OS 환경이 같고 Gradle 캐시 폴더 구조도 비슷한 과거 캐시를 활용해 다운로드 양을 줄일 수 있습니다.
(4) JDK 설치
- name: set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: 17
- Eclipse Temurin 배포판의 Java 17 JDK를 설치하여, Android Gradle Plugin이 요구하는 Java 버전과 일치시켜 호환성 문제를 방지합니다.
- GitHub 호스티드 러너는 “매번 깨끗한 상태”로 새로 프로비저닝되는 가상 환경이기 때문에, JDK 같은 런타임도 매 CI 실행 시마다 설치 과정을 거쳐야 합니다.
- actions/setup-java을 사용하면, GitHub 제공의 Tool Cache를 활용해 한 번 설치된 JDK 버전을 캐시해 두고 이후 같은 버전을 요청할 때는 네트워크 다운로드 없이 즉시 적용할 수 있습니다.
(5) Gradlew 실행 권한
- name: Change gradlew permissions
run: chmod +x ./gradlew
- 리포지토리에 포함된 Gradle Wrapper 스크립트(gradlew)에 실행 권한을 부여합니다.
(6) local.properties 생성
- name: Create Local Properties
run: touch local.properties
- name: Access Local Properties
env:
base_url: ${{ secrets.BASE_URL }}
// ...
run: |
echo "base.url=\"$base_url\"" >> local.properties
// ...
- Android 프로젝트 빌드 시 참조하는 local.properties 파일을 우선 빈 상태로 생성합니다.
- env 블록: GitHub Secrets에 저장된 환경 변수를 로컬 스크립트 내 변수로 매핑합니다.
- run: 여러 echo 명령으로 local.properties에 Key=Value 형태로 추가하여 런타임이나 빌드스크립트에서 참조할 값을 주입합니다.
(7) Firebase 설정 복원
- name: Access Firebase Service
run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > ./app/google-services.json
- Firebase 프로젝트의 google-services.json 내용을 Secret에서 꺼내 app/google-services.json으로 저장합니다.
- Crashlytics, Analytics 같은 Firebase 기능을 빌드 시 사용하기 위해 필요합니다.
(8) Debug APK 빌드
- name: Build debug APK
run: ./gradlew assembleDebug --stacktrace
- Gradle Wrapper를 이용해 assembleDebug 태스크를 실행, 디버그용 APK를 생성합니다.
- --stacktrace 옵션으로, 빌드 실패 시 Gradle의 상세 스택트레이스를 출력해 원인 분석을 돕습니다.
이 과정을 거쳐 Pull Request마다 일관되고 빠른 Debug APK 빌드가 자동화되어, 팀 내 코드 리뷰 품질과 개발 효율을 동시에 높일 수 있습니다.
4. CD - Pull Request 빌드 설정
(1) 워크플로우 생성
name: Genti Firebase App Distribution Builder
on:
push:
branches: [ production ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- on.push: production 브랜치에 push가 진행될 때 실행되도록 트리거를 설정합니다.
(2) CI 구현
앞서 진행했던 CI 과정을 동일하게 작성합니다.
(3) Keystore 복원
- name: Set up keystore
env:
keystore_file: ${{ secrets.KEYSTORE_FILE }}
run: |
echo "$keystore_file" > app/gentiKeyStore.b64
base64 -d -i app/gentiKeyStore.b64 > app/gentiKeyStore.jks
- secrets.KEYSTORE_FILE (Base64로 인코딩된 .jks 바이너리 텍스트)를 app/gentiKeyStore.b64로 저장합니다.
- base64 -d -i 명령어로 디코딩해 실제 app/gentiKeyStore.jks 파일을 생성합니다.
(4) keystore.properties 생성
- name: Access Keystore Properties
env:
store_password: ${{ secrets.STORE_PASSWORD }}
key_password: ${{ secrets.KEY_PASSWORD }}
key_alias: ${{ secrets.KEY_ALIAS }}
run: |
echo "storeFile=gentiKeyStore.jks" > keystore.properties
echo "storePassword=$store_password" >> keystore.properties
echo "keyAlias=$key_alias" >> keystore.properties
echo "keyPassword=$key_password" >> keystore.properties
- Gradle이 참조할 keystore.properties 파일을 동적으로 생성합니다.
(5) Firebase Secret Key 설정
1. Firebase App Id
FirebaseConsole > 프로젝트 설정 에서 확인할 수 있습니다.
2. serviceCredentialsFileContent
https://github.com/wzieba/Firebase-Distribution-Github-Action/wiki/FIREBASE_TOKEN-migration
다음 링크를 참조해서 생성할 수 있습니다.
(6) Firebase App Distribution 업로드
- name: Upload to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_APP_DISTRIBUTION_KEY }}
groups: genti
file: app/build/outputs/apk/release/app-release.apk
- wzieba/Firebase-Distribution-Github-Action 액션을 이용합니다.
- appId: Firebase Console에서 발급받은 앱 ID
- serviceCredentialsFileContent: 서비스 계정 JSON(Key) 내용
- groups: 배포 대상 테스터 그룹
- file: 빌드된 APK 파일 경로
- 정보를 전달해 App Distribution에 APK를 업로드합니다.
최종 결과
이 과정을 통해 프로덕션 브랜치에 코드가 머지될 때마다 자동으로 서명된 APK를 Firebase에 배포하여, 테스터 및 내부 사용자에게 즉시 배포할 수 있습니다.
'Feature' 카테고리의 다른 글
[Android] Iamport SDK(포트원)로 휴대폰 본인인증 및 결제 기능 구현하기 (0) | 2025.05.17 |
---|---|
[Android] FCM(Firebase Cloud Messaging) 푸시알림 & 알림 권한 요청 구현 및 이후 액션 지정하기 (0) | 2025.05.17 |
[Android] AudioManager를 활용한 Audio Focus 관리로 안정적인 사운드 제어하기 (0) | 2025.05.16 |
[Android] suspendCancellableCoroutine으로 ExoPlayer와 SoundPool 비동기 로드 (0) | 2025.05.16 |
[Android] wear OS 모듈의 Data Layer API로 워치 & 폰 데이터 동기화하기 (0) | 2025.05.16 |