
토스의 디바이스 팜 만들기
안녕하세요, 사내 디바이스 팜 '네뷸라(Nebula)'를 만들고 운영하는 토스 Node.js 개발자 차영훈입니다.
먼저 디바이스 팜(Device Farm)이 뭔지부터 간단히 짚을게요. 테스트용 실제 스마트폰 여러 대를 서버에 연결해두고, 원격에서 제어하며 자동화 테스트를 돌리는 설비입니다. 개발자가 자리에 앉은 채로, 기기를 직접 만지지 않고도 실제 폰 위에서 앱을 테스트할 수 있게 해주죠.
네뷸라는 실기기를 24시간 연결해두고, 전사가 API 호출 한 번으로 함께 쓰는 디바이스 팜입니다. 그리고 지금도 수백 대 규모를 향해 계속 증설하고 있고요.

그런데 처음부터 이랬던 건 아닙니다. 작은 팜 몇 개에서 시작해 여기까지 온 이야기를 지금부터 들려드릴게요.
팀마다 각자 폰을 꽂던 시절
먼저 네뷸라가 없던 시절의 풍경을 조금 더 들여다볼게요.
당시 각 팀의 테스트 자동화 환경은 대략 이런 모습이었습니다.
한마디로, 모두가 조금씩 손해 보는 구조였어요. 각 팀은 테스트 코드를 짜고 품질을 높이는 본업 대신 '기기 운영'이라는 부업에 시간을 쓰고 있었습니다. 이 부업을 한 곳으로 모아서 전문적으로 운영하면, 여러 팀이 자기 일에 집중할 수 있겠다는게 네뷸라의 출발점이었어요.
다만 처음부터 거창했던 건 아니에요. 맥미니 5대에 기기 15대, 개발자는 저 한 명이었습니다. 그렇게 6개월 가까이 혼자 고군분투했고, 이후 팀원이 합류하면서 지금은 1년째 운영하고 있어요. 그사이 기기는 100대를 넘어, 수백 대를 향해 꾸준히 늘고 있습니다.
그래서 만들었어요: API 호출 한 번으로
네뷸라의 목표는 처음부터 단 한 문장이었습니다.
누구든, 어디서든, API 호출 한 번으로 실기기를 제어할 수 있게 하자.
이 기기들을 쓰는 쪽은, 기기가 어느 맥미니에 물려 있는지도, ADB(안드로이드 기기를 명령으로 제어하는 도구)나 Xcode를 어떻게 세팅하는지도 알 필요가 없어요. 기기를 하나 점유하고, 액션을 호출하면 끝이죠.
POST /device/occupy { platform: "android", tags: ["smoke"] }
→ 200 assigned: (기기 ID)
POST /actions/click { x: 540, y: 1200 }
→ 200 ok"안드로이드 기기 하나 잡아줘 → 거기 (540, 1200)을 눌러줘." 딱 이 두 줄로, 서버실 어딘가의 실제 기기가 동작합니다. 예약판도, 케이블도, 세팅도 없어요. 이 단순함이 네뷸라가 파는 전부예요. 그리고 이 단순함을 만들기 위해, 뒤에서는 꽤 많은 것이 돌아가고 있습니다.
네뷸라의 아키텍처
네뷸라는 크게 네 겹으로 이루어져 있습니다.

