안드로이드 MCP: Claude로 클라우드 안드로이드(ReDroid) 앱 조작하기

요즘은 핸드폰에 모든 것을 저장한다. 은행/카드 등 모든 정보가 저장되어 있고, 핸드폰을 분실하면 은행 업무부터 막힐 것이다.

PC는 데이터를 클라우드(MS OneDrive)에 저장해 두면, Windows가 설치된 다른 PC에서 내 OneDrive 계정으로 로그인만 해도 대부분 복원된다. 하지만 핸드폰은 글쎄… 폰이 없는 상태에서는 아무것도 할 수 없다.

그래서 폰 환경 자체를 클라우드에 두는 방법을 시도했다. 이 글의 주제는 MCP다. 클라우드 안드로이드(ReDroid)에서 Claude가 MCP로 앱을 조작하게 해 본 과정과 결과, 그리고 그 과정에서 다시 생각하게 된 시큐어 코딩 이야기를 정리한 것이다. 원래는 이 환경을 바로 실사용하려 했지만, 보안 사고를 계기로 지금은 꺼 두었고 다시 켤 때는 VPN 안에서만 접근하도록 하기로 했다. 그 계획은 뒤쪽에 따로 적었다.

클라우드에 둔 폰에는 MCP(Model Context Protocol)도 연결했다. Claude가 화면을 보고 탭하고 스와이프하게 하는 방식인데, 써 보니 특히 금융 앱에서 쓸모가 커 보였다. 금융 앱은 잔액 변동이나 거래 내역을 주기적으로 확인해야 하는데, 개인이 직접 쓸 수 있는 API는 거의 없다(오픈뱅킹·마이데이터는 사업자용이다). PC(Windows)에서는 서비스마다 설치된 개별 보안 프로그램 때문에 자동화가 어렵다. 반면 안드로이드는 보안 프로그램과의 결합이 PC만큼 강하지 않아서, 화면을 읽고 조작하는 방식이 기술적으로 가능하다. 내가 써 본 범위에서의 판단이다.

먼저 밝혀 둔다. 이 글은 보안 우회 방법을 다루지 않는다. 테스트는 모두 내 기기와 내 계정으로, 소액으로만 진행했고 개인정보는 가림 처리했다.

글이 길어서 읽는 순서를 먼저 적어 둔다.

  1. ReDroid 구성과 금융 앱 실행 결과
  2. MCP 활용 사례: 앱 설치와 매뉴얼, 퍼즐게임, 은행 이체, 은행 앱 동시 실행, 금융 앱 조회 자동화에 대한 생각
  3. 앱이 실행 환경을 믿는 방식, SELinux, 시큐어 코딩 이야기
  4. 운영 환경(Oracle Free Tier)과 VPN 전용 접근 계획, 정리

왜 ReDroid인가

클라우드에서 안드로이드를 돌리는 후보는 세 가지였다.

  • Waydroid: 로컬에서 화면을 가진 환경에 특화. 클라우드에서는 화면이 필요 없어서 제외
  • Cuttlefish: Google의 가상 디바이스. 커스터마이징 등을 고려해 제외
  • ReDroid: Docker, podman, Kubernetes로 여러 인스턴스 실행 가능. arm64와 x86_64 지원, Android 8.1부터 16까지 이미지 제공

결국 화면 없이 컨테이너로 띄울 수 있고 ARM 서버에서도 돌아가는 ReDroid를 선택했다.

전체 구성

flowchart LR
  CL["🤖 Claude<br/>MCP 클라이언트"] -->|"스크린샷 · 탭 · 스와이프 · 키"| MCP["MCP 인터페이스"]
  MCP --> RD["📦 ReDroid 컨테이너<br/>클라우드 안드로이드"]
  PH["📱 내 폰 / 💻 PC<br/>웹브라우저 카메라 · 센서"] <-->|"WebSocket"| RD
  RD --> APP["📲 앱"]
  style RD fill:#ffe6cc,stroke:#f57c00
  style CL fill:#dbe7ff,stroke:#2563eb

그림 1. ReDroid에 MCP로 Claude를 연결하고, 카메라 등 센서는 내 폰이나 PC의 웹브라우저에서 WebSocket으로 공급하는 구성

컨테이너에는 카메라 같은 하드웨어가 없다. 그래서 내 폰이나 PC의 웹브라우저를 WebSocket으로 안드로이드에 연결해, 브라우저의 카메라와 각종 센서를 그대로 이용했다. 구조는 WebSocket으로 가상 센서 구현하기에 정리해 두었다.

ReDroid의 신분증 촬영 화면에 웹브라우저의 카메라 영상이 들어가는 모습. 상단에 외부 장치 ON/OFF 스위치, 하단에 안드로이드 Home/Recent 버튼 표시
신분증 촬영 화면에 가상 카메라로 웹브라우저의 카메라 영상이 들어가는 모습 (① 외부 장치 ON/OFF 스위치, ② 안드로이드 버튼)

그림 속 표시는 다음과 같다.

  • ① 외부 장치 연결 스위치: 안드로이드에 연결하는 외부 장치(마이크, 스피커, 카메라)를 켜고 끄는 스위치. 화면의 Mic, Speaker, Camera 버튼이 해당
  • ② 안드로이드 버튼: 하단의 Home과 Recent는 안드로이드의 홈과 최근 앱 버튼. 화면 위에서 터치로 누르는 방식

금융 앱 실행 결과

금융 앱은 설치와 실행이 까다로웠다. 앱별 결과는 아래와 같다.

앱LineageOS 실제 디바이스ReDroid (에뮬레이터 환경)
앱 A사용 가능실행 환경 검사로 실행되지 않음
앱 B—실행됨, 소액 이체까지 진행

작성자 환경에서의 결과이며, 앱 버전과 환경에 따라 달라질 수 있다. —는 이 글에서 다루지 않은 항목.

앱 A가 LineageOS를 올린 실제 디바이스에서는 되는데 에뮬레이터에서는 실행되지 않았다는 점이 눈에 띈다. 앱 코드는 같고 달라진 것은 실행 환경뿐이기 때문이다. 이 차이는 아래 ‘앱은 실행 환경을 어떻게 믿는가’에서 정리한다.

MCP 활용 사례

화면 캡처와 터치 같은 조작을 MCP로 노출하면 Claude가 화면을 보고 사람처럼 앱을 조작할 수 있다. MCP로 연 도구는 스크린샷, 탭, 스와이프, 키 입력 네 가지뿐이다. 사람이 폰을 쓰는 동작과 같다. 이 구성으로 성격이 다른 사례 4개를 시도해 보았다.

사례보여주려는 것결과
① 앱 설치와 매뉴얼 작성탐색과 문서화영상과 PDF
② 퍼즐게임 플레이판단과 조작2048 완성
③ 은행 이체고위험 작업과 안전장치은행 이체 성공
④ 은행 앱 2개 동시 실행운영 관점의 교훈결론만 기록 (영상 비공개)

활용 사례 ① 앱 설치와 매뉴얼 작성

