클라우드 Mac에서 Android ARM64 에뮬레이터 실행하기

클라우드 Mac에서 Android ARM64 에뮬레이터 실행하기

모바일 프로젝트에 iOS와 Android 클라이언트가 모두 포함되면 팀에서는 Android 검증을 별도의 실행 환경에 맡기는 경우가 많습니다. 하지만 Apple Silicon 클라우드 Mac에 ARM64 에뮬레이터를 배포하면 코드 체크아웃, API 스모크 테스트, 양쪽 플랫폼의 인수 검증을 하나의 파이프라인에서 처리할 수 있습니다. 실제로 오류가 발생하기 쉬운 지점은 도구 설치가 아니라 잘못된 아키텍처 선택, 성급한 부팅 완료 판정, 동시 실행 작업 간의 상태 공유, 그리고 실패 후 재검토할 증거가 남지 않는 문제입니다.

아키텍처와 디렉터리 고정하기

실행 노드에서는 먼저 하드웨어 가상화 지원 여부를 확인한 뒤 Android SDK, AVD, 빌드 산출물의 위치를 통일해야 합니다. 스크립트가 대화형 셸에서만 일시적으로 적용되는 환경 변수에 의존해서는 안 됩니다.

export ANDROID_HOME="$HOME/Library/Android/sdk"
export ANDROID_AVD_HOME="$HOME/.android/avd"
export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"

sysctl kern.hv_support
emulator -accel-check
adb version

kern.hv_support는 사용 가능한 상태를 반환해야 하며 emulator -accel-check도 통과해야 합니다. 두 결과가 서로 다르다면 SDK를 반복해서 재설치하기보다 명령이 실제 작업을 실행하는 사용자 컨텍스트에서 수행되고 있는지 먼저 확인하십시오.

Apple Silicon 노드에서는 arm64-v8a 시스템 이미지를 선택해야 합니다. x86_64 이미지를 사용하면 아키텍처 변환 비용이 늘어날 뿐 아니라 네이티브 라이브러리 로딩 문제를 애플리케이션 코드 문제로 잘못 판단할 수도 있습니다. 시스템 이미지 버전은 저장소 변수로 고정하고, 매 작업에서 자동으로 최신 버전을 따르게 하지 말고 병합 요청을 통해 업그레이드해야 합니다.

재사용 가능한 기본 AVD 만들기

먼저 프로젝트에 명시적으로 필요한 플랫폼, 에뮬레이터, 이미지를 설치한 다음 개인의 대화형 상태에 종속되지 않는 기본 기기를 만듭니다.

API_LEVEL=35
IMAGE="system-images;android-${API_LEVEL};google_apis;arm64-v8a"
AVD_NAME="ci-arm64-api-${API_LEVEL}"

sdkmanager "platform-tools" "emulator" "platforms;android-${API_LEVEL}" "$IMAGE"
printf "no\n" | avdmanager create avd \
  --force \
  --name "$AVD_NAME" \
  --package "$IMAGE" \
  --device "pixel_6"

생성이 끝나면 config.ini를 확인합니다. 지속적 통합 환경에는 일반적으로 카메라, 마이크, 대용량 쓰기 가능 데이터 디스크가 필요하지 않습니다. 불필요한 장치를 비활성화하고 메모리, 화면 밀도, 해상도를 고정할 수 있습니다. 매개변수가 적을수록 기준 환경을 재현하기 쉽습니다.

기본 AVD는 첫 부팅을 완료한 깨끗한 템플릿만 제공합니다. 테스트 데이터, 로그인 상태, 애플리케이션 캐시는 기본 디렉터리에 다시 기록하면 안 됩니다.

최초 부팅에서는 시스템 초기화를 완료해야 합니다. 홈 화면 서비스가 사용 가능한지 확인한 후 애니메이션을 끄고 임시 애플리케이션을 정리한 다음 ci-base 스냅샷을 저장합니다. 스냅샷을 생성한 에뮬레이터 버전과 이를 복원하는 버전은 같아야 합니다. 에뮬레이터를 업그레이드했다면 기존 스냅샷을 계속 재사용하지 말고 새로 만들어야 합니다.

헤드리스 부팅에서는 ADB만 기다리면 안 됩니다

파이프라인에서는 -no-window로 그래픽 창을 비활성화하고 각 작업에 서로 다른 짝수 포트를 할당합니다. adb devicesdevice가 표시되는 것은 전송 채널이 연결되었다는 의미일 뿐, 시스템 부팅이 완료되었다는 뜻은 아닙니다.

AVD_NAME="ci-arm64-api-35"
EMULATOR_PORT=5556
SERIAL="emulator-${EMULATOR_PORT}"

emulator "@${AVD_NAME}" \
  -no-window \
  -no-audio \
  -no-boot-anim \
  -port "$EMULATOR_PORT" \
  -snapshot ci-base \
  -no-snapshot-save &

adb -s "$SERIAL" wait-for-device

for attempt in $(seq 1 90); do
  status="$(adb -s "$SERIAL" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')"
  [ "$status" = "1" ] && break
  sleep 2
done

[ "$status" = "1" ] || exit 1

부팅 대기에는 전체 제한 시간을 반드시 설정해야 합니다. 시간이 초과되면 getprop, logcat, 에뮬레이터 표준 오류를 저장한 후 프로세스를 종료합니다. 무한 대기는 실행 슬롯만 점유하고 이미지 손상이나 포트 충돌을 감출 뿐입니다.

애니메이션을 끄고 상태 정규화하기

스냅샷을 복원한 후에도 멱등성 있는 설정을 적용해야 합니다. 그래야 기본 이미지를 다시 만들 때 핵심 상태가 누락되는 일을 방지할 수 있습니다.