① 클라이언트 — 네뷸라를 부르는 다양한 입구 네뷸라는 API를 호출하는 여러 클라이언트로 열려 있습니다. 예를 들면 화면을 보며 직접 조작하는 Frontend, 테스트 코드에서 불러 쓰는 SDK·CLI, 저수준으로 붙는 API 직접 호출 같은 식이죠.
② 서버 — 전체를 총괄하는 오케스트레이션 계층
기기의 발견·점유·테스트 실행을 관리하는 두뇌입니다. 테스트 실행 요청은 Kafka(대량의 이벤트를 흘려보내는 메시지 큐)로 흐르고, 여러 러너(Runner)가 나눠서 소비해요. 덕분에 실행량이 늘어도 러너를 늘려 수평으로 확장할 수 있습니다. 그리고 한 기기에 두 테스트가 동시에 붙어 서로 간섭하는 일을 막기 위해, occupy · assign · release로 기기에 분산 락(여러 주체가 동시에 접근해도 한 번에 한 명만 쓰게 하는 잠금)을 걸어요.
③ 에이전트 — 기기가 물린 호스트에서 도는 다리 기기가 실제로 연결된 호스트(iOS는 mac mini, Android는 linux)에서 돌면서, 서버의 손발이 되어 로컬 기기를 관리합니다. ADB·Xcode로 연결된 기기를 자동으로 발견하고, 서버의 요청을 각 기기의 컨트롤러로 전달하죠.
④ 기기 — 실제로 손을 대는 맨 아래 계층 호스트마다 기기 하나당 controller server가 하나씩 떠서 그 기기로 가는 요청을 받고, 기기 안의 controller runner가 실제로 화면을 누르고 글자를 넣습니다. 이 맨 아래 계층을 저희는 직접 만들었는데, 사실 이 글에서 가장 강조하고 싶은 이야기가 이 부분이에요.
Appium을 버리고, 직접 만든 Nebula Driver
원래 각 팀의 작은 팜은 전부 Appium으로 돌아갔습니다. 네뷸라도 Appium을 키우는 길로 갈 수 있었죠. 그런데 규모를 키울수록 Appium의 구조적인 한계에 자꾸 부딪혔어요. 그때마다 임시로 고쳐 쓰기보다는, 이럴 바엔 아예 처음부터 우리 드라이버를 만드는 게 낫겠다고 판단했습니다. 이유는 세 가지였어요.
1. 더 빠른 속도
같은 안드로이드 기기에서 명령 지연(p50, 낮을수록 좋음)을 나란히 재봤습니다.
가장 자주 쓰는 클릭·입력에서 네뷸라가 10배 넘게 빨랐어요. 이 격차의 상당 부분(클릭은 약 80%)은, Appium이 견고성을 위해 매 동작 전에 화면이 안정될 때까지 기다리는(waitForIdle) 시간입니다. 그 대기를 끄면 2~3배로 좁혀지고요. 즉 Appium은 '진행 중인 화면을 피하는 견고성 우선', 네뷸라는 '노드에 바로 걸고 즉시 반환하는 속도 우선'이라는 철학의 차이가 있습니다. 서버실 기기를 실시간으로 보면서 조작해야 하는 우리에게는 속도 우선이 맞았어요.
2. stateless로 걷어낸 세션 정책
Appium은 세션 기반입니다. 그 세션이 이런 문제를 만들어요.
네뷸라는 이 구조를 근본적으로 걷어냈어요. 기기 컨트롤러를 상시 띄워두고(pre-warm), 그 위로 상태 없는(stateless) HTTP 호출을 던져요. 세울 세션도, 매번 치를 시작 비용도, 규모에 비례해 늘던 세션 실패도 없죠.
3. 토스에 맞게 커스텀할 수 있어요
인터페이스 스펙을 우리가 소유하니, 필요한 건 뭐든 넣을 수 있습니다.
덕분에 앞으로 커스텀이 필요한 부분이 생겨도, 남의 도구를 기다릴 필요 없이 바로바로 고쳐서 적용할 수 있게 됐어요.
이 세 가지를 위해, 컨트롤러를 기기 계층부터 다시 만들었습니다. Android는 ADB와 UiAutomation, iOS는 Swift·XCTest 위에 얇게 얹고, 인터페이스는 OpenAPI 스펙 하나로 정의해 Go·TypeScript 코드를 자동 생성했죠.
결국 관점의 차이예요. Appium은 고도화된 무거운 범용 도구로, 모든 앱과 기기를 견고하게 떠받치도록 만들어졌습니다. 반면 Nebula Driver는 가볍고 빠른 전용 도구로, 우리 환경에 필요한 것만 골라 담은 툴이죠. 둘 다 좋은 도구이지만, 토스의 문제를 해결하기에는 후자가 맞았습니다.
진짜 어려웠던 건 '실시간' 문제
일반적인 테스트 자동화는 정해진 스텝을 순서대로 재생하면 그만입니다. 화면을 실시간으로 지켜볼 필요가 크지 않죠. 그런데 네뷸라의 요구는 조금 더 무거웠어요.
기기가 서버실에 있으니 직접 보거나 만질 수가 없어요. 그래서 테스트를 만드는 사람이, 기기 화면을 실시간으로 보면서 동시에 조작할 수 있어야 했어요. ‘보면서 동시에 조작’, 이 한 문장이 네뷸라에서 가장 오래 저희를 붙잡은 요구였습니다. 이걸 만족하려면 실시간 조작과 실시간 화면 미러링이 동시에 돌아야 하고, 그걸 Android와 iOS 양쪽에서 각각 풀어야 했거든요.
Android 미러링 — scrcpy를 걷어내고 직접 내재화
안드로이드 미러링에는 scrcpy라는 훌륭한 오픈소스가 있습니다. 하지만 이건 '안드로이드 화면 → 데스크톱 앱'을 전제로 만들어졌어요. 자체 클라이언트와 프로토콜이 필요하고, 서버를 거쳐 브라우저로 여러 명에게 뿌리는 구조에는 맞지 않았죠.
그래서 scrcpy를 그대로 쓰는 대신 인코딩 방식만 참고해 직접 내재화했어요. 기기 안에서 SurfaceControl(안드로이드 화면 출력 계층)로 가상 디스플레이를 만들고, MediaCodec으로 H.264 영상으로 인코딩합니다. 이걸 브로드캐스터로 보내면, 브라우저 시청자 여러 명에게 동시에 뿌릴 수 있어요.

