모바일 디바이스 팜을 활용한 크래시 자동 리포팅 파이프라인 구축 가이드: AWS Device Farm과 Firebase Test Lab 비교부터 CI/CD 연동까지

AWS Device Farm과 Firebase Test Lab을 활용해 모바일 게임 빌드의 기기 파편화 크래시를 자동 감지하고 심볼리케이션 로그를 리포팅하는 CI/CD QA-Ops 파이프라인 구축 가이드입니다.

✓ 복사 완료!
챕터크래시 분석 & 메모리 덤프
난이도중급
상태● 검토 완료
대상 엔진
Unity
최종 업데이트
모바일 디바이스 팜을 활용한 크래시 자동 리포팅 파이프라인 구축 가이드: AWS Device Farm과 Firebase Test Lab 비교부터 CI/CD 연동까지

핵심 요약

모바일 게임 개발에서 기기 파편화로 인한 크래시를 사전에 차단하려면 클라우드 기반 디바이스 팜(Firebase Test Lab, AWS Device Farm)을 CI/CD 파이프라인에 통합해야 합니다. 커밋 또는 빌드 완료 시점에 자동으로 Robo 테스트나 Game Loop 테스트를 트리거하고 실행 결과에서 NDK/네이티브 크래시 덤프(tombstone, minidump) 및 Unity IL2CPP 심볼을 추출하여 Slack 및 Jira로 자동 발송하는 자동화 파이프라인을 구축하면 QA 비용을 크게 절감하고 크래시 프리율(Crash-free users rate)을 99.9% 이상으로 유지할 수 있습니다.

왜 모바일 게임에서 디바이스 팜 기반 크래시 테스트가 필수적인가?

모바일 게임 환경은 OS 버전(Android 10~15, iOS 15~18), 칩셋(Snapdragon, MediaTek, Exynos, Apple Silicon), GPU 드라이버(Adreno, Mali 등), 해상도와 메모리 크기까지 극도로 파편화되어 있습니다. 소수의 사내 테스트 기기로는 특정 저가형 Mali GPU 드라이버 셰이더 컴파일 버그나 제조사별 백그라운드 메모리 킬러(OOM Killer)로 인한 비정상 종료를 사전에 잡아내기 어렵습니다.

디바이스 팜(Cloud Device Farm)을 활용하면 수백 대의 실제 물리 기기(Real Devices)에서 빌드를 자동으로 설치·실행하고 그래픽 렌더링 결함, 메모리 누수, 네이티브 크래시(SIGSEGV, SIGBUS, SIGABRT) 로그를 자동으로 수집할 수 있습니다.

AWS Device Farm vs Firebase Test Lab: 어떤 솔루션을 선택해야 할까?

클라우드 테스트 인프라를 도입할 때 가장 널리 쓰이는 두 솔루션의 핵심 기능과 특징을 비교해 보겠습니다.

비교 항목Firebase Test Lab (FTL)AWS Device Farm (ADF)
주요 장점구글 플레이 콘솔 및 Firebase 생태계 밀접 연동, Game Loop 테스트 기본 지원AWS VPC 연동 가능, 커스텀 테스트 환경 설정 유연성
테스트 방식Robo Test, Game Loop Test, Instrumentation, XCTestAppium, Calabash, Espresso, XCTest, Custom Shell Script
비용 모델일일 무료 할당량(가상 10회, 실기기 5회), Spark/Blaze 종량제 ($5/실기기 시간)1,000분 무료 체험, 종량제 (0.17/분=0.17/분 = 10.2/시간) 또는 월간 정액 슬롯 ($250/슬롯)
게임 테스트 적합도매우 높음 (Game Loop Intent를 통한 씬 자동 순회 지원)보통 (Appium 또는 커스텀 Python 스크립트 작성 필요)
CLI 도구gcloud firebase test android/ios runaws devicefarm schedule-run

모바일 디바이스 팜 테스트 인프라 비교 및 기기 테스트 현황

크래시 자동 리포팅 파이프라인 아키텍처

지속적 통합(CI) 서버에서 빌드가 생성되면 디바이스 팜으로 아티팩트가 전송되고 테스트 실행 후 크래시 로그 파싱과 심볼릭 디매글링을 거쳐 개발팀에 전달되는 전체 워크플로우는 다음과 같습니다.

flowchart TD
    A[Git Push / PR 생성] --> B[CI/CD 빌드: Unity IL2CPP / Unreal]
    B --> C[APK/AAB & IPA 빌드 아티팩트 생성]
    C --> D[디바이스 팜 테스트 실행 gcloud/AWS CLI]
    D --> E{테스트 통과 여부 검사}
    E -- 크래시/실패 감지 --> F[Logcat / Tombstone 로그 다운로드]
    F --> G[addr2line / symbolicatecrash 디매글링]
    G --> H[Slack / Discord 크래시 알림 & Jira 티켓 자동 생성]
    E -- 성공 --> I[빌드 배포 파이프라인 진행]