메뉴와 설정이 여러 개 있는 일반 앱을 대상으로, Claude가 Play 스토어에서 앱을 찾아 설치하고 주요 화면을 둘러본 뒤 초보자용 사용 매뉴얼을 쓰게 했다. 대상은 네이버 파파고다. 로그인 없이 쓸 수 있고 화면 이동이 많아서 매뉴얼로 정리할 내용이 많은 앱이다.

  • 환경 구성: 일본어 메뉴판 이미지를 OBS 가상 카메라의 입력으로 쓰고, 웹브라우저에서 웹캠을 OBS 가상 카메라로 선택했다. 이 영상이 WebSocket을 거쳐 안드로이드의 카메라 영상으로 들어간다
  • 흐름: 설치, 실행, 이미지 번역 메뉴 탐색과 언어 설정(일본어→한국어), 촬영, 화면별 캡처, 매뉴얼 작성. 카메라 권한은 이미 허용된 상태라 권한 팝업은 나오지 않았다
  • 결과물: 캡처 이미지와 설명이 짝으로 묶인 12쪽 PDF 매뉴얼

(영상은 2배속이며, 개인정보 보호를 위해 채팅의 접속 정보와 앱 목록을 가림 처리했다.)

결과: Claude가 Play 스토어에서 파파고를 설치하고, 이미지 번역으로 일본어 메뉴판을 한국어로 번역해 본 뒤, 12쪽짜리 사용 매뉴얼을 만들었다. 캡처마다 빨간 박스와 화살표를 넣고 단계별 설명과 목차를 붙였다. 만들어진 매뉴얼은 여기(PDF)에서 볼 수 있다. 3쪽은 앱 목록을 가리느라 이미지로 처리해서 글자 선택은 되지 않는다.

활용 사례 ② 간단한 퍼즐게임 플레이

게임에는 매뉴얼이 필요 없고 플레이 자체가 볼거리다. 2048이나 스도쿠처럼 턴제로 진행되는 퍼즐은 화면 캡처와 조작이 오가는 지연이 문제 되지 않고, 상태가 명확해서 판단이 맞았는지 바로 확인할 수 있다.

  • 흐름: 스크린샷으로 판을 읽고, 다음 수를 계산해서 스와이프하는 과정을 반복
  • 모델: Opus로 돌려 보았다. Sonnet이 그때그때 상황에 맞춰 수를 두었다면, Opus는 스스로 퍼즐을 푸는 프로그램을 만들고 그 프로그램으로 해법을 찾아 문제를 풀었다. 이 점이 인상 깊었다

결과: 2048 타일에 도달했고 점수는 20,352점이었다. 승리 화면은 사람이 넘겨 주었다. (영상은 2배속이며, 중간 구간은 녹화가 유실되어 앞부분과 결과 화면만 담았다.)

활용 사례 ③ 은행 이체: 사람이 개입해야 하는 지점

은행 앱에서 Claude로 이체를 진행해 본 결과가 아래 영상이다. (영상은 2배속이며, 개인정보 보호를 위해 가림 처리했다. 테스트는 내 기기와 내 계정 사이의 100원 소액 이체로만 진행했다. 특정 은행 앱을 평가하려는 것이 아니라 에이전트가 어디서 멈추는지를 보려는 실험이다.)

테스트하면서 확인한 점은 다음과 같다.

  • 모델 선택: Haiku로는 화면을 제대로 이해하지 못해서 Sonnet으로 변경
  • 중복 이체 경고: 앱이 ‘오늘 동일한 이체 건이 있다’는 경고를 띄우자 Claude가 임의로 진행하지 않고 멈춰서 사용자에게 확인
  • 간편비밀번호 단계: Claude가 비밀번호를 추측해서 입력하지 않겠다고 밝히고 사용자에게 요청
  • 키패드 점검: 비밀번호를 전달받은 뒤, 숫자를 누를 때마다 키패드 배열이 바뀌는지 먼저 확인하고 나서 입력했다. 경우에 따라 매번 키패드가 바뀌는 앱도 있다는 것을 이미 알고 있다는 점이 놀라웠다

비밀번호 단계는 실제로 써 보면 중요하다. 이번에는 비밀번호를 대화로 전달해 Claude가 입력하게 했지만, 비밀 값은 대화에 남기지 않는 것이 맞다. 환경변수나 Vault 같은 비밀 저장소에 두고 꺼내 쓰는 방식이 더 낫다고 본다. 이 영상에서도 해당 구간은 가렸다.

①②는 ‘Claude가 할 수 있는 일’을 보여 주고, ③은 ‘어디까지 맡기면 안 되는가’를 보여 준다. 중복 이체 경고에서 멈추고 비밀번호를 추측하지 않은 것처럼, 돈이 오가는 단계는 사람이 최종 승인하는 구조가 맞다고 생각한다. 참고로 은행 앱을 자동화하는 것은 약관상 제한될 수 있고 보안 위험도 있으므로, 이 글은 실험 기록이지 따라 해 보라는 권장이 아니다.

활용 사례 ④ 은행 앱 2개 동시 실행: 그래서 클라우드 폰을 껐다

ReDroid를 하나 더 띄워 같은 은행 앱을 두 개 실행해 보았다. 예상과 다른 결과가 나왔지만, 자세한 내용은 적지 않는다. (내 기기와 내 계정으로 소액만 테스트했고, 영상은 공개하지 않는다.)

이것이 앱 쪽의 문제인지는 내가 판단할 수 없고, 이 글에서 따질 일도 아니다. 다만 환경을 운영하는 입장에서 분명해진 것이 있다. 로그인된 금융 앱이 올라간 환경은 그 자체가 민감한 자산이라는 점이다. 이 환경이 통째로 털리면 복제폰처럼 쓰일 수 있다는 걱정을 무시할 수 없다.

그래서 클라우드 폰은 지금 꺼 두었다. 다시 켤 때는 뒤쪽의 VPN 전용 접근 계획대로 막아 둔 상태에서만 쓸 생각이다. 앱 보안에 관해 확인이 필요한 부분이 생기면 블로그가 아니라 해당 금융사에 직접 알리는 것이 맞다고 생각한다. 그래서 이 사례는 영상 없이 결론만 적는다.

왜 금융 앱에 MCP가 잘 맞는다고 보는가

서론에서 말한 금융 앱 이야기를 조금 더 정리하면 다음과 같다. 내가 써 본 범위에서의 개인적인 판단이다.

  • 개인용 API가 거의 없다: 오픈뱅킹·마이데이터는 사업자용이라, 개인이 내 내역을 확인하려면 앱을 직접 열어 봐야 한다
  • Windows에서는 자동화가 어렵다: PC에서는 금융 서비스마다 개별 보안 프로그램이 설치되어 있어서 화면을 읽고 조작하는 식의 자동화를 붙이기 어렵다
  • 안드로이드는 화면 기반 접근이 가능하다: 안드로이드에도 보안 프로그램이 없는 것은 아니지만(V3 같은 앱 설치를 요구하기도 한다), PC처럼 보안 프로그램과 매우 강하게 결합된 상태는 아니어서 화면을 캡처하고 터치하는 MCP 방식이 기술적으로 가능하다
  • 내 화면을 대신 읽는다: Claude가 사람처럼 화면을 보고 읽으므로, 내가 앱에서 볼 수 있는 내 잔액과 내역을 정리하는 용도로 쓸 수 있다