iOS 미러링 — 'USB라는 벽'과 가장 오래 싸웠어요
iOS는 안드로이드처럼 화면을 자유롭게 뽑을 수가 없습니다. 기존 방식들은 하나같이 ‘보면서 동시에 조작’이라는 요구에 부딪혔어요.
앞의 두 방식(QVH·Appium MJPEG)은 저희 요구를 만족하지 못했어요. 그래서 QuickTime Player가 iOS 화면을 가져올 때 쓰는 것과 같은 캡처 장치를, USB를 독점하지 않는 경로로 활용하는 방식을 자체적으로 내재화했어요. USB를 통째로 잡지 않으니, 조작과 미러링이 비로소 공존할 수 있었죠.

이렇게 Android·iOS를 모두 실시간 H.264 + 브로드캐스팅이라는 하나의 경로로 통일한 덕분에, 이제는 브라우저 한 화면에서 디바이스 팜 전체를 동시에 띄워 놓고 볼 수 있습니다.

기술만큼 어려웠던 것 — 보안과 컴플라이언스
사실 네뷸라를 만들며 어려웠던 건 기술만이 아니었습니다. 그만큼, 어쩌면 그 이상으로 신경 쓴 게 보안과 컴플라이언스였어요.
팀마다 각자 팜을 굴리던 시절에는, 개발자 한 명이 테스트도 하고 기기도 관리하면서 보안·컴플라이언스까지 빠짐없이 챙기기가 현실적으로 어려웠어요.
회사 입장에서도 이런 규모의 디바이스 팜을 구축하는 건 처음 있는 일이었어요. 그래서 저희는 처음부터 사내 보안팀과 긴밀히 협력했어요. 모바일 기기에 대한 보안·컴플라이언스 요건을 위반하지 않는 선에서 팜을 운영하려면 무엇을 지켜야 하는지 함께 정의하고, 그 기준이 기기와 플랫폼 전반에 빠짐없이 적용되도록 만들었죠.
이건 중앙화하고 직접 내재화했기에 얻을 수 있었던 이점이기도 해요. 각 팀이 흩어져 운영할 때는 보안 기준을 똑같이 맞추기가 어려웠지만, 하나의 플랫폼으로 모으니 보안팀과 정한 기준을 모든 기기에 일괄로 적용할 수 있었어요. 덕분에 네뷸라를 쓰는 팀원들은, 보안 사항이 모두 적용된 기기 위에서 안심하고 테스트에만 집중할 수 있게 됐고요.
수많은 기기를 24시간 살아있게 — 운영과 안정성
이 많은 기기를 24시간 열어둔다는 것은 그만큼 끊임없이 죽고 되살아나는 것들을 관리한다는 뜻입니다. 그리고 이 문제는 손에 잡히는 하드웨어, 그리고 그 위에서 도는 소프트웨어 두 층위로 나뉘어요.
하드웨어 관점
사실 일반적인 서비스 개발자라면 평생 해볼 일 없는 이런 하드웨어 고민까지 직접 부딪혀본 것은 오히려 흔치 않은 좋은 기회였습니다.
소프트웨어 관점
이 모든 게 결국 한 가지 이유 때문입니다. 다양한 팀에서 쓰다 보니, 네뷸라는 밤이고 낮이고 쉼 없이 돌아가거든요. 누군가는 항상 쓰고 있으니, 안정성은 네뷸라에게 기능만큼이나 큰 과제였어요.
지금, 이만큼 쓰이고 있어요
기기 15대와 개발자 1명으로 시작한 플랫폼이 지금은 이렇게 자랐습니다.
무엇보다 인상적인 건, 밑바탕에 ‘API 호출 한 번으로’라는 단단한 API가 깔리자 그 위로 활용이 알아서 늘어났다는 점입니다. 저희가 다 설계하지 않았는데도요. 지금은 같은 네뷸라 API를 이렇게 여러 갈래로 가져다 쓰고 있습니다.
이 모든 도구가 공개된 API만으로 만들어졌습니다. 잘 뚫린 하나의 관문이, 그 위의 생태계를 끌어올린 셈이죠.
실제로 쓰는 분들의 목소리도 조금 옮겨볼게요.
💬 "Appium에서 거의 그대로 옮겼는데도 테스트가 훨씬 빨리 돌아요. 덕분에 회귀 테스트를 붙이는 게 부담이 없어졌어요." — 프레임워크팀 개발자💬 "사람이 직접 검수하면 OS 합쳐 30~40분 걸리던 걸, 네뷸라로는 10분 안에 자동 검증해요." — 서비스 개발팀
기기를 넘어, '테스트를 빠르게 구축하는 환경'으로
지금까지 네뷸라가 잘하게 된 건 기기를 빠르게 제공하는 일입니다. 하지만 저희가 보는 다음 단계는 조금 더 위에 있습니다. 더 많은 팀이, 더 쉽게 테스트를 만들 수 있는 환경이에요.
방향은 두 가지예요.
조작 속도에 이어 조건 준비까지 값싸지면, 여러 팀이 변인 통제에 시간을 쓰지 않고 테스트 그 자체에만 집중할 수 있어요.
이 일이 지금 중요한 이유
마지막으로, 저희가 이 일을 왜 중요하게 여기는지 이야기하고 싶습니다.
AI 덕분에 코드를 만드는 비용은 극적으로 줄었어요. 그런데 재미있게도, 그만큼 코드를 리뷰하고 검증하는 비용은 오히려 늘었습니다. 만들어지는 양이 폭발하니까요. 예전엔 사람이 짠 코드를 사람이 리뷰하면 얼추 균형이 맞았는데, 이제는 생산 속도가 검증 속도를 한참 앞지르고 있습니다.
이 검증을 사람이 전부 감당하는 건 불가능합니다. 결국 검증도 자동화해야 하죠. 그런데 모바일 서비스에서 자동화된 검증이 의미를 가지려면, 그 전제로 실물 기기 위에서의 테스트가 받쳐줘야 해요. 그리고 실물 기기 위 테스트를 안정적으로 제공하려면, 결국 잘 관리되는 디바이스 팜이 필요합니다. 네뷸라가 하는 일이 바로 이 맨 아래를 받치는 일이에요.
그럼 에뮬레이터나 시뮬레이터가 아니라 왜 굳이 실기기일까요? 이유는 분명합니다. 유저가 실제로 경험하는 환경은 진짜 휴대폰이고, 그걸 최대한 그대로 재현하는 게 중요하기 때문이에요. 게다가 에뮬레이터·시뮬레이터는 생각보다 컴퓨팅 자원을 많이 먹어서, 대수를 늘리는 scale out이 실기기보다 오히려 어려운 면도 있습니다. (물론 에뮬레이터·시뮬레이터도 그 나름의 쓸모가 분명히 있어서, 앞으로 함께 지원해 나갈 계획이에요.)
그럼 AWS Device Farm 같은 Third-party 서비스를 쓰면 되지 않을까요? 그것도 좋은 선택지이지만, 저희에게는 맞지 않았습니다. 토스 앱에 필요한 커스텀을 마음대로 넣기 어렵고, 금융 서비스의 보안·컴플라이언스 기준을 우리 방식으로 강제하기도 힘들기 때문이에요. 무엇보다 실기기와 드라이버를 우리가 소유해야 지금까지 이야기한 속도·실시간·보안·확장을 우리 문제에 딱 맞게 풀 수 있었습니다.
정리하면, 네뷸라는 팀마다 각자 고생하던 작은 팜들을 하나로 모아, 수백 대를 향해 늘어나는 실기기를 API 하나로 열고, Appium 대신 자체 드라이버로 빠르게 만들고, 미러링부터 입력까지 토스에 맞게 특화한 플랫폼입니다. 그리고 이제는 기기를 넘어, AI 시대에 폭증하는 검증을 떠받치는 테스트 인프라로 나아가고 있어요.
기기 15대와 개발자 한 명으로 시작한 일이 여기까지 왔습니다. 혼자 시작한 프로젝트가 여기까지 올 수 있었던 것은, 함께 네뷸라를 만들고 지켜주는 팀 덕분이에요. 다음 이야기도 기대해 주세요.
읽어주셔서 감사합니다.
