Android 개발/Gradle・빌드

손빌드 6년차가 GitHub Actions로 안드로이드 CI 붙이며 밟은 지뢰들

stackD 2026. 9. 30. 18:00
반응형

 

얼마 전에 친구 카톡 단톡방에서 '너 없으면 그 앱 아무도 못 올리는 거 아니냐'는 말을 들었어요. 서명키도 빌드 환경도 제 노트북에만 있었거든요. 그게 진짜 무서웠습니다.

 

그날 바로 GitHub Actions 안드로이드 CI를 붙이기 시작했는데요. 첫 커밋은 열두 줄짜리 YAML 한 장이었는데, 초록불 뜨기까지 실패를 열한 번 봤습니다. 그 삽질을 순서대로 풀어볼게요.

 

손 빌드로 놓쳤던 것들과 버스 팩터 1의 공포

릴리스 빌드 한 번에 대충 15분이 걸렸어요. 빌드 돌리고, 서명하고, 스토어에 올리고. 한 달에 스무 번쯤 하면 그것만 5시간입니다. 5시간이 어느 정도냐면, 반나절 근무를 통째로 반복 노동에 갈아 넣은 셈이지요.

 

근데 시간보다 무서웠던 건 따로 있었어요. 서명키와 빌드 세팅이 전부 제 노트북에만 있으니까, 제가 사라지면 앱을 아무도 못 올리는 상태였던 거죠. 이걸 "버스 팩터 1"이라고 부르더군요. 딱 한 사람이 빠지면 프로젝트가 멈추는 상태입니다.

 

한 가지는 미리 말씀드릴게요. "CI 도입 = 전부 자동화"는 오해예요. 배포가 뜸한 토이 프로젝트라면 구축·유지 시간이 절약 시간보다 커서 오히려 손해일 수 있습니다. 저처럼 반복 배포가 잦고, 나 혼자 빌드하는 구조가 불안한 경우에 효과가 크다고 봅니다.

 

 

복붙 가능한 android.yml 뼈대와 JDK 17 고정

파일은 저장소 안 .github/workflows/android.yml 자리에 둡니다. push랑 pull_request에 걸어두면 코드가 올라올 때마다 알아서 돌아가요.

 

name: Android CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'
          cache: 'gradle'
      - name: Grant execute permission
        run: chmod +x gradlew
      - name: Build debug APK
        run: ./gradlew assembleDebug
      - uses: actions/upload-artifact@v4
        with:
          name: app-debug
          path: app/build/outputs/apk/debug/app-debug.apk

 

여기서 제 첫 실패 3대장이 나왔습니다.

 

  1. gradlew: Permission denied — 윈도우에서 커밋하면 실행 권한이 안 붙어서 자주 터집니다. chmod +x gradlew 한 줄이면 해결돼요.
  2. JDK 버전 불일치 — 로컬은 21인데 러너는 다른 버전이면 빌드가 어긋납니다. AGP 8.x가 최소 JDK 17을 요구해서, temurin 배포판에 17로 못 박았어요.
  3. 욕심 — 처음부터 테스트·서명·배포까지 넣으려다 다 깨졌습니다. 그래서 assembleDebug 하나만 초록불 띄우고 나머지는 뒤로 미뤘어요.

 

Gradle 캐시로 CI 시간 줄이되 이중 설정은 피하기

캐시를 안 걸면 매번 의존성을 새로 받아서 빌드가 느려집니다. 캐싱 방식은 크게 두 가지예요.

 

하나는 위 예시처럼 setup-java@v4에 cache: 'gradle'을 붙이는 방식이고, 다른 하나는 gradle/actions/setup-gradle 액션을 쓰는 방식입니다. 여기서 함정이 있는데요. 두 개를 동시에 켜면 캐시가 충돌합니다. setup-gradle을 쓰기로 했으면 setup-java 쪽 cache: gradle은 빼야 해요. 저는 이거 몰라서 캐시가 이상하게 꼬여 한참 헤맸습니다.

 

그리고 러너는 **무조건 Linux(ubuntu-latest)**로 두시는 게 좋아요. macOS 러너는 무료 사용량에서 10배로 차감됩니다. 비공개 저장소 무료 한도가 월 2,000분인데, macOS로 돌리면 200분처럼 순식간에 녹아요. 윈도우도 2배라 안드로이드 빌드엔 리눅스가 정답이라고 보시면 되겠습니다.

 

 

PR마다 안드로이드 테스트·린트 게이트 세우기

빌드가 안정되면 pull_request 트리거에 ./gradlew test랑 lint를 걸어요. 초록불이 아니면 머지를 못 하게 막는 문지기 역할입니다. 사람 눈으로 놓치던 걸 기계가 잡아주니까 마음이 한결 편해지더라고요.

 

계측 테스트, 그러니까 에뮬레이터를 띄워서 돌리는 테스트는 reactivecircus/android-emulator-runner를 표준처럼 씁니다. connectedCheck를 돌리는 식인데요. 리눅스에서 하드웨어 가속을 쓰려면 KVM 권한을 열어주는 udev 규칙을 활성화해야 해서 여기서 자주 막힌다고 합니다.

 

개인적으로는 처음부터 계측 테스트까지 욕심내지 마시라고 권하고 싶어요. 로컬에선 되는데 CI에서만 깨질 때가 있는데, 커밋하고 푸시하고 결과 기다리는 루프가 은근히 고통스럽거든요. 유닛 테스트부터 안정화하고 에뮬레이터는 나중에 붙이는 게 덜 지칩니다.

 

 

키스토어·시크릿을 로그 유출 없이 넘기기

마지막이 서명 자동화예요. keystore 파일을 base64로 인코딩해서 GitHub Secrets(Settings > Secrets and variables > Actions)에 넣고, 빌드 때 디코드해서 signingConfigs에 주입하거나 r0adkll/sign-android-release 액션으로 처리합니다. keystore 원본이랑 비밀번호는 절대 커밋하면 안 됩니다.

 

보안 쪽에서 한 가지 덧붙이면요. 마켓플레이스 액션을 @v2 같은 태그로 당겨오면 공급망 공격에 노출될 여지가 있어요. 커밋 SHA로 버전을 못 박아두는 게 더 안전하다고 봅니다. 그리고 main에 푸시될 때마다 스토어에 자동 업로드하는 건 실수 배포 위험이 크니까, 승인 게이트(environment protection rules)를 하나 끼워두시는 게 안전합니다.

 

참고로 비용이 부담되면 직접 호스팅하는 러너(self-hosted)도 여전히 답이에요. 자체 러너 분당 과금 얘기가 나왔다가 커뮤니티 반발로 시행이 연기된 것으로 전해집니다.

 

 

오늘 커밋할 열두 줄

돌아보면 제가 제일 오래 미룬 이유는 "완벽한 파이프라인을 한 번에 세워야 한다"는 부담이었어요. 근데 첫 CI는 완벽한 자동화가 아니라 그냥 초록불 하나면 충분하더라고요. 빌드 → 유닛 테스트 → 서명 → 배포, 이 순서로 하나씩 붙여나가면 됩니다.

 

월요일 아침에 코드를 푸시해두고 커피 한 잔 뽑아 오면, 빌드가 알아서 끝나 있고 디버그 APK가 아티팩트에 올라와 있는 장면. 그거 하나 그려보면 오늘 열두 줄짜리 assembleDebug YAML 하나 커밋하는 게 하나도 아깝지 않으실 거예요.

 

반응형
LIST