다만 보안 프로그램과의 결합이 약하다는 것이 ‘검사가 없다’는 뜻은 아니다. 앞서 본 앱 A처럼 앱 자체가 실행 환경을 검사하는 경우는 여전히 있고, 그래서 에뮬레이터에서 실행되지 않기도 한다. 따라서 현실적인 활용 범위는 조회로 한정하는 것이 맞다고 생각한다.

  • 조회 위주: 잔액과 거래 내역을 주기적으로 확인하고 변동이 있으면 알려 주는 용도
  • 자금 이동은 사람이: 이체처럼 돈이 움직이는 단계는 위 사례 ③처럼 사람이 최종 승인
  • 비밀번호는 대화에 남기지 않기: 사용자가 직접 입력하거나 환경변수·Vault 같은 비밀 저장소에서 꺼내 씀
  • 약관 확인: 서비스마다 자동화 이용을 제한할 수 있으므로 사용 전에 확인

주기적으로 실행하는 조회 자동화는 아직 구상 단계이고, 해 보면 결과를 이어서 정리할 생각이다.

앱은 실행 환경을 어떻게 믿는가 (추정)

금융 앱이 실행 환경을 확인하는 대표적인 방법 중 하나가 Google Play Integrity API이다. 공식 문서 기준으로 판정(verdict)은 다음과 같이 나뉜다.

  • MEETS_DEVICE_INTEGRITY: 정품 인증 안드로이드 기기. Android 13 이상에서는 부트로더가 잠겨 있고 제조사 인증 이미지가 올라가 있다는 하드웨어 기반 증명이 필요
  • MEETS_STRONG_INTEGRITY: 위 조건에 더해 최근 1년 이내 보안 업데이트가 적용된 기기 (Android 13 이상 기준)
  • 에뮬레이터와 루팅 등으로 변조된 기기: 위 판정을 받지 못함

앱 A가 정확히 어떤 검사를 쓰는지는 내가 확인하지 못했다. 다만 실제 디바이스에서는 되고 에뮬레이터에서는 안 된다는 패턴은 이런 기기 증명 계열의 검사와 잘 맞는다. ReDroid는 제조사가 인증한 이미지와 잠긴 부트로더를 가진 기기가 아니기 때문이다. 어디까지나 정황에 기반한 추정이다. 중요한 점은 이런 판정을 앱이 아니라 앱 서버가 검증하도록 설계되어 있다는 것이다.

소주제: SELinux, 앱이 쓰는 건 처음 봤다

이 과정에서 새로 눈에 들어온 것이 SELinux다. Android 공식 문서 기준으로 SELinux는 root 권한 프로세스까지 포함한 모든 프로세스에 강제 접근 제어(MAC)를 적용하는 장치이고, ‘명시적으로 허용하지 않은 것은 거부’가 기본 원칙이다.

  • Enforcing: 위반을 기록하고 실제로 차단. Android는 이 모드로 출하되며 Android 5.0 이후 완전 적용
  • Permissive: 위반을 기록만 하고 차단하지 않음. 정책 개발과 디버깅용
  • 앱 샌드박스: Android 4.3부터는 앱마다 고유한 UID에 더해 SELinux가 샌드박스의 경계를 정의

그동안 SELinux는 OS(프레임워크) 쪽이 앱을 가두기 위해 쓰는 도구라고만 생각했다. 그런데 이번에 금융 앱이 자기가 돌고 있는 환경의 SELinux 상태까지 신호로 살펴본다는 것을 알게 됐다. 가둬지는 쪽인 앱이 거꾸로 환경을 확인한다는 발상이 신선했고, 보안 설계 관점에서 배울 점이 많았다. 참고로 SELinux는 안드로이드에서는 기본이지만, Ubuntu에서는 AppArmor를 사용한다. ReDroid는 이런 일반 리눅스 위에서 돌아가므로, 이러한 환경 차이에 대한 인식이 필요하다.

컨테이너는 호스트 커널을 공유하는데, ReDroid 공식 문서에도 Fedora 38 호스트에서는 SELinux를 먼저 꺼야 한다고 적혀 있다. 즉 컨테이너 환경의 SELinux 상태는 실제 폰과 다를 수 있고, 앱이 이를 환경 신호로 쓴다면 실제 기기와 에뮬레이터를 구분하는 단서가 될 수 있다. 어디까지나 가설이다.

금융 앱이 실행 환경을 이렇게까지 따지는 이유는 반대로 생각하면 이해가 된다. 이 앱이 어떤 환경에서 돌고 있는지를 모르면 앱 내부의 보호 장치를 믿을 수 없기 때문이다. 여기서 시큐어 코딩 이야기로 넘어간다.

시큐어 코딩에 대한 생각/경험

예전에 하드웨어 소모품 인증 같은 보안 모듈 개발에 참여한 적이 있다. 보안은 두더지 잡기가 아니라 톰과 제리다. 이미 고객에게 간 코드는 언젠가 풀릴 가능성이 높고, AI 때문에 그 속도는 더 빨라질 것이다. 공개된 자료도 같은 이야기를 한다.

  • OWASP 모바일 보안 검증 표준(MASVS)에는 변조와 역공학에 대한 저항(MASVS-RESILIENCE)이 별도 영역으로 있다. 난독화와 변조 탐지는 막는 장치가 아니라 시간을 버는 장치다
  • 2022년 USENIX Security에서 삼성 갤럭시(S8~S21) 하드웨어 키스토어의 설계 결함(CVE-2021-25444, CVE-2021-25490)이 보고됐다. 하드웨어 안의 키도 구현이 틀리면 뚫린다 (패치는 2021년 8월과 10월)
  • Play Integrity도 판정은 앱이 아니라 앱 서버가 검증한다. 클라이언트 안의 검사는 결국 클라이언트가 가장 잘 속일 수 있기 때문이다

그래서 내 결론은 제로 트러스트(Zero Trust)다. 기기도, 앱도, 네트워크도 기본으로 믿지 않고 매번 검증한다는 원칙이다.

  • 핵심 판단은 서버에서: 금액 한도, 이상 거래 탐지, 기기 증명 검증
  • 뚫린다는 전제: 탐지하고 폐기하고 갱신할 수 있는 경로를 처음부터 설계
  • 에이전트도 예외가 아니다: AI가 앱을 조작하더라도 돈이 움직이는 단계는 사람이 승인하고, 비밀 값은 대화에 남기지 않는다

다시 밝혀 두면, 이 글은 보안 우회 방법을 설명하는 글이 아니다. 테스트는 내 기기와 내 계정으로, 소액으로만 진행했다.

