ADB + FFmpeg 기반 Android 디바이스 팜: 10~50대 기기 대상 자동화 UI 테스트

@ridark_eth
영어1일 전 · 2026년 7월 07일
209K
104
10
22
175

TL;DR

이 가이드는 ADB와 FFmpeg를 사용하여 수십 대의 Android 기기에서 APK 설치, UI 테스트, 영상 리포트 생성을 자동화하는 Python 파이프라인을 제공하며, 상세한 비용 대비 효과 분석을 포함합니다.

앱이 수십 대의 실제 휴대폰, 다양한 제조사, 안드로이드 버전, 화면 해상도에서 작동해야 한다면 수동 테스트는 순식간에 악몽으로 변합니다. 아래는 연결된 모든 기기를 자동으로 발견하고, 모든 기기에 APK를 병렬로 설치하며, 계측 테스트를 실행하고, 각 실행 영상을 녹화한 후 FFmpeg로 하나의 영상 보고서로 합쳐주는 Python 파이프라인입니다.

전체 스택은 > Python + ADB + FFmpeg < 로, 표준 QA 도구입니다. 특별한 기술은 없고, 그저 반복 작업을 자동화하는 것뿐입니다.

Ridark - inline image

파이프라인 아키텍처

text
1adb devices ──► 시리얼 목록
2
3
4APK 설치 (모든 기기에 병렬로, ThreadPoolExecutor 사용)
5
6
7각 기기별:
8 screenrecord (백그라운드) → am instrument (테스트 실행) → 중지 + 영상 가져오기
9
10
11FFmpeg: 각 클립에 시리얼 오버레이 + 연결 ──► test_report.mp4

필요한 것

  • ADB (Android Debug Bridge): Android Platform Tools에서 제공하는 기기 제어 도구
  • Python 3.10+: 오케스트레이션용 (from __future__ 없이 list[str], tuple[...] 사용)
  • FFmpeg: 영상 처리 및 조립
  • USB 디버깅이 활성화된 기기들 (USB 연결 또는 adb tcpip를 통한 Wi-Fi 연결)

모든 코드에 적용되는 원칙: 서브프로세스에 인자를 리스트로 전달하고 shell=True 없이 실행합니다. 이렇게 하면 더 안전하며 (파일명을 통한 인젝션 방지), 경로에 공백이나 특수 문자가 있어도 문제가 없습니다.

1. 기기 발견

Ridark - inline image

adb devices는 승인되지 않았거나 오프라인 상태인 기기도 함께 표시합니다. 실제 'device' 상태인 기기만 필터링합니다.

python
1import subprocess
2
3def get_devices() -> list[str]:
4 """'device' 상태인 모든 기기의 시리얼을 반환합니다."""
5 out = subprocess.run(
6 ["adb", "devices"],
7 capture_output=True, text=True, check=True,
8 ).stdout
9
10 serials: list[str] = []
11 for line in out.splitlines()[1:]: # 첫 줄은 "List of devices" 헤더
12 line = line.strip()
13 if line.endswith("\tdevice"): # unauthorized / offline 제외
14 serials.append(line.split("\t")[0])
15 return serials

2. 병렬 APK 설치

50대의 기기에 하나씩 설치하는 것은 느립니다. 스레드 풀을 사용해 작업을 분산합니다. 각 adb install은 별도의 프로세스이므로 스레드가 여기서 효과적입니다 (CPU를 많이 사용하는 작업이 아닌 I/O 대기 작업이기 때문).

python
1from concurrent.futures import ThreadPoolExecutor, as_completed
2
3def install_apk(serial: str, apk_path: str) -> tuple[str, bool, str]:
4 r = subprocess.run(
5 ["adb", "-s", serial, "install", "-r", "-g", apk_path],
6 capture_output=True, text=True,
7 )
8 ok = r.returncode == 0 and "Success" in r.stdout
9 return serial, ok, (r.stdout + r.stderr).strip()
10
11def install_on_all(apk_path: str, serials: list[str]) -> None:
12 with ThreadPoolExecutor(max_workers=len(serials) or 1) as pool:
13 futures = [pool.submit(install_apk, s, apk_path) for s in serials]
14 for f in as_completed(futures):
15 serial, ok, log = f.result()
16 print(f"[{'OK' if ok else 'FAIL'}] {serial}")
17 if not ok:
18 print(f" {log}")