Step-by-Step: Firebase Test Lab 기반 크래시 자동 리포팅 파이프라인 구축

Firebase Test Lab과 GitHub Actions를 활용해 빌드 시점에 기기 팜 크래시 테스트를 실행하고 결과를 통보받는 실전 절차를 단계별로 살펴보겠습니다.

1단계: 안드로이드 매니페스트에 Game Loop Intent 선언

UI 트리를 파싱하기 어려운 게임 엔진(Unity, Unreal) 특성상 표준 Robo Test는 화면 구석만 터치하다 끝날 수 있습니다. 게임 내부의 벤치마크 씬이나 튜토리얼을 자동 순회하도록 Game Loop 인텐트 필터를 AndroidManifest.xml에 등록합니다.

<activity android:name="com.unity3d.player.UnityPlayerActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <!-- Firebase Test Lab Game Loop 지원 -->
    <intent-filter>
        <action android:name="com.google.intent.action.TEST_LOOP" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="application/javascript" />
    </intent-filter>
    <meta-data android:name="com.google.test.loops" android:value="3" />
</activity>

게임 클라이언트 C# 스크립트에서는 해당 Intent로 실행되었는지 감지하고 각 시나리오(Loop 1: 로그인 및 로비, Loop 2: 인게임 전투, Loop 3: 상점 진입)를 자동으로 실행한 뒤 종료합니다.

using UnityEngine;

public class TestLoopController : MonoBehaviour
{
    void Start()
    {
#if UNITY_ANDROID && !UNITY_EDITOR
        using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
        using (var currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"))
        using (var intent = currentActivity.Call<AndroidJavaObject>("getIntent"))
        {
            string action = intent.Call<string>("getAction");
            if (action == "com.google.intent.action.TEST_LOOP")
            {
                int scenario = intent.Call<int>("getIntExtra", "scenario", 1);
                StartCoroutine(RunAutomatedTestScenario(scenario));
            }
        }
#endif
    }

    private System.Collections.IEnumerator RunAutomatedTestScenario(int scenarioIndex)
    {
        Debug.Log($"[TestLoop] Running Scenario #{scenarioIndex}");
        // 시나리오별 UI 및 플레이 로직 자동 순회
        yield return new WaitForSeconds(30f);
        
        // 테스트 정상 종료 알림
        Application.Quit();
    }
}

2단계: CI/CD(GitHub Actions)에서 gcloud CLI를 통한 테스트 트리거

빌드 산출물(APK)을 Firebase Test Lab 매트릭스에 전달하여 다양한 디바이스 모델에서 병렬 테스트를 수행합니다.

name: Device Farm Crash Automation

on:
  workflow_dispatch:
  schedule:
    - cron: '0 18 * * *' # 매일 새벽 3시(KST) 정기 회귀 테스트

jobs:
  run-device-farm:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_SA_KEY }}

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v2

      - name: Run Firebase Test Lab (Game Loop)
        id: test_lab
        continue-on-error: true
        run: |
          gcloud firebase test android run \
            --type game-loop \
            --app ./builds/Android_Release.apk \
            --device model=oriole,version=33,locale=ko,orientation=landscape \
            --device model=dm3q,version=34,locale=ko,orientation=landscape \
            --device model=a14,version=33,locale=ko,orientation=landscape \
            --scenario-numbers 1,2,3 \
            --timeout 10m \
            --results-bucket=gs://game-test-reports-bucket \
            --results-dir=build-${{ github.run_id }} \
            --format="json" > test_result.json

      - name: Parse Crash Logs & Notify
        if: always()
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
        run: |
          python ./scripts/parse_ftl_results.py \
            --result-json test_result.json \
            --gcs-bucket gs://game-test-reports-bucket/build-${{ github.run_id }}

3단계: 크래시 로그 파싱 및 심볼릭 디매글링(Symbolication)

테스트가 비정상 종료(Crash)되었을 때 클라우드 스토리지 버킷에 업로드된 logcat과 tombstone 파일을 분석하여 사람이 읽을 수 있는 스택 트레이스로 변환하는 파이썬 스크립트입니다.

import argparse
import json
import os
import sys
import urllib.request

def parse_test_results(result_file):
    with open(result_file, "r", encoding="utf-8") as f:
        data = json.load(f)

    # Accept a TestMatrix API object or a normalized list of executions.
    executions = data.get("testExecutions", []) if isinstance(data, dict) else data
    failures = []
    for execution in executions:
        state = str(execution.get("state", "")).upper()
        details = execution.get("testDetails", {})
        outcome = str(details.get("outcome", "")).lower()
        if state == "ERROR" or outcome in {"failure", "failed"}:
            failures.append({
                "device": execution.get("axisValue", "Unknown device"),
                "details": details.get("progressMessages", []),
            })

    if isinstance(data, dict) and str(data.get("outcomeSummary", "")).upper() not in {"", "SUCCESS", "SUCCESSFUL"}:
        failures.append({"device": "Test matrix", "details": [str(data.get("outcomeSummary"))]})
    return failures