adb -s "$SERIAL" shell settings put global window_animation_scale 0
adb -s "$SERIAL" shell settings put global transition_animation_scale 0
adb -s "$SERIAL" shell settings put global animator_duration_scale 0
adb -s "$SERIAL" shell input keyevent 82

이 명령들은 테스트 픽스처를 대체하지 않습니다. 언어, 시간대, 권한, 네트워크 상태는 테스트 케이스에서 명시적으로 설정하고 종료 후 원래 상태로 복원해야 합니다.

ADB로 최소 인수 검증 루프 구성하기

에뮬레이터 준비가 끝나면 전체 테스트 스위트를 실행하기 전에 설치, 실행, 프로세스 생존 여부부터 검증합니다. 이렇게 하면 환경 장애와 비즈니스 로직의 어설션 실패를 구분할 수 있습니다.

adb -s "$SERIAL" install -r "$APK_PATH"
adb -s "$SERIAL" shell am force-stop "$APP_ID"
adb -s "$SERIAL" shell am start -W -n "${APP_ID}/${LAUNCH_ACTIVITY}"
adb -s "$SERIAL" shell pidof "$APP_ID"

am start -W는 실행 결과와 소요 시간 필드를 반환합니다. 스크립트에서는 상태가 성공인지 검사하는 동시에 pidof의 출력이 있는지도 확인해야 합니다. 설치 명령이 0을 반환했다는 사실만으로는 진입 Activity를 해석할 수 있는지, 프로세스가 시작되는지, 네이티브 라이브러리 아키텍처가 올바른지 증명할 수 없습니다.

실패 시에는 다음과 같은 최소 증거 세트를 보관하는 것이 좋습니다.

증거 명령 또는 위치 용도
기기 속성 adb shell getprop API, ABI, 부팅 상태 확인
시스템 로그 adb logcat -d -v threadtime 비정상 종료, 권한 및 서비스 오류 파악
설치 정보 adb shell dumpsys package "$APP_ID" 버전, 진입점, ABI 확인
화면 상태 adb exec-out screencap -p 화면 가림, 팝업, 검은 화면 판별
에뮬레이터 출력 작업 표준 오류 파일 스냅샷 및 가상화 문제 식별

로그를 보관하기 전에는 민감한 정보를 제거해야 합니다. 환경 변수, 액세스 토큰, 테스트 계정 자격 증명이 장기간 보존되는 파이프라인 산출물에 포함되지 않도록 하십시오.

동시 실행 작업 격리 및 안정적인 정리

동일한 물리 노드에서 여러 에뮬레이터를 실행할 때는 각 작업이 독립적인 포트, AVD 사본, 임시 디렉터리를 가져야 합니다. 여러 프로세스가 같은 기본 AVD를 직접 열도록 해서는 안 됩니다. 잠금 파일, 사용자 데이터, 스냅샷이 서로 덮어쓰일 수 있기 때문입니다.

작업을 시작할 때 기본 AVD를 작업 디렉터리로 복사하고 해당 .ini 경로를 다시 지정할 수 있습니다. 포트는 스케줄러가 할당하며, 짝수이면서 서로 중복되지 않아야 합니다. 테스트가 성공하든 실패하든 종료 후에는 반드시 다음 정리 작업을 실행해야 합니다.

cleanup() {
  adb -s "$SERIAL" emu kill >/dev/null 2>&1 || true
  wait "$EMULATOR_PID" 2>/dev/null || true
  rm -rf "$JOB_AVD_HOME"
}
trap cleanup EXIT INT TERM

동시 실행 상한을 CPU 코어 수만으로 결정해서는 안 됩니다. 에뮬레이터, 애플리케이션, 빌드 작업은 메모리와 디스크 대역폭을 동시에 사용합니다. 더 안정적인 방법은 단일 인스턴스로 시작해 최대 메모리 사용량, 부팅 시간, 테스트 소요 시간을 기록한 다음 동시 실행 수를 단계적으로 늘리는 것입니다. 부팅 시간과 실패율이 함께 증가하기 시작하면 이전 단계로 낮춰야 합니다.

최종적으로 재현 가능성을 좌우하는 것은 특정 부팅 매개변수가 아니라 네 가지 경계입니다. 이미지 버전을 고정하고, 부팅 완료를 검증할 수 있어야 하며, 작업 상태를 서로 격리하고, 실패 당시의 현장을 보관할 수 있어야 합니다. 이 네 가지를 파이프라인 계약에 명시해야 Android 에뮬레이터가 임시 도구를 넘어 안정적인 엔지니어링 실행 단위가 될 수 있습니다.

자주 묻는 질문

adb devices에 device가 표시되면 바로 테스트해도 되나요?

아닙니다. 이는 ADB 채널이 연결됐다는 뜻일 뿐입니다. sys.boot_completed 값이 1이 될 때까지 확인한 뒤 앱 설치와 테스트를 시작해야 합니다.

여러 CI 작업이 하나의 AVD를 공유해도 되나요?

권장하지 않습니다. 작업마다 AVD 복사본, 포트, 데이터 디렉터리를 분리해야 잠금 충돌과 스냅샷 덮어쓰기, 이전 테스트 상태 유입을 막을 수 있습니다.

전용 물리 노드

다음 빌드를 전용 클라우드 Mac에서 실행하세요

기종, 노드와 결제 주기를 선택하세요. 구성과 달러 금액은 주문 전에 모두 안내되며, 이용 가능 여부는 콘솔에 실시간으로 표시되는 정보를 기준으로 합니다.

요금제 선택 및 주문