플래그 설명: -r -> 데이터 유지하며 재설치, -g -> 모든 런타임 권한 즉시 부여 (테스트 중 권한 팝업으로 인한 문제 방지에 유용).

3. 계측 테스트 실행

am instrument는 기기에서 Espresso/JUnit 테스트를 실행합니다. 성공 시 OK, 실패 시 FAILURES!!!를 stdout에 출력하므로 이를 통해 결과를 판단합니다.

python
1def run_instrumented_tests(
2 serial: str,
3 package: str,
4 runner: str = "androidx.test.runner.AndroidJUnitRunner",
5) -> tuple[str, bool]:
6 r = subprocess.run(
7 ["adb", "-s", serial, "shell", "am", "instrument", "-w",
8 f"{package}/{runner}"],
9 capture_output=True, text=True,
10 )
11 ok = "FAILURES!!!" not in r.stdout and r.returncode == 0
12 return serial, ok

package는 테스트 패키지 ID로, 일반적으로 com.example.app.test 형식입니다.

4. 테스트 중 화면 녹화

screenrecord는 기기에서 직접 영상을 녹화합니다. 알아야 할 제한 사항: 파일당 약 3분 제한, 오디오 없음. 백그라운드에서 녹화를 시작하고, 테스트를 실행한 후, 깔끔하게 중지하고 호스트로 파일을 가져옵니다.

가장 안정적인 녹화 중지 방법은 로컬 adb에 신호를 보내는 것이 아니라 기기 자체에서 pkill을 사용하는 것입니다. 이렇게 하면 screenrecord가 MP4 컨테이너를 올바르게 마무리합니다.

python
1import time
2
3def start_recording(serial: str, remote: str = "/sdcard/run.mp4") -> subprocess.Popen:
4 return subprocess.Popen(
5 ["adb", "-s", serial, "shell", "screenrecord", remote]
6 )
7
8def stop_recording(
9 serial: str,
10 proc: subprocess.Popen,
11 remote: str = "/sdcard/run.mp4",
12 local: str = "run.mp4",
13) -> None:
14 # 기기에서 SIGINT를 보내면 screenrecord가 파일을 올바르게 닫습니다
15 subprocess.run(["adb", "-s", serial, "shell", "pkill", "-SIGINT", "screenrecord"])
16 proc.wait(timeout=10)
17 time.sleep(1) # 기기가 컨테이너를 마무리할 시간을 줍니다
18 subprocess.run(["adb", "-s", serial, "pull", remote, local], check=True)

5. FFmpeg로 영상 보고서 조립

기기마다 화면 해상도가 다르므로 -c copy로 단순 연결할 수 없습니다. 각 클립을 공통 형식(1080×1920)으로 정규화하고, drawtext를 사용해 시리얼을 오버레이합니다. 이후 모든 클립이 동일해지면 재인코딩 없이 빠르게 concat으로 최종 조립합니다.

python
1def label_clip(src: str, dst: str, label: str) -> None:
2 """1080x1920으로 크기 조정하고 라벨(기기 시리얼)을 오버레이합니다."""
3 vf = (
4 "scale=1080:1920:force_original_aspect_ratio=decrease,"
5 "pad=1080:1920:(ow-iw)/2:(oh-ih)/2,"
6 f"drawtext=text='{label}':x=20:y=20:fontsize=42:"
7 "fontcolor=white:box=1:[email protected]"
8 )
9 subprocess.run(
10 ["ffmpeg", "-y", "-i", src, "-vf", vf,
11 "-an", "-c:v", "libx264", "-preset", "veryfast", "-crf", "23", dst],
12 check=True,
13 )
14
15def concat_report(clips: list[str], out: str = "test_report.mp4") -> None:
16 with open("concat_list.txt", "w") as f:
17 for c in clips:
18 f.write(f"file '{c}'\n")
19 subprocess.run(
20 ["ffmpeg", "-y", "-f", "concat", "-safe", "0",
21 "-i", "concat_list.txt", "-c", "copy", out],
22 check=True,
23 )