운영 환경: Oracle Cloud Free Tier

Oracle Cloud Free Tier가 ARM 인스턴스(Ampere A1)를 지원하므로, 그 환경에 ReDroid를 올려서 사용하면 된다. 공식 Always Free 리소스 문서 기준으로 알아 둘 점은 다음과 같다.

  • ARM(VM.Standard.A1.Flex) 한도: 월 1,500 OCPU 시간과 9,000 GB 시간, Always Free 테넌시 기준 2 OCPU와 메모리 12GB에 해당. 한 인스턴스에 몰아 쓰거나 1 OCPU 인스턴스 2개로 분할 가능
  • 스토리지: 부트 볼륨과 블록 볼륨을 합쳐 200GB, 아웃바운드 전송 월 10TB
  • 리전: Always Free 컴퓨트는 홈 리전에서 생성해야 하며, A1 인스턴스는 한국 북부(춘천) 리전 제외
  • 유휴 인스턴스 회수: 7일간 CPU 95퍼센타일, 네트워크, 메모리(A1)가 모두 20% 미만이면 유휴로 판단되어 회수될 수 있음

이 사양에서 ReDroid는 매우 쾌적하게 돌아갔고, 두 개를 띄웠을 때도 사용에 문제가 없었다. 다만 유휴 회수 조건은 상시로 띄워 두는 용도에서는 신경 써야 한다. ReDroid 자체의 준비물은 문서 기준으로 다음과 같다.

  • 호스트 커널: binder 모듈(binder, hwbinder, vndbinder)과 ashmem_linux 로드 필요
  • 실행: Docker에서 --privileged로 컨테이너 실행, adb 포트 5555 사용
  • MCP 등을 사용하려면 별도의 안드로이드 빌드 필요

공개 전에 보안부터: VPN 안에서만

이 환경은 금융 앱과 로그인 정보가 올라가는 곳이라 노출되면 피해가 크다. 보안 사고를 계기로 바로 실사용하려던 계획을 접었고, 지금은 꺼 둔 상태다. 다시 켤 때는 외부에서 직접 접근할 수 없게 막은 상태로만 쓸 생각이다. 아래는 그 계획이다.

  • 접근 경로: VPN(WireGuard, Tailscale 등 후보)에 접속한 기기에서만 접근 가능하도록 제한
  • 포트 노출 금지: adb(5555)와 원격 화면 중계 등 모든 서비스 포트를 공인 IP에 직접 열지 않음. ReDroid 문서도 adb 포트를 공개 네트워크에 노출하지 말라고 경고
  • 방화벽: Oracle 쪽 네트워크 규칙(보안 목록 등)에서 VPN 대역과 필요한 포트만 허용
  • 환경 복사 주의: 금융 앱이 설치된 환경의 백업, 스냅샷, 이미지도 로그인 정보가 남아 있을 수 있으므로 같은 수준으로 보호하고 외부에 두지 않음
  • 사용 방식: 소액 테스트 위주, 비밀번호는 대화에 남기지 않고 직접 입력하거나 비밀 저장소에서 꺼내 씀

VPN 구성은 아직 계획 단계이고, 적용하고 나면 방법과 확인 결과를 이어서 정리할 생각이다.

정리

  • 화면 조작 도구 네 개만 MCP로 열어 줘도 Claude는 앱 설치, 화면 탐색, 매뉴얼 작성, 게임 플레이까지 해냈다
  • QA 관점에서 쓸모가 크다고 본다. 사람이 하던 탐색적 테스트와 화면별 기록, 매뉴얼 작성을 에이전트가 맡고, 사람은 결과를 검토하는 구조가 가능하다
  • 경계도 분명했다. 돈이 움직이는 단계는 사람이 승인하고, 비밀 값은 대화에 남기지 않고, 중요한 판정은 서버에 둬야 한다
  • 로그인된 앱이 올라간 환경은 그 자체가 민감한 자산이다. 그래서 지금은 꺼 두었고, 다시 켤 때는 VPN 전용으로만 쓴다

참고 자료

어디에도 기록 안 된 줄 알았던 서브도메인이 털렸다 — @@Dh2OA

사이트 하나가 털렸다. flywithu.com 이야기는 아니고, 개인적으로 아주 작게 쓰던 기록용 사이트다. 아이들 학원비 납부 여부를 월별로 체크하는 정도의 페이지였다. 주소는 어디에도 링크를 걸거나 공유한 적이 없는 서브도메인이었다. 외부 어디에도 기록되지 않은 도메인이라 노출될 일이 없다고 생각했다.

그런데 어느 날 들어가 보니 기존 데이터는 사라졌고, 과목명 자리에 @@Dh2OA라는 정체불명의 한 줄만 남아 있었다.

학원비 관리 페이지에 @@Dh2OA라는 항목만 남아 있는 화면
남아 있던 흔적. 개인 정보는 블러 처리.

피해와 조치

  • 피해: 기존 기록 삭제, 의미 없는 항목 1건 추가
  • 복구: 주기적으로 백업해 둔 데이터로 전체 복원
  • 조치: 로그인 추가. “어차피 아무도 모르는 주소”라는 이유로 인증 없이 열어 두었던 것이 가장 큰 원인

결국 핵심은 나의 무관심이다. 그래도 궁금한 건 남는다. 어떻게 이 주소를 알았고, 왜 굳이 들어와서 이런 걸 남겼을까.

어떻게 찾아왔을까 — 추정

로그를 깊게 본 것은 아니라 확정할 수는 없다. 다만 가장 유력한 경로는 인증서 투명성(Certificate Transparency, CT) 로그다.

  • Let’s Encrypt 등으로 HTTPS 인증서를 발급받으면, 그 인증서에 들어간 도메인 이름이 공개 CT 로그에 기록됨
  • crt.sh 같은 사이트에서 누구나 조회 가능. 내가 어디에도 적지 않았어도 인증서 발급 기록에는 남아 있음 – 가능성에 대해서는 Claude 가 알려줌.
  • 스캐너 봇들은 CT 로그를 실시간으로 구독하다가 새 호스트가 생기면 수 분~수 시간 안에 찾아옴
  • DNS 자체는 “등록된 레코드 목록 보기” 기능이 없음. 이름을 알아야 조회되는 구조
  • 다만 우회 경로는 여럿: 공개 리졸버를 거친 조회를 모아 둔 passive DNS 데이터베이스, 흔한 이름을 대입해 보는 서브도메인 브루트포스, 설정이 잘못된 존 전송(AXFR), DNSSEC NSEC 레코드를 따라가는 존 워킹

즉 “주소를 숨겼다”는 생각 자체가 틀렸던 셈이다. 개별 인증서를 발급받는 순간, 그 서브도메인은 사실상 공개 목록에 올라간다. 숨기고 싶다면 와일드카드 인증서(*.example.com)를 쓰는 편이 낫지만, 그것도 보안이 아니라 약간의 은닉일 뿐이다.

왜 들어와서 이런 걸 남겼을까

