앱을 배포하는 팀이 REA 실측기를 먼저 읽어야 하는 이유
![]()
여섯 개 중 다섯 개를 맞혔습니다. 앱을 뜯어 분석해 주는 오픈소스 도구 REA에게 한 터미널 앱의 동작 여섯 가지를 설명하게 하고 공개 소스와 대조해 채점한 결과입니다. "이제 아무 앱이나 뜯어서 똑같이 만들 수 있다"는 말과 함께 퍼지는 도구치고는 성적이 좋아 보이죠. 그런데 이 실험에서 제가 가장 중요하게 읽은 정보는 맞힌 다섯이 아니라 채점표에 오르지도 못한 것들이었고, 그 정보는 앱을 복제하려는 사람보다 앱을 사용자에게 내려보내는 팀에게 더 급한 소식이라고 봅니다.
실측기의 결론은 "복제가 쉬워졌다"가 아니라 "읽는 비용이 싸졌을 뿐"이었습니다. 저는 이 결론에 동의하면서도, 이 말이 위안으로 읽히는 건 경계하고 싶습니다. 읽는 비용이 싸졌다는 건 클라이언트에 넣어 둔 모든 것이 이제 싸게 읽힌다는 뜻이니까요.
결과보다 먼저 볼 만한 실험 설계
이 글에서 제가 가장 높이 사는 건 숫자가 아니라 대상을 고른 방식입니다. 실험 대상은 Hyper라는 터미널 앱의 3.4.1 버전이었습니다. MIT 라이선스라 소스가 전부 공개돼 있어서, 도구가 내놓은 설명을 정답지와 맞춰 채점할 수 있습니다. "AI가 앱을 복제했다"는 시연 대부분이 건너뛰는 단계가 바로 이 채점입니다. 공식 배포본에서 Electron 앱이 자바스크립트 코드를 하나로 포장해 담아 두는 asar 파일만 꺼냈고, 앱은 실행하지 않은 채 코드만 읽는 정적 분석으로, 전역 설정을 건드리지 않는 격리 환경에서 돌렸습니다. 유료 호출이 없어 비용은 0원이었고요.
도구 쪽 사실도 짚어 두면, REA는 네이티브 프로그램과 자바스크립트 앱, 안드로이드 앱, 웹사이트를 분석해 "이 앱이 대략 이렇게 생겼고 이런 걸 주고받는다"를 증거와 함께 돌려주는 MIT 라이선스 도구이고, 분석은 내 컴퓨터 안에서만 돕니다. 깃허브 별은 28만 개를 넘겼습니다. 공식 문서는 자동 복제도, 원본 소스 복원도 하지 않으며 서버에서만 도는 로직과 암호화된 부분은 관찰할 수 없다고 적어 두었습니다. 사용 범위 고지문은 복제 방지 장치 우회나 남의 독점 소프트웨어를 베끼는 용도를 지원하지 않는다고 못 박고요. 복제기라는 소문과 달리 도구 스스로는 분석기라고 선을 긋고 있는 셈입니다.
첫 실행에서 벌어진 일도 인상적입니다. 도구는 앱 안 일부 파일의 선언된 값과 실제 내용이 어긋난다며, 패키징에서 흔히 생기는 일이니 의도된 것이면 이렇게 다시 돌리라는 조치 방법까지 적어 실행을 한 번 거부했습니다. 어긋난 부분을 '믿을 수 없는 값'으로 따로 표시하고 계속하라는 설정을 준 뒤에야 분석이 끝났고요. 저는 분석 도구의 신뢰도를 가늠할 때 이런 거부를 결과보다 먼저 봅니다. 모르는 걸 모른다고 말할 줄 아는 도구여야 그다음에 내놓는 '안다'를 믿을 수 있으니까요.
'6개 중 5개'는 정밀도일 뿐이다
채점 결과는 여섯 가지 동작 설명 중 다섯이 맞고 하나가 틀렸습니다. 숫자만 보면 80%를 넘는 정답률이지만, 저는 이 숫자를 정밀도로만 읽어야 한다고 봅니다. 채점 대상이 된 여섯 가지가 애초에 REA 출력만 보고 뽑은 목록이기 때문입니다. 도구가 말한 것 중 몇 개가 맞았는지는 알려 주지만, 도구가 말하지 않은 것 중 중요한 게 몇 개였는지는 알려 주지 않습니다. 재현율은 이 실험에서 측정되지 않았습니다.
그 측정되지 않은 쪽에 진짜 문제가 있었습니다. 이 앱의 화면 쪽과 시스템 쪽이 메시지를 주고받는 핵심 통로를 처음 여는 핸드셰이크, 즉 화면이 켜질 때 시스템 쪽이 고유 번호를 건네고 화면 쪽이 그걸 받아 기억해 두는 절차를 도구는 통째로 놓쳤습니다. 메시지 포장 형식이 양쪽에서 서로 다르다는 점도 그래프에 잡히지 않았습니다.
맞은 다섯의 안쪽도 들여다볼 필요가 있습니다. 그중 둘은 Hyper가 직접 짠 코드가 아니라 널리 쓰이는 공용 라이브러리를 가져다 쓴 부분이었습니다. 통로가 있다는 것까지는 정확히 읽었지만, 그게 이 앱만의 기능인지 남이 만든 부품인지 가르는 일은 사람 몫이었죠. 틀린 하나는 설정값을 읽는 동작을 외부 네트워크 접속으로 분류한 오탐이었는데, 도구가 이를 단정하지 않고 '후보'로 표시해 둔 점은 공정하게 적어 둘 만합니다.
성적표를 세 줄로 줄이면 이렇습니다. 말한 것의 정밀도는 꽤 높다. 남이 만든 부품과 고유 기능을 구분하지 못한다. 실행 중에만 드러나는 흐름은 목록에서 빠진다. 저는 이 세 줄이 REA만이 아니라 정적 분석 도구 일반의 성적표로도 맞다고 봅니다.
복제를 막은 건 문서화되지 않은 약속이었다
실측은 한 걸음 더 나갑니다. REA가 읽어 준 내용만으로, 앱 내부의 두 부분이 메시지를 주고받는 IPC 계층을 재구현해 실제 앱과 연결해 봤습니다. 소스를 보고 만든 쪽은 양방향 통신이 통과했고, REA 출력만 보고 만든 쪽은 실제 화면 쪽 코드와 연결되지 않았습니다. 막힌 이유는 네 가지였습니다. 핸드셰이크를 몰랐고, 통로를 여는 고유 번호가 어디서 어떻게 만들어지는지 몰랐고, 메시지 포장 형식이 양쪽에서 다르다는 걸 몰랐고, 화면을 새로 고칠 때 번호를 기억해 두는 장치를 몰랐습니다. 재구현 시험에 걸린 시간은 약 15분, 분석까지 합친 전체 작업이 20분 남짓이었습니다.
저는 이 네 가지가 모두 같은 종류라는 데 주목합니다. 넷 다 코드의 '모양'이 아니라, 두 프로그램이 실행 중에 지키는 '약속'입니다. 정적 분석은 함수와 통로가 어디 있는지는 보여 주지만, 그 통로를 언제, 어떤 순서로, 어떤 형식으로 쓰는지는 실행해 봐야 드러납니다. 이 앱의 복제를 막은 건 난독화도 보안 장치도 아니었고, 어디에도 문서로 적히지 않은 런타임 규약이었습니다.
그런데 여기서 안도하면 곤란합니다. 이번 실험은 앱을 실행하지 않는다는 조건을 스스로 걸었고, 그 빈칸은 소스나 실행 관찰을 더하면 메울 수 있다는 점도 실측기가 직접 적어 두었습니다. 핸드셰이크는 앱을 한 번 띄우고 통신을 지켜보면 보이는 종류의 정보입니다. 이번에 복제를 막은 벽은 방어하는 쪽이 세운 게 아니라 실험 조건이 세운 것이고, 앱을 베끼려는 사람은 그 조건을 지킬 이유가 없습니다.
대상의 성격도 감안해야 합니다. asar는 압축이라기보다 포장이라 자바스크립트 원문에 가깝게 읽히고, Electron 앱은 정적 분석에 꽤 친절한 대상입니다. 네이티브 바이너리였다면 결과가 더 나빴을 수도 있고 다른 곳에서 막혔을 수도 있는데, 그쪽은 이번에 재지 않았습니다. 실측기도 한 사례일 뿐 일반화할 표본은 아니라고 범위를 분명히 해 두었고요. 저는 이 정직함 덕분에 오히려 결론을 어디까지 가져가도 되는지가 선명해졌다고 봅니다.
배포하는 팀이 이 20분에서 가져갈 것
실측기가 제안한, '복제됐다'는 말을 접할 때 확인할 세 가지는 쓸모 있는 습관입니다. 무엇을 대상으로 했는가, 정답지와 대조했는가, 못 한 목록을 같이 공개했는가. 저는 셋째가 가장 좋다고 봅니다. 성공 사례만 보여 주는 시연은 오탐이 몇 개인지 아무도 모르게 만드니까요. 다만 매일 코드를 배포하는 팀에게는 이 세 가지보다 한계를 적은 단락의 한 문장이 더 급합니다. 남에게 나눠 준 앱은 읽히기 쉬워졌으니 그걸 전제로 만들어야 한다는 문장입니다.
구체적으로 저라면 이렇게 하겠습니다. 데스크톱 앱과 모바일 앱, 웹 번들에 들어 있는 API 키와 엔드포인트, 가격·할인·권한 판단 같은 비즈니스 규칙을 목록으로 적고, 하나씩 '읽혀도 되는가'를 묻습니다. 읽혀도 되는 건 그대로 두고, 읽히면 곤란한 건 서버로 옮깁니다. 난독화나 Electron 패키징이 그 사이를 지켜 줄 거라는 기대는 버리는 게 맞습니다. 20분짜리 실험이 보여 준 건 읽기의 비용이 이미 그 정도라는 사실이니까요.
"구독형 소프트웨어 장사가 끝난다"는 말에 대해서는 실측기와 같은 쪽에 서겠습니다. 많은 서비스의 진짜 핵심은 내 컴퓨터로 내려온 앱이 아니라 서버 안에 있고, 데이터와 운영과 쌓인 신뢰가 함께 묶여 있습니다. 배포된 앱의 겉모습을 읽는 일과 그 서비스를 대신하는 일 사이의 간격은 이번에도 넓게 남았습니다. 하지만 그 간격이 우리 제품에서도 넓은지는 소문이 아니라 클라이언트에 무엇을 넣어 뒀는지가 정합니다.
복제가 아직 어렵다는 결론은 복제하려는 사람에게 하는 말이고, 배포하는 사람에게 남는 결론은 따로 있습니다. 내려보낸 코드에서 읽히는 것은 이미 다 읽혔다고 보고, 지켜야 할 것은 처음부터 내려보내지 않는 것. 저는 이 20분짜리 실험에서 가장 오래 쓸 결론이 그쪽이라고 생각합니다.