6. 모든 것을 하나로

python
1def main() -> None:
2 apk = "app-debug.apk"
3 package = "com.example.app.test"
4 runner = "androidx.test.runner.AndroidJUnitRunner"
5
6 serials = get_devices()
7 if not serials:
8 print("No devices found. Check USB and the output of 'adb devices'.")
9 return
10
11 print(f"Devices found: {len(serials)}")
12 install_on_all(apk, serials)
13
14 labeled: list[str] = []
15 for serial in serials:
16 proc = start_recording(serial)
17 _, ok = run_instrumented_tests(serial, package, runner)
18 stop_recording(serial, proc, local=f"{serial}.mp4")
19 print(f"[{'PASS' if ok else 'FAIL'}] tests on {serial}")
20
21 out = f"{serial}_labeled.mp4"
22 label_clip(f"{serial}.mp4", out, serial)
23 labeled.append(out)
24
25 concat_report(labeled, "test_report.mp4")
26 print("Done: test_report.mp4")
27
28if __name__ == "__main__":
29 main()

여기서 "녹화 + 테스트" 루프는 기기별로 순차적으로 실행됩니다. 이렇게 하면 가독성이 좋습니다. 실제 대규모 환경에서는 이 블록을 ThreadPoolExecutor로 감싸 모든 기기를 동시에 테스트할 수 있습니다. 로직은 섹션 2의 설치 부분과 동일합니다.

처음부터 만들지 마세요: 기성 도구

Ridark - inline image
  • scrcpy: PC에서 기기를 실시간으로 미러링하고 제어합니다. 실패하는 테스트를 디버깅할 때 필수적입니다.
  • Appium / Espresso / UI Automator: 본격적인 UI 테스트 프레임워크입니다. 위의 am instrument가 이들의 엔진입니다.
  • Gradle Managed Devices: 빌드에서 직접 에뮬레이터에서 테스트를 실행합니다. 수동 ADB 작업이 필요 없습니다.
  • Firebase Test Lab / AWS Device Farm: 자체 하드웨어를 유지하고 싶지 않다면 클라우드 기반 실제 기기 팜을 제공합니다.
  • GNU parallel: Python 대신 bash에서 오케스트레이션을 원한다면 사용할 수 있습니다.

경제성: 비용과 절감 효과

테스트 자동화는 "공중에서 돈을 버는 것"이 아닙니다. 가장 비용이 많이 드는 두 가지 항목인 인력 시간클라우드 사용 시간을 줄이는 것이 목표입니다. 아래는 세 가지 일반적인 규모에 대한 추정치입니다. 수치는 예시이며 지역, 기기 제조사, 제공업체 가격에 따라 다르므로 구매 전에 현재 요금을 확인하세요.

자체 팜 구축 -> 일회성 투자

항목

10대 기준

30대 기준

50대 기준

중고 안드로이드 폰 (대략 $60)

~$600

~$1,800

~$3,000

전원 USB 허브

~$100

~$250

~$400

미니 PC / 호스트

~$400

~$400

~$500

케이블, 랙, 기타 잡비

~$80

~$150

~$250

일회성 총계

~$1,200

~$2,600

~$4,150

월 전기료

거의 없음

~$10–20

~$20–40

이는 자본 지출(CAPEX)입니다. 한 번 지불하면 팜을 수년간 거의 비용 없이 사용할 수 있습니다.

클라우드 -> 분당 비용 지불

Firebase Test Lab, AWS Device Farm, BrowserStack 등은 기기-분당 요금을 청구합니다. 대략 기기-분당 $0.05–0.20입니다. 30대 기기에서 각 5분씩 한 번의 회귀 테스트를 실행하면 150 기기-분, 즉 약 $7.5–30입니다.