아마 “왜”라고 물을 상대가 없을 가능성이 높다. 사람이 학원비 페이지를 노리고 들어왔다기보다는, 자동화된 스캐너가 지나가다 건드린 쪽에 가깝다고 본다.

  • 입력 폼 발견 → 아무 값이나 넣고 제출 → 그 값이 저장되어 다시 화면에 나오는지 확인. 저장형 XSS·인젝션 취약점을 찾는 전형적인 절차
  • 인증 없이 쓰기가 되는 페이지는 스캐너 입장에서 “쓸 만한 곳” 목록에 올라감. 이후 스팸 링크 게시나 다른 공격의 발판 후보
  • 데이터가 통째로 사라진 것도 같은 맥락으로 추정. 화면의 삭제가 단순 링크(GET 요청)였다면, 페이지의 링크를 전부 따라가는 크롤러가 지나가기만 해도 행이 하나씩 지워짐. 악의라기보다 설계 실수가 만든 결과일 가능성

마지막 항목이 개인적으로 제일 뜨끔했다. 삭제나 수정 같은 상태 변경을 GET 링크로 만들면, 사람이 아니라 봇이 누르게 된다.

정리

  • “어디에도 안 적었다”는 착각. 인증서 발급과 동시에 공개 기록에 남음
  • 인터넷에 열린 페이지는 개인용이든 아니든 봇이 찾아옴. 시간 문제일 뿐
  • 최소한의 인증은 필수. 이번에 로그인 추가
  • 삭제·수정은 GET 링크가 아니라 POST로
  • 백업 덕분에 피해는 0. 백업 습관 하나는 칭찬

작은 사이트라서 괜찮겠지, 라는 생각이 제일 위험했다.

WebSocket으로 가상 센서 구현하기

1. 들어가며

redroid로 안드로이드를 컨테이너에 띄워 쓰다 보면 금방 벽에 부딪힌다. 화면과 입력은 원격으로 해결되지만, 카메라와 GPS, 그리고 BLE는 없다. 서버에는 그런 하드웨어가 처음부터 없기 때문이다. 카메라로 코드를 비추고, 그 결과로 근처 BLE 장치의 잠금을 여는 식의 앱이라면 첫 단계부터 막힌다. 읽을 카메라도, 말을 걸 BLE 라디오도 없기 때문이다.

그래서 방향을 바꿨다. 하드웨어는 이미 손에 있는 폰에 다 있다. 폰이 센서와 무선 신호를 WebSocket으로 보내고, 서버가 이를 가상 카메라, 가상 BLE 라디오, 가상 GNSS로 재현하면 된다. 컨테이너 안의 앱은 자기가 실제 폰에서 돌고 있다고 믿게 된다. 폰 앱 대신 PC의 웹브라우저로도 연결할 수 있어서, PC의 웹캠을 카메라로 쓸 수도 있다.

이 글은 그 구조를 정리하고, 특히 BLE를 붙이면서 겪은 “광고 패킷 홍수” 문제를 어떻게 4단계로 풀었는지 다룬다.

2. 전체 아키텍처

flowchart TB
  subgraph C["📱 클라이언트 · RemoteController 앱"]
    direction TB
    CAM[Camera<br/>ForwardService]
    GPS[Gps<br/>ForwardService]
    BLE[ScoutService<br/>BLE]
  end
  WS(("WebSocket<br/>LAN"))
  R["🖥️ RemoteRelay :8080<br/>(redroid 컨테이너)"]
  C <-->|"카메라 · GPS 업로드 / BLE 양방향"| WS
  WS <--> R

그림 1-1. 클라이언트: 폰의 서비스들이 WebSocket 하나로 서버의 RemoteRelay에 연결

flowchart TB
  R["RemoteRelay :8080"]
  R --> CF[CameraUploadForwarder]
  R --> GF[GpsForwarder]
  R <--> BB["BleBridgeClient<br/>(Bluetooth 모듈)"]
  CF --> RH["remotecamera_helper<br/>:28554"] --> VC[가상 카메라]
  GF --> FG["fakegps_helper<br/>:2947"] --> GH[GNSS HAL]
  BB <--> GS[GattService]
  VC --> APP[["컨테이너 안 앱"]]
  GH --> APP
  GS <--> APP

그림 1-2. 서버: RemoteRelay가 기능별 helper와 가상 센서를 거쳐 컨테이너 앱으로 전달

구성 요소는 크게 둘이다.

  • 클라이언트(RemoteController 앱): CameraForwardService, GpsForwardService, ScoutService(BLE)
  • 서버(redroid 컨테이너): RemoteRelay가 WebSocket(:8080)으로 모든 스트림을 받아 중계

서버 쪽 경로는 기능별로 나뉜다.

  • 카메라: CameraUploadForwarder → remotecamera_helper(:28554) → 가상 카메라
  • GPS: GpsForwarder → fakegps_helper(:2947) → GNSS HAL
  • BLE: RemoteRelay → Bluetooth 모듈의 BleBridgeClient → GattService

여기에 두 가지 운영 규칙을 더했다.

  • 기능별 소유권: camera, gps, ble는 한 번에 한 컨트롤러만 사용. claim으로 가져오고 기존 소유자는 revoked 수신
  • 수요 기반 전송: 컨테이너 앱이 카메라를 실제로 열 때만 서버가 demand 메시지 송신, 그때부터 클라이언트 전송 시작 (DemandMonitor가 감지)
stateDiagram-v2
  [*] --> 비어있음
  비어있음 --> A소유: 컨트롤러 A claim
  A소유 --> B소유: 컨트롤러 B claim / A에 revoked
  B소유 --> A소유: 컨트롤러 A claim / B에 revoked
  A소유 --> 비어있음: A 연결 끊김
  B소유 --> 비어있음: B 연결 끊김
  note right of A소유
    camera, gps, ble
    각 기능마다 독립적으로 적용
  end note

그림 6. claim → 사용 중 → revoked 기능 소유권 상태도

수요 기반 전송은 생각보다 효과가 컸다. 아무도 카메라를 쓰지 않는데 JPEG를 계속 밀어 넣던 낭비가 사라졌다.

3. 기능별 구현

카메라와 GPS: 내 폰의 센서를 원격 폰에 빌려주기

카메라부터 보자. 한 문장으로 줄이면 “내 폰의 카메라를 원격 폰에 빌려주는 기능”이다. 원격 폰(redroid 컨테이너)에는 카메라가 없다. 그래서 내 폰이 찍은 영상을 네트워크로 보내고, 원격 폰은 그 영상을 자기 카메라에서 나온 것처럼 앱에 넘겨준다.

원격 폰의 앱은 영상이 어디서 왔는지 모른다. 평소처럼 카메라를 열었을 뿐인데, 그 카메라에 내 폰이 보고 있는 장면이 들어 있는 것이다. 앱 입장에서는 그냥 “내 카메라에 무언가가 비쳤다”일 뿐이다.