def send_slack_alert(failures, gcs_bucket, webhook_url):
    device_summary = "\n".join(f"- {item['device']}: failed" for item in failures)
    payload = {"text": f"Device farm test failure\n{device_summary}\nLogs: {gcs_bucket}"}
    req = urllib.request.Request(
        webhook_url, data=json.dumps(payload).encode("utf-8"),
        headers={"Content-Type": "application/json"}, method="POST",
    )
    with urllib.request.urlopen(req, timeout=15) as response:
        response.read()

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--result-json", required=True)
    parser.add_argument("--gcs-bucket", required=True)
    args = parser.parse_args()
    failures = parse_test_results(args.result_json)
    if failures:
        webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
        if webhook_url:
            send_slack_alert(failures, args.gcs_bucket, webhook_url)
        sys.exit(1)
    print("All device farm tests passed.")

디바이스 팜 로그 뷰어 및 크래시 콜스택 분석 화면

네이티브 및 IL2CPP 콜스택 심볼리케이션 자동화 팁

Android 빌드에서 Unity의 libunity.so, libil2cpp.so 또는 Unreal Engine의 libUnreal.so 라이브러리가 크래시되면 logcat에 16진수 메모리 주소만 남습니다:

#00 pc 00000000005a3f28  /data/app/~~.../lib/arm64/libil2cpp.so
#01 pc 00000000005a4110  /data/app/~~.../lib/arm64/libil2cpp.so

이를 해결하기 위해 빌드 시점에 생성된 심볼 파일(symbols.zip 또는 libil2cpp.so.debug)을 NDK 툴체인의 llvm-symbolizer 또는 ndk-stack과 연동하는 자동화 단계를 구성해야 합니다.

# ndk-stack을 활용한 실시간 로그 심볼릭 변환
$NDK_HOME/ndk-stack -sym ./builds/symbols/arm64-v8a/ -dump logcat.txt > human_readable_crash.log

이를 거치면 난해한 메모리 오프셋이 실제 소스 코드 파일명과 라인 번호(InventoryManager.cs:142)로 변환되어 알림 메시지에 포함됩니다.

자주 묻는 질문 (FAQ)

Q1. 디바이스 팜 실행 비용을 최적화하려면 어떻게 해야 하나요?

모든 커밋마다 수십 대의 실제 기기 전체 매트릭스를 실행하면 비용이 급격히 증가합니다. PR 검증 단계에서는 대표적인 파편화 기기 2~3종(저사양 Mali GPU 1대, 최신 Adreno 1대, iOS 1대)에 대해 5분 미만의 스모크 테스트만 수행하고 10~20대 이상의 풀 매트릭스 회귀 테스트는 일일 야간 빌드(Nightly Build) 스케줄러로 격리하여 실행하는 것이 좋습니다.

Q2. ANR(Application Not Responding)이나 메모리 누수도 감지할 수 있나요?

네, 가능합니다. Firebase Test Lab과 AWS Device Farm 모두 테스트 도중 발생하는 ANR 이벤트(ActivityManager: ANR in ...)를 감지하여 실패로 처리합니다. 또한 실행 중 수집된 CPU 사용률, 메모리 PSS(Proportional Set Size) 프로파일링 그래프를 아티팩트로 제공하므로 특정 씬 진입 시 메모리 누수가 일어나는지 추적할 수 있습니다.

Q3. 게임 루프(Game Loop) 테스트와 일반 UI/Robo 테스트 중 무엇이 더 적합한가요?

UI 계층이 네이티브 안드로이드 View 위젯이 아니라 단일 Canvas/SurfaceView 위에서 렌더링되는 게임 엔진(Unity, Unreal, Godot) 구조상 표준 Robo 테스트는 무작위 화면 탭 수준에 그칩니다. 따라서 정밀한 크래시 리포팅을 위해서는 게임 내부의 씬 이동과 핵심 로직을 스크립트로 제어하는 Game Loop 테스트를 도입하는 것이 훨씬 효과적입니다.

결론

모바일 디바이스 팜을 활용한 크래시 자동 리포팅은 라이브 출시 후 사용자 리뷰 테러와 환불 사태를 예방하는 가장 확실한 QA-Ops 솔루션입니다. CI/CD 파이프라인에 Game Loop 기반의 디바이스 팜 테스트와 NDK 심볼리케이션을 통합하여 파편화 기기 버그를 개발 단계에서 선제적으로 해결하시기 바랍니다.