CI 실행 빈도에 따른 비용:

실행 빈도

월 실행 횟수

비용 (약 $15/회 기준)

하루 2회

~44

~$660/월

하루 10회

~220

~$3,300/월

푸시할 때마다 (활발한 팀)

500+

$7,500+/월

손익분기점: 30대 팜(~$2,600)은 하루 2회 실행 시 약 4개월 만에 클라우드 비용 대비 본전을 회수합니다. 활발한 CI 환경에서는 한 달도 안 걸립니다. 이후에도 클라우드 비용은 매달 계속 발생하지만, 자체 팜은 추가 비용이 없습니다.

수동 작업 -> 절약되는 시간

30대 기기에서 단일 회귀 테스트 시나리오를 수동으로 실행하는 것은 QA 엔지니어의 하루 작업량에 해당합니다. 주 2회 회귀 테스트를 실행하면 월 약 8인-일이 소요됩니다. QA 비용이 월 약 $1,600–2,700임을 감안하면, 이 파이프라인을 통해 "연결-설치-확인-녹화"를 30번 반복하는 지루한 작업 대신 의미 있는 작업에 투입할 수 있는 상당한 인건비가 절약됩니다.

이것이 어떻게 수익으로 이어지는가

"스크립트 자체에서 직접적인 수익"이 발생하는 것은 아닙니다. 하지만 세 가지 간접적이면서도 매우 현실적인 메커니즘이 있습니다:

  • 더 빠른 릴리스. 회귀 테스트가 하루가 아닌 몇 분 만에 완료됩니다 → 기능을 더 자주 출시할 수 있습니다 → 시장에 더 빠르게 대응할 수 있습니다. 구독형 제품의 경우 이는 바로 유지율과 수익으로 이어집니다.
  • 프로덕션 버그 감소. 특정 Samsung 기기에서 발생하는 크래시를 출시 전에 잡는 비용은 몇 푼에 불과합니다. 동일한 크래시가 사용자에게 도달하면 스토어 평점 하락, 이탈, 환불로 이어집니다. 조기에 잡히는 모든 버그는 작성되지 않은 부정적인 리뷰 몇 개와 같습니다.
  • 서비스로 판매 가능. 기기 팜 구축 및 CI 테스트 설정은 프리랜싱과 아웃소싱에서 수요가 높은 역할입니다. 위의 파이프라인은 이러한 서비스의 핵심으로 바로 사용할 수 있습니다.

"참여도 조작 방식"과의 핵심 차이점: 그런 방식에서는 알고리즘을 속여 돈을 벌다가 결국 차단됩니다. 여기서는 절약된 시간과 방지된 손실에서 비용이 발생합니다. 전자는 무너지지만, 후자는 고객에게 부끄럽지 않게 제시할 수 있는 지속 가능한 비즈니스 사례입니다.

마무리

결과는 재현 가능한 파이프라인입니다. 하나의 명령어로 전체 기기 fleet에서 앱을 테스트하고, 각 기기에서의 동작을 보여주는 영상 보고서를 얻을 수 있습니다.

유용한 글을 놓치지 않도록 북마크하고 구독하세요 📝

원클릭 저장

YouMind로 바이럴 글을 AI 심층 읽기

Save the source, ask focused questions, summarize the argument, and turn a viral article into reusable notes in one AI workspace.

Explore YouMind
크리에이터를 위해

당신의 Markdown을 깔끔한 𝕏 글로

직접 쓴 장문을 올릴 때 이미지, 표, 코드 블록을 𝕏에 맞게 정리하는 일은 번거롭습니다. YouMind는 전체 Markdown 초안을 깔끔하고 바로 게시할 수 있는 𝕏 글로 바꿔 줍니다.

Markdown → 𝕏 사용해 보기

분석할 패턴 더 보기

최근 바이럴 아티클

더 많은 바이럴 아티클 보기