아래 스크린샷이 그 모습이다. 화면이 두 겹이라 처음에는 헷갈린다. 바깥쪽 파란 테두리는 지금 손에 들고 있는 내 폰의 RemoteController 앱이다. 안쪽 주황 테두리는 그 앱이 띄워 주는 원격 폰의 화면이다. 원격 폰에서 QR 스캔 앱을 실행해 두고, 그 화면을 내 폰으로 다시 보고 있는 상태다. 그림 속 번호를 따라가면 영상이 한 바퀴 도는 길이 보인다.

  • ① 내 폰 카메라 촬영: 원격 폰의 앱이 카메라를 여는 순간 서버가 demand 메시지 송신, 내 폰의 CameraForwardService가 실제 카메라로 촬영 시작. 앱 상단 CAM 스위치가 이 기능의 켜짐 상태
  • ② 원격 폰으로 전송: 찍은 프레임을 JPEG로 압축해 WebSocket으로 송신 → 원격 폰의 RemoteRelay → CameraUploadForwarder → remotecamera_helper(:28554) → 가상 카메라. QR 스캔 테두리 안에 보이는 QR 코드가 바로 내 폰 카메라가 보고 있는 장면
  • ③ 원격 폰 앱의 인식: 원격 폰의 QR 스캔 앱이 안드로이드 표준 카메라 API로 가상 카메라를 열고, 들어오는 프레임을 자기 카메라 입력으로 처리. 앱 코드 수정 없음
  • ④ 다시 내 폰으로: 원격 폰의 화면 전체를 다시 JPEG로 내 폰에 송신, RemoteController의 SCREEN 탭에 표시. 터치와 BACK·HOME 버튼은 반대 방향으로 원격 폰에 주입
내 폰(파란 테두리)의 RemoteController 앱 안에 원격 폰(주황 테두리)의 QR 스캔 화면이 보이고, 카메라 영상이 도는 순서가 1~4 번호로 표시된 스크린샷
① 내 폰 카메라 촬영 → ② 원격 폰의 가상 카메라로 전송 → ③ 원격 폰 앱이 자기 카메라로 인식 → ④ 원격 폰 화면이 다시 내 폰에 표시. 파란 테두리는 내 폰, 주황 테두리는 원격 폰 화면

정리하면 내 폰의 카메라 영상은 내 폰 → 원격 폰 → 다시 내 폰으로 한 바퀴를 돈다. 스크린샷 속 QR 코드는 내 폰 카메라가 찍은 장면이 원격 폰의 앱을 거쳐 다시 내 폰 화면에 나타난 것이다. 같은 흐름을 구성 요소 단위로 그리면 아래와 같다.

flowchart TB
  subgraph MY["📱 내 폰 · RemoteController 앱"]
    CAMR["실제 카메라<br/>CameraForwardService"]
    VIEW["SCREEN 탭<br/>원격 폰 화면 표시"]
  end
  subgraph RM["🖥️ 원격 폰 · redroid 컨테이너"]
    RELAY["RemoteRelay :8080"]
    HELP["remotecamera_helper :28554"]
    VCAM["가상 카메라"]
    APP["QR 스캔 앱<br/>(코드 수정 없음)"]
  end
  APP -. "⓪ 카메라 열림 → demand" .-> CAMR
  CAMR -->|"① 촬영 · ② JPEG 전송"| RELAY
  RELAY --> HELP --> VCAM
  VCAM -->|"③ 일반 카메라 API"| APP
  APP -->|"④ 화면을 JPEG로 되돌림"| VIEW
  style CAMR fill:#dbe7ff,stroke:#2563eb
  style VIEW fill:#dbe7ff,stroke:#2563eb
  style VCAM fill:#ffe6cc,stroke:#f57c00
  style APP fill:#ffe6cc,stroke:#f57c00

그림 1-3. 카메라 영상이 한 바퀴 도는 경로 (번호는 위 스크린샷과 같음)

이 경로에는 실제로 쓰면서 넣은 세부 동작이 몇 가지 있다.

  • 수요 기반 시작: 원격 폰의 앱이 카메라를 쓰지 않을 때는 촬영도 전송도 없음. 앱이 카메라를 닫으면 약 3초 뒤 전송 중단
  • 끊김 처리: 전송이 잠깐 끊겨도 가상 카메라가 마지막 프레임을 유지해 앱 화면이 깨지지 않음. 부팅 후 영상이 한 번도 없었을 때만 대체 그림 표시
  • 최신 프레임 우선: 원격 폰 화면을 내 폰으로 보낼 때는 최신 JPEG 한 장만 유지하고 밀린 프레임은 버림

GPS는 같은 방식이지만 더 단순하다. 내 폰의 위치를 fakegps_helper(:2947)로 보내면, 원격 폰의 GNSS HAL이 이를 실제 위성 신호처럼 앱에 전달한다. 위치는 한쪽으로 흘려보내기만 하면 되니 되돌아오는 경로가 없다.

BLE

BLE는 양방향이라 이야기가 다르다.

  • 스캔: 클라이언트가 실제 스캔 수행 → ble_scan_result(address, rssi, advData) 송신 → 서버 앱의 스캔 콜백으로 전달
  • GATT: 서버 앱의 connect/read/write/notify 요청 → 클라이언트로 전달 → 실제 장치와 통신 → ble_gatt_event로 결과 회신
  • MTU 247 요청: 한 번에 실어 보내는 양을 늘려 LAN 왕복 횟수 감소

핵심은 서버의 Bluetooth 모듈 안에 넣은 BleBridgeClient다. GattService 입장에서는 이것이 로컬 BLE 라디오처럼 보인다. 그래서 앱 코드는 한 줄도 바꾸지 않아도 된다.

sequenceDiagram
  participant A as 컨테이너 앱
  participant G as GattService
  participant B as BleBridgeClient
  participant R as RemoteRelay
  participant S as ScoutService (폰)
  participant D as 실제 BLE 장치
  A->>G: connectGatt / readCharacteristic
  G->>B: GATT 요청
  B->>R: GATT 명령 (ble_connect 등)
  R->>S: WebSocket (text 레인, 우선)
  S->>D: 실제 GATT 연결 · 읽기
  D-->>S: 응답
  S-->>R: ble_gatt_event
  R-->>B: ble_gatt_event
  B-->>G: 즉시 처리 (큐 우회)
  G-->>A: onCharacteristicRead

그림 2. 앱 connect/read → GattService → BleBridgeClient → relay → ScoutService → 실제 장치 → ble_gatt_event 회신 시퀀스

4. 트러블슈팅: BLE가 매우 느려지다

증상

주변에 BLE 기기가 많은 곳에서 BLE가 눈에 띄게 느려졌다. 스캔 목록이 버벅이고, characteristic read 하나에 한참이 걸렸다. 따라가 보니 read 응답이 앞서 쌓인 광고 수백 개 뒤에서 차례를 기다리고 있었다.

나중에 집에서 다시 재 보니, 60초 동안 보이는 주소가 36~43개(랜덤 주소 포함)인 평범한 환경에서도 폰이 받는 광고만 초당 약 40건이었다. 필터가 없으면 이 40건이 하나하나 JSON 메시지가 되어 LAN을 건넌다.

원인

원인은 세 가지가 겹친 것이었다.

  • LOW_LATENCY 스캔은 기기당 초당 10~100개의 광고 보고
  • 클라이언트가 이를 전부 JSON으로 변환해 그대로 송신
  • 스캔 결과와 GATT 이벤트가 같은 FIFO 사용, 서버는 소켓 읽기 스레드에서 무거운 스캔 콜백을 직접 처리

즉 꼭 도착해야 하는 GATT 응답이, 몇 초 뒤면 의미가 없어지는 광고 데이터에 막혀 있었다.

flowchart TB
  subgraph BEFORE["개선 전 · 단일 FIFO"]
    direction LR
    b1[광고] --> b2[광고] --> b3[광고] --> b4["… 수백 개 …"] --> b5[광고] --> b6["⏳ GATT read 응답"]
  end
  subgraph AFTER["개선 후 · 우선순위 분리"]
    direction LR
    a1["⚡ GATT read 응답"] --> a2["광고 (주소별 최신 1개)"] --> a3["광고 (주소별 최신 1개)"]
  end
  BEFORE ~~~ AFTER
  style b6 fill:#fde2e2,stroke:#d33
  style a1 fill:#dff5e3,stroke:#2a7

그림 3. 개선 전(광고 뒤에 GATT read가 대기) vs 개선 후(GATT가 앞질러 감) 타임라인 비교

개선 1: 클라이언트 ScanThrottle

보내기 전에 거르는 것이 가장 싸다. 판정 규칙은 이렇다.

  • 첫 발견은 항상 전송
  • 같은 기기는 300ms 이내 재전송 억제
  • 광고 데이터가 바뀌었거나 RSSI가 6dB 이상 변했을 때만 전송
  • 변화가 없어도 2초마다 keep-alive (기기가 사라진 것처럼 보이지 않도록)
// ScanThrottle.shouldSend 핵심 부분 (발췌)
State s = mStates.get(address);
if (s == null) { s = new State(); mStates.put(address, s); }   // 첫 발견: 전송
else {
    long since = nowMs - s.lastSentMs;
    if (since < mMinIntervalMs) return false;                    // 300ms 억제
    boolean changed = hash != s.advHash || len != s.advLen
            || Math.abs(rssi - s.lastRssi) >= mRssiDelta;        // 광고 변경, RSSI 6dB
    if (!changed && since < mRefreshMs) return false;            // 2초 keep-alive
}

광고 데이터는 바이트 배열을 통째로 비교하지 않고 해시와 길이만 기억해 둔다. 걸러진 광고는 JSON을 만드는 단계까지 가지도 않는다.

flowchart TD
  IN([스캔 결과 수신]) --> Q1{처음 본 기기?}
  Q1 -- 예 --> SEND([전송])
  Q1 -- 아니오 --> Q2{"마지막 전송 후<br/>300ms 이내?"}
  Q2 -- 예 --> DROP([버림])
  Q2 -- 아니오 --> Q3{광고 데이터 변경?}
  Q3 -- 예 --> SEND
  Q3 -- 아니오 --> Q4{"RSSI 변화<br/>6dB 이상?"}
  Q4 -- 예 --> SEND
  Q4 -- 아니오 --> Q5{"마지막 전송 후<br/>2초 경과?"}
  Q5 -- "예 (keep-alive)" --> SEND
  Q5 -- 아니오 --> DROP
  style SEND fill:#dff5e3,stroke:#2a7
  style DROP fill:#eee,stroke:#999

그림 4. 첫 발견 → 300ms → 데이터 변화/RSSI 6dB/2초 판정 흐름도

개선 2: 클라이언트 송신 큐

거르고 남은 것도 순서를 바꿨다.

  • GATT 이벤트: 순서 유지, 항상 스캔보다 먼저 송신
  • 스캔: 주소를 키로 최신 값만 유지, 이전 값은 덮어쓰기

개선 3: 서버 ClientOutbox 레인 분리

서버에서 클라이언트로 내려가는 방향도 같은 문제가 있었다. 큐를 데이터 성격별 레인으로 나눴다.

  • text: 제어 메시지와 GATT 명령, 최우선
  • scans: 주소별 최신 값
  • frame: 최신 JPEG 한 장

text를 먼저 비우고, scans와 frame은 번갈아 보내서 한쪽이 다른 쪽을 굶기지 않게 했다.

flowchart LR
  T["text 레인<br/>제어 · GATT<br/>순서 보장"]
  SC["scans 레인<br/>주소별 최신 값"]
  FR["frame 레인<br/>최신 JPEG 1장"]
  P{{"1순위: text 먼저 비움"}}
  RR{{"2순위: 라운드로빈"}}
  SND[["sender → WebSocket"]]
  T --> P --> SND
  SC --> RR
  FR --> RR
  RR --> SND
  style T fill:#dff5e3,stroke:#2a7

그림 5. text 우선, scans와 frame은 번갈아 sender로 가는 레인 구조

개선 4: 서버 수신 측 ScanCoalescer

마지막은 서버의 받는 쪽이다. 소켓을 읽는 스레드가 onScanResult를 직접 부르던 구조를 끊었다.

  • 소켓 읽기 스레드: 스캔 결과를 큐에 넣기만 함
  • 별도 처리 스레드: 무거운 onScanResult 호출
  • GATT 이벤트: 큐를 거치지 않고 즉시 처리

공통 원칙

네 단계를 정리하고 보니 결국 원칙은 하나였다. 데이터를 두 종류로 나누는 것이다.

종류예처리 방식
흘러야 하는 데이터영상, 스캔최신 값만 유지, 밀리면 버림
반드시 도달해야 하는 데이터GATT, 제어순서 보장, 항상 우선

결과

항목A: 필터 끔B: ScanThrottle
서버가 받은 ble_scan_result (초당 평균 / 최대)39.9 / 5111.9 / 17
페이로드 바이트 (초당 평균 / 최대)6,599 / 8,4361,970 / 2,818
characteristic read 지연 (중앙값 / p95, 30회)744 / 805 ms741 / 799 ms

ScanThrottle만 끄고 켜서 60초씩 비교했다. 서버 쪽 레인 분리와 ScanCoalescer, 폰의 GATT 우선 큐는 두 경우 모두 그대로다. 폰은 두 경우 모두 초당 약 40건의 광고를 받았고, 필터를 켜면 그중 30% 정도만 보냈다. 메시지 수와 바이트가 약 3.3분의 1로 줄었다.

%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#3b6fd4"}}}}%%
xychart-beta
  title "서버가 받은 ble_scan_result (초당, 60초 평균)"
  x-axis ["A: 필터 끔", "B: ScanThrottle"]
  y-axis "메시지/초" 0 --> 45
  bar [39.9, 11.9]

그림 7. 필터를 켜면 같은 광고 흐름에서 전달하는 메시지가 약 3.3분의 1로 줄어듦

반면 read 지연은 거의 같았다. 이 환경에서는 필터를 꺼도 폰과 서버의 대기열이 최대 1건 정도라 줄 자체가 생기지 않았다. 처음 문제가 됐던 붐비는 환경이 집에서는 재현되지 않은 것이다. 그래서 이번 측정으로 확인한 것은 트래픽 감소까지다. 지연 개선은 예전 단일 FIFO 구조와 붐비는 곳에서 직접 비교해야 드러날 것 같다.

측정 조건: Galaxy S9+(LineageOS), 5GHz Wi-Fi, x86_64 redroid 컨테이너, 시나리오별 1회 측정.

5. 검증

  • 회귀 시험: 자동 시험 66 통과 / 1 실패. 실패한 1건은 앞선 시험이 열어 둔 앱 화면이 남아 있어서 생긴 시험 시작 상태 문제로, 이번 BLE 변경과는 관련 없음 (시작 전에 정리하도록 고쳤고 전체 재실행은 아직)
  • 호스트 단위 테스트: ScanCoalescerTest로 주소별 최신 값 유지와 대기열 상한 동작 검증

6. 한계와 다음 단계

네트워크 상태가 나쁘면 GATT 지연이 그대로 앱에 드러난다. 기능 소유권도 한 번에 한 컨트롤러만 허용하므로, 여러 폰의 센서를 섞어 쓰는 것은 아직 안 된다. 측정에서 본 read 한 번에 740ms 안팎이 걸리는 것도 네트워크 왕복만으로는 설명이 안 되는 숫자라 따로 들여다볼 생각이다.

다음으로는 붐비는 환경에서 예전 단일 FIFO 구조와 지금 구조를 직접 비교해 지연 차이를 확인하고, 남은 회귀 시험 실패 1건을 정리할 계획이다. 웹 뷰어(클라이언트 목록, 강제 끊기, 회전)와 GApps/ARM 번역 환경 이야기는 따로 한 편으로 다루겠다.

하드웨어가 없는 컨테이너에 하드웨어를 “빌려주는” 일은 결국 네트워크 위에 장치를 흉내 내는 일이었다. 그리고 그 흉내가 자연스러우려면, 무엇을 버려도 되고 무엇은 절대 버리면 안 되는지를 먼저 정해야 했다.

덧붙이면, 이름은 “over LAN”이지만 같은 LAN 안에서만 되는 것은 아니다. 폰을 Wi-Fi 대신 5G 모바일 데이터로 붙여서도 써 봤는데, 실제로 쓰는 데는 문제가 없었다. LTE나 5G 정도의 연결이면 실사용에는 충분하다.

LineageOS(Custom/Rooted Android)에서 Kbank 사용하기

나의 갤럭시 S9+를 재활용하기 위해 LineageOS를 깔았다. 덕분에 Android 10에서 13으로 업그레이드됐다. 문제는 은행앱이었다. LineageOS는 Custom OS라서, 은행앱의 루팅·변조 탐지에 그대로 걸린다. 아무 조치도 안 하면 이렇게 나온다. 아무 조치 없이 실행한 케이뱅크 사용자단에서 이것저것 막는 게 능사는 아니다. 서버에서 검증하면 될 일을, 클라이언트에 루팅·상태 검증 로직을 잔뜩 심어놓고 사용자를 잠재적 공격자로 취급하는 셈이다. 테스트…

Read more

테슬라 모델Y vs 팰리세이드 — 차가 운전자에게 말을 거는 방식 (앰비언트 라이트, HUD)

팰리세이드에서 모델Y로 바꾸고 가장 크게 달라진 건 성능이 아니라 알림이었다. 사각지대를 앰비언트 라이트로 알려주는 방식은 왜 그렇게 좋았고, HUD 없는 테슬라는 왜 답답한가. Euro NCAP 점수까지 찾아봤다.

Read more

Setting Up xrdp with Korean Font Support on Ubuntu 24.04

If you've just spun up a fresh Ubuntu server and want a full desktop you can reach over RDP — with proper Korean (Hangul) rendering and Chrome ready to go — here's the complete, copy-paste-friendly walkthrough. Every command below uses environment variables for the server IP, the new user's name, and password, so you can…

Read more

차량 판매 경험(헤이딜러) : 팰리세이드와 미니 이야기

팰리세이드와 미니를 정리하면서 현대 인증중고차, 엔카, 헤이딜러를 모두 거쳤습니다. 3년째 미뤄지는 안전벨트 리콜, 일반 경매와 역경매를 쓰는 순서, 감가율 비교, 그리고 가장 귀찮았던 명의이전과 단기보험 이야기.

Read more

정지선에서도 신호등이 보이게: trafficlight 앱을 만든 이유

📱 Google Play에서 trafficlight 앱 다운로드 (프로젝트명 LightSignal · Android 전용) ⚠️ 운전 보조 도구입니다. 신호 확인과 운전 판단의 최종 책임은 운전자에게 있습니다. 0. 서론 . 정지선에 맞춰 서면 오히려 신호등이 시야 밖으로 사라지는 구조 . 신호등이 차량 바로 위쪽 각도에 위치, 앞유리 상단 프레임에 가려짐 . 고개를 숙이거나 앞으로 밀고 나가야만 신호 확인이 가능한…

Read more

게임 머니를 파는 아이 전용 카드, ‘퍼핀’에 묻습니다

우리집은 '돈이 사라지는 감각'부터 가르쳤습니다 저는 아이에게 돈을 가르칠 때 나름의 원칙이 있었습니다. 남는 자산과 사라지는 소비를 구분하는 것입니다. 게임 아이템, 콘텐츠 결제, 오락실, 인형 뽑기처럼 손에 남는 게 없는 '소모성·무형 지출'은 되도록 현금으로만 쓰게 했습니다. 지갑에서 지폐가 줄어드는 걸 눈으로 보게 해서, "이건 한 번 쓰면 돌아오지 않는 돈"이라는 감각을 몸에 익히게 하려는 거였죠.…

Read more

Virtual Print – 설치 개선 & 활용

안녕하세요. Virtual Print 설치 과정을 단순화 했습니다.다만, 다운로드후 아래에서 More-Info 후에 강제 설치를 해야 합니다. 혹 과정이 부담스럽다면, 이전 방법도 가능합니다. 이전 방법은 아래를 참고해주세요. https://flywithu.com/archives/7941 아래 파일을 다운 및 설치 합니다. - Win64Bit 전용 입니다. SHA256: f9a09ae10258400ab47ea48dc289a561429bbbc33cfac91b8a8e9767eec947c4 검사: https://www.virustotal.com/gui/file/f9a09ae10258400ab47ea48dc289a561429bbbc33cfac91b8a8e9767eec947c4?nocache=1 NetPrinterDriverSetupDownload 2. 설치 화면 3. 프린터 드라이벙- Samsung CLX-6200 Series PS-VP VP 붙은것이 설치 된…

Read more