트렌딩 딥다이브 · 2026-07-22 · TrendShift Daily #6 · EYALM2000

eyalm2000/adns 딥다이브
— VPN 없이 안드로이드의 숨은 DNS 스위치를 한 탭으로 켜는 광고 차단기

ADNS는 이스라엘 개발자 Eyal Meirom이 만든 DNS 기반 광고 차단 안드로이드 앱이다. 흔한 광고 차단 앱과 결정적으로 다른 점 — VPN을 켜지 않는다. 대신 안드로이드 9(Pie)부터 OS에 내장돼 있지만 설정 깊숙이 숨어 있는 Private DNS(DNS-over-TLS) 스위치를, 앱이 시스템 설정값을 직접 써서 대신 눌러 준다. 광고·추적기 차단은 그 DNS 서버(NextDNS·AdGuard 등)가 서버 쪽에서 처리한다.

그래서 "백그라운드 서비스도, VPN 아이콘도, 배터리 소모도 없다"가 이 앱의 슬로건이다. 대신 시스템 설정을 쓰려면 WRITE_SECURE_SETTINGS라는 특권 권한이 필요한데, 이걸 Shizuku 또는 ADB로 한 번만 부여받는 방식으로 우회한다. 여기에 NextDNS 계정 전체(로그·통계·차단목록·재작성 규칙)를 앱 안에서 관리하는 미니 관리 콘솔까지 얹었다.

(저장소 eyalm2000/adns · 100% Kotlin + Jetpack Compose · Material 3 Expressive · minSdk 27(Android 8.1) / targetSdk 36 · 버전 2.0.0-beta3.1(versionCode 11) · 소스 173개 파일·약 24,357줄 · 테스트 55개 · 33개 언어 번역 · 라이선스 MIT · 배포 GitHub Releases + IzzyOnDroid · 클론 소스 직접 분석)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 주목받는가 — "VPN 없는 광고 차단"의 정체
  3. 기술 스택 전체 지도 — Kotlin·Compose·Shizuku·NextDNS
  4. 아키텍처 심화 — 설정값 하나로 광고를 막는 법
  5. 디렉토리 구조 해부
  6. 학습 포인트 — 여기서 훔쳐올 설계
  7. 시스템 / 실행 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

"광고 차단에 VPN이 왜 필요해?" — 그 질문에 OS 내장 기능으로 답한 앱
한 문장으로

ADNS = 안드로이드가 깊이 숨겨 둔 "Private DNS" 스위치를, 특권 권한을 빌려 한 번의 탭·퀵 설정 타일로 켜고 끄게 해 주는 얇은 앱 + NextDNS를 통째로 관리하는 콘솔

대부분의 안드로이드 광고 차단 앱(AdAway·Blokada·RethinkDNS 등)은 가짜 VPN을 만들어 모든 트래픽을 자기 앱으로 끌어와 DNS를 가로챈다. 늘 켜져 있는 VPN이라 배터리를 먹고, 진짜 VPN과 동시에 못 쓴다. ADNS는 정반대다 — 트래픽을 한 바이트도 건드리지 않는다.

대신 안드로이드 9부터 OS에 들어온 Private DNS(DoT) 기능을 대신 조작한다. 이건 원래 설정 → 네트워크 → 고급에 숨어 있어 켜기 번거로운데, ADNS가 그 시스템 설정값(private_dns_mode, private_dns_specifier)을 직접 써서 버튼 하나로 토글해 준다. 실제 광고 차단은 지정된 DNS 서버(NextDNS 등)가 서버에서 수행한다.

즉 ADNS의 정체는 두 겹이다. ① 얇은 스위치: OS의 숨은 DNS 설정을 대신 눌러 주는 아주 작은 코어(핵심 로직은 사실상 네 줄이다). ② 두꺼운 콘솔: NextDNS 웹 대시보드를 거의 통째로 앱 안에 재구현한 관리 화면(코드의 대부분이 여기다). 광고를 막는 기술 자체는 놀랄 만큼 단순하고, 앱의 부피는 "그 스위치를 예쁘고 편하게 감싸는" 데서 나온다.

용어
DNS · Private DNS(DoT)
DNS는 "도메인 이름(ads.example.com) → IP 주소" 변환을 해 주는 인터넷의 전화번호부다. 광고·추적기 도메인을 물어봤을 때 "그런 번호 없음"이라고 답하게 만들면 광고가 원천 차단된다. Private DNS는 안드로이드 9(Pie)부터 내장된 기능으로, 이 전화번호부 질의를 TLS로 암호화(DNS-over-TLS, DoT)해 지정한 서버로만 보낸다. ADNS는 이 "지정 서버"를 광고 차단 DNS로 바꾸는 것뿐이다.

저자 Eyal Meirom(GitHub eyalm2000)은 이 앱을 MIT 라이선스로 공개하고, F-Droid 계열 IzzyOnDroid와 GitHub Releases로 배포한다. 재현 가능한 빌드(reproducible build)를 지원해 "배포된 APK가 소스와 일치함"을 제3자가 검증할 수 있게 했고(README 상단의 RB 배지), 광고 차단 앱에 요구되는 투명성·신뢰를 정면으로 챙겼다. 2026년 현재 버전은 2.0.0-beta3.1로, 2.0에서 Wi-Fi 규칙·상태 알림·범용 DNS 서버·NextDNS 로그/설정 관리가 대거 추가됐다.

2왜 주목받는가 — "VPN 없는 광고 차단"의 정체

경쟁: AdAway · Blokada · RethinkDNS · Nebulo — 이들과 무엇이 다른가

안드로이드에서 루팅 없이 광고를 막는 길은 사실상 두 갈래뿐이었다. ADNS는 그중 잘 안 알려진 두 번째 길을 가장 매끄럽게 포장해 등장했다.

방식대표 앱원리대가
로컬 VPN 루프백AdAway, Blokada, RethinkDNS, Nebulo가짜 VPN을 만들어 전 트래픽을 앱으로 끌어와 DNS/패킷을 필터링상시 VPN → 배터리 소모, 진짜 VPN과 배타적, VPN 아이콘 상주
Private DNS 토글
(ADNS)
ADNSOS 내장 Private DNS 설정값을 대신 써서 DoT 서버 지정특권 권한 1회 부여 필요(Shizuku/ADB), Android 9+ 한정

차별점 ① — VPN을 쓰지 않는다는 근본적 이점

ADNS의 핵심 세일즈 포인트. VPN 루프백 방식은 안드로이드가 VPN을 한 번에 하나만 허용하기 때문에, 회사 VPN이나 프라이버시 VPN을 쓰면서 동시에 광고 차단을 할 수 없다. ADNS는 VPN 슬롯을 전혀 쓰지 않으므로 진짜 VPN과 광고 차단이 공존한다. 또 상시 실행되는 백그라운드 서비스가 없어 배터리·성능 부담이 사실상 0이다.

비유

모든 우편물을 집으로 가져와 검열하기 vs. 우체국에 "이 발신인 거부" 등록하기. VPN 방식은 집(기기)에 사설 우체국을 차려 놓고 들어오는 우편을 전부 뜯어 광고지를 골라낸다 — 일손(배터리)이 계속 든다. ADNS는 그냥 동네 우체국(OS의 DNS 설정)에 가서 "앞으로 우리 집 우편은 이 분류센터(NextDNS)를 거쳐 오게 해 주세요"라고 등록만 한다. 등록 후엔 우체국이 알아서 걸러 주니 우리 집엔 일손이 안 든다.

차별점 ② — "설정 깊이 숨은 기능"을 꺼내 준다

Private DNS는 이미 모든 안드로이드 9+ 기기에 있다. 하지만 설정 → 연결 → 기타 연결 설정 → 개인 DNS처럼 4~5단계 깊이에 묻혀 있고, 서버 호스트명을 손으로 타이핑해야 한다. ADNS는 이걸 첫 화면의 큰 토글 + 퀵 설정 타일(알림창 아이콘) + 홈 화면 바로가기로 끌어올린다. "기능을 새로 만든 게 아니라, 숨은 기능의 접근성을 극적으로 높인" 앱이다.

차별점 ③ — NextDNS 관리 콘솔 내장 + 재현 가능 빌드

단순 토글을 넘어, NextDNS 계정을 앱 안에서 통째로 관리한다: 실시간 로그(무엇이 차단됐나), 통계, 차단/허용 목록 편집, 도메인 재작성(rewrite), 자녀 보호, 계정 공유까지. NextDNS 웹사이트를 열 필요가 없다. 여기에 재현 가능한 빌드로 "이 APK는 이 소스에서 나왔다"를 증명하니, 내 DNS를 통째로 맡기는 광고 차단 앱에 필요한 신뢰가 확보된다.

그래서 트렌딩
"덜 침습적인데 더 강력하다"는 역설

트래픽을 건드리지 않는(=덜 침습적) 방식인데도, NextDNS 연동으로 기기 전체·앱별 필터링까지 가능해 오히려 더 강력하다. VPN 상시 실행의 피로감에 지친 사용자, 그리고 "루팅 없이 앱별이 아니라 기기 전역 광고 차단"을 원하는 사용자에게 정확히 꽂힌다. Material 3 Expressive의 세련된 UI와 33개 언어 지원도 확산에 한몫했다.

주의 · 오해 방지
"이 앱 자체가 광고를 막는" 게 아니다

ADNS는 필터 목록을 갖고 있지 않다. 실제 차단 판단은 당신이 지정한 DNS 서버(NextDNS·AdGuard DNS·Cloudflare 등)가 서버에서 한다. ADNS는 그 서버를 OS에 "지정해 주는 리모컨"일 뿐이다. 그래서 어떤 DNS 서버를 고르느냐가 차단 성능을 결정한다. 또한 DoT는 도메인 단위 차단이라, 같은 도메인에서 콘텐츠와 광고가 섞여 나오면(예: 유튜브 인앱 광고) 막지 못한다.

3기술 스택 전체 지도 — Kotlin·Compose·Shizuku·NextDNS

UI 층 · 특권 획득 층 · 네트워크(NextDNS) 층 · 보안 저장 층

ADNS는 웹 서버·백엔드가 없는 순수 안드로이드 클라이언트다. 그래서 스택을 "프론트/백엔드"가 아니라 이 앱이 실제로 하는 네 가지 일로 나눠 보는 게 이해가 빠르다: 화면 그리기(UI), 특권 권한 얻기(privilege), NextDNS와 통신(network), 비밀 안전 저장(crypto).

① UI 층 — 100% Kotlin + Jetpack Compose

요소버전역할
Kotlin2.2.10전 코드 언어(자바 0줄). 코루틴으로 비동기 처리
Jetpack ComposeBOM 2026.02.01선언형 UI. XML 레이아웃 없이 코드로 화면 구성
Material 3 Expressive1.5.0-alpha16구글 최신 디자인 시스템(알파). 물결 진행바 등 표현적 컴포넌트
AGP (빌드 도구)9.2.1Android Gradle Plugin. minSdk 27 / targetSdk 36 / compileSdk 36.1
coroutines1.11.0Flow로 DNS 상태를 실시간 관찰·UI 반영
용어
Jetpack Compose · 선언형 UI
예전 안드로이드는 XML로 화면 뼈대를 그리고 코드로 일일이 값을 채웠다(명령형). Compose는 "상태가 이러면 화면은 이렇게 생겼다"를 코틀린 함수로 선언하면, 상태가 바뀔 때 프레임워크가 알아서 다시 그린다(선언형). ADNS는 DNS on/off 같은 상태를 Flow로 흘려보내고 Compose가 구독해, 외부에서 설정이 바뀌어도 화면이 즉시 따라간다.

② 특권 획득 층 — Shizuku

요소버전역할
Shizuku api / provider13.1.5루팅 없이 ADB(shell) 권한으로 특권 명령 실행을 중개
AIDL 인터페이스IPrivilegedService — 앱과 특권 프로세스 간 통신 계약
용어
Shizuku · WRITE_SECURE_SETTINGS
WRITE_SECURE_SETTINGS는 시스템 보안 설정을 바꿀 수 있는 특권 권한이라, 일반 앱은 가질 수 없다(구글이 막아 둠). Shizuku는 사용자가 한 번 ADB로 켜 두면, 그 뒤로는 앱들이 ADB와 같은 높은 권한(shell UID)으로 특정 명령을 실행하도록 중개해 주는 도구다. ADNS는 Shizuku를 통해 자기 자신에게 pm grant ... WRITE_SECURE_SETTINGS를 실행해, 시스템 DNS 설정을 쓸 자격을 얻는다. Shizuku가 없으면 사용자가 PC에서 ADB 명령 한 줄로 직접 부여할 수도 있다.

③ 네트워크 층 — NextDNS 관리 클라이언트

요소버전역할
OkHttp5.4.0HTTP 클라이언트. 요청 디스패처·인터셉터(API 키 주입, 401 처리)
Retrofit + Gson3.0.0NextDNS REST API를 코틀린 인터페이스로 선언·직렬화
MockWebServer5.4.0테스트에서 가짜 NextDNS 서버로 API 로직 검증

NextDNS 연동은 단순 조회가 아니라 거의 완전한 관리 API다: 프로필 목록/생성/복제/이름변경/삭제, 페이지별 설정 GET/PATCH, 차단·허용 목록, 재작성(rewrite) CRUD, 통계·로그, 기기·접근 공유, 자녀 보호까지. 엔드포인트는 api.nextdns.io(메인) · ipv4.api.nextdns.io · link-ip.nextdns.io(DDNS)로 나뉜다.

④ 보안 저장 층 — Google Tink

요소버전역할
Tink (tink-android)1.22.0NextDNS API 키·이메일을 AES-256-GCM으로 암호화
Android KeystoreOS 내장Tink 마스터 키를 하드웨어 보안 영역에 보관

사용자의 NextDNS 비밀번호는 절대 저장하지 않는다. 로그인은 이메일+비밀번호(2단계 인증 지원)로 API 키를 발급받는 데만 쓰고, 발급된 API 키만 Tink로 암호화해 SharedPreferences에 넣는다. 마스터 키는 Android Keystore(android-keystore://...)에 두어, 앱 데이터를 통째로 빼가도 키 없이는 복호화가 안 된다.

스택을 관통하는 설계
인터페이스 + 안드로이드 어댑터 쌍

ADNS는 데이터 층 곳곳에서 순수 인터페이스(PrivateDnsSettings)와 안드로이드 구현(AndroidPrivateDnsSettings)을 분리한다. 덕분에 실제 기기 없이도 가짜 구현으로 단위 테스트가 가능하다(테스트 파일 55개, MockWebServer 활용). 안드로이드 앱에서 보기 드문 수준의 클린 아키텍처라, 그 자체가 좋은 학습 표본이다.

4아키텍처 심화 — 설정값 하나로 광고를 막는 법

코어는 4줄 · 그 4줄을 실행할 특권을 얻는 우회로 · 실시간 상태 관찰

ADNS의 "광고 차단 엔진"이라 부를 만한 코드는 놀랍도록 작다. Settings.Global이라는 시스템 설정 테이블에 두 개의 키를 읽고 쓰는 게 전부다. 핵심 어댑터를 그대로 옮기면:

// data/dns/AndroidPrivateDnsAdapters.kt — "차단 엔진"의 실체
class AndroidPrivateDnsSettings(private val resolver: ContentResolver) : PrivateDnsSettings {
    override fun readMode()      = Settings.Global.getString(resolver, MODE_KEY)
    override fun readSpecifier() = Settings.Global.getString(resolver, SPECIFIER_KEY)
    override fun writeMode(v: String)      = Settings.Global.putString(resolver, MODE_KEY, v)
    override fun writeSpecifier(v: String) = Settings.Global.putString(resolver, SPECIFIER_KEY, v)
    companion object {
        const val MODE_KEY      = "private_dns_mode"       // off / opportunistic / hostname
        const val SPECIFIER_KEY = "private_dns_specifier"  // DoT 서버 호스트명
    }
}

광고 차단을 켜기 = private_dns_specifier에 광고 차단 DNS 호스트명(예: ADNS-abc123.dns.nextdns.io)을 쓰고, private_dns_mode"hostname"으로 쓰는 것. 그러면 OS가 기기의 모든 DNS 질의를 그 서버로 TLS 터널링하고, 서버가 광고·추적기를 걸러 준다. 끄기 = 모드를 "off" 또는 "opportunistic"(자동)으로 되돌리기.

쓰고 끝이 아니다 — "쓰고 검증"

설정 쓰기는 실패하거나 다른 앱·OS가 되돌릴 수 있다. 그래서 PrivateDnsController는 쓴 다음 즉시 다시 읽어 기대값과 같은지 확인하고, 결과를 봉인 클래스(sealed class)로 돌려준다.

// data/dns/PrivateDnsController.kt — write-and-verify 상태기계
override suspend fun enable(hostname: String?): DnsWriteResult {
    val normalized = PrivateDnsHostname.parsePreservingCase(hostname)?.ascii
        ?: return DnsWriteResult.MissingHostname
    return writeAndVerify(expected = Hostname(normalized)) {
        settings.writeSpecifier(normalized) && settings.writeMode(MODE_HOSTNAME)
    }
}
// 결과 타입: Success / PermissionMissing / MissingHostname / Rejected / Failure
용어
sealed class(봉인 클래스) · write-and-verify
봉인 클래스는 "가능한 결과의 종류를 미리 못박아 둔" 코틀린 타입이다. DnsWriteResult는 성공/권한없음/호스트명없음/거부됨/실패 다섯 가지만 존재할 수 있어, 호출부가 when으로 모든 경우를 빠짐없이 처리하도록 컴파일러가 강제한다. write-and-verify는 "썼다고 믿지 말고 되읽어 확인"하는 방어적 패턴 — 시스템 설정처럼 외부가 간섭할 수 있는 상태를 다룰 때 필수다.

그런데 그 4줄을 실행할 "권한"이 문제

Settings.Global.putStringWRITE_SECURE_SETTINGS 권한이 있어야 한다. 일반 앱은 이 권한을 못 가진다. ADNS는 Shizuku를 통해 우회한다. Shizuku가 띄운 shell 권한 프로세스 안에서 작은 서비스(PrivilegedService)를 돌리고, 그 서비스가 pm grant 명령으로 앱 자신에게 권한을 부여한다.

// PrivilegedService.kt — Shizuku shell 프로세스 안에서 실행됨
override fun grantWriteSecureSettings(packageName: String): Boolean {
    if (packageName != BuildConfig.APPLICATION_ID)          // 자기 패키지에만
        throw SecurityException("non-app 패키지엔 부여 거부")
    val proc = ProcessBuilder(
        "pm", "grant", packageName, "android.permission.WRITE_SECURE_SETTINGS"
    ).redirectErrorStream(true).start()
    if (proc.waitFor() != 0) throw RuntimeException("pm grant 실패")
    return true
}

Shizuku가 없는 사용자는 PC에서 딱 한 줄을 실행하면 된다(앱이 안내):

adb shell pm grant com.eyalm.adns android.permission.WRITE_SECURE_SETTINGS

전체 요청 흐름

사용자 조작 상태 변경 경로 실제 차단 ───────────── ───────────── ───────── [메인 화면 토글] ┐ [퀵 설정 타일] ├──▶ DnsRepository.toggle() [홈 바로가기] │ │ (WRITE_SECURE_SETTINGS 있는지 확인) [ToggleReceiver] ┘ ▼ DnsConfigurationRepository.enable() │ ▼ PrivateDnsController.enable(hostname) │ write + verify(되읽어 확인) ▼ AndroidPrivateDnsSettings Settings.Global.putString( private_dns_specifier = "ADNS-abc123.dns.nextdns.io", private_dns_mode = "hostname") │ ▼ ┌──────────────────────────────┐ ┌───────────────┐ │ Android OS: 모든 DNS 질의를 │──DoT──▶│ NextDNS 서버 │ │ TLS로 지정 서버에 터널링 │◀───────│ 광고·추적기 차단 │ └──────────────────────────────┘ └───────────────┘ ※ 상태는 폴링이 아니라 ContentObserver로 '관찰'된다 → 외부(설정 앱 등)에서 DNS를 바꿔도 타일·알림·UI가 즉시 동기화.

주목할 점은 상태를 능동적으로 감시한다는 것이다. DnsRepository는 두 시스템 설정 URI에 ContentObserver를 걸고 이를 Flow로 노출한다. 그래서 사용자가 안드로이드 설정 앱에서 직접 Private DNS를 꺼도, ADNS의 퀵 설정 타일·상태 알림·앱 화면이 즉시 실제 상태를 반영한다. "내 앱이 켰다"가 아니라 "지금 OS가 실제로 이 상태다"를 진실의 원천으로 삼는다.

설계 포인트
Wi-Fi 규칙 — DoT가 깨지는 네트워크 자동 회피

카페·공항의 캡티브 포털(로그인 페이지를 띄우는 Wi-Fi)에서는 엄격한 DoT가 인터넷 연결을 막아 버린다. ADNS는 WifiRuleEngine이라는 순수 상태기계로 "이 SSID에선 Private DNS를 자동으로 잠시 끈다"를 구현했다. 정확한 SSID를 알아야 해서 위치 권한을 요구하며, 이 규칙 감시는 DnsRuntimeService 포그라운드 서비스가 담당한다. 광고 차단 앱이 흔히 놓치는 "차단 때문에 인터넷이 끊기는" 실전 함정을 정면으로 해결한 부분이다.

5디렉토리 구조 해부

얇은 코어(dns) + 두꺼운 NextDNS 콘솔(nextdns ~45파일) + UI/서비스
adns/ ├── build.gradle.kts · settings.gradle.kts · gradle/libs.versions.toml ├── readme.md · LICENSE (MIT) ├── fastlane/metadata/android/en-US/ # 스토어 설명·변경로그·스크린샷 └── app/ ├── build.gradle.kts · proguard-rules.pro # flavor: normal / foss └── src/ ├── main/ │ ├── AndroidManifest.xml │ ├── aidl/com/eyalm/adns/IPrivilegedService.aidl # 특권 통신 계약 │ ├── res/ (values + values-XX 32개 로케일, drawable, xml/) │ └── java/com/eyalm/adns/ │ ├── MainActivity.kt · OnboardingActivity.kt · ProviderLoginActivity.kt │ ├── AdnsApplication.kt · PrivilegedService.kt # pm grant 실행 │ ├── data/ │ │ ├── DnsRepository.kt # ★ 파사드: toggle·Flow·타일·바로가기 │ │ ├── TokenManager.kt # Tink AES-256-GCM 키 저장 │ │ ├── PrivateDnsObservation.kt # "지금 활성인가" 판정 │ │ ├── dns/ # ★ 얇은 코어: Controller·Adapters·Configuration │ │ ├── provider/ # DNS 서버 프리셋 카탈로그·호스트명 검증 │ │ ├── activation/# 권한 상태·마이그레이션 │ │ ├── nextdns/ # ★ ~45파일: api·auth·profile·logs·rewrites·access… │ │ ├── wifi/ # WifiRuleEngine 상태기계 │ │ ├── runtime/ # 포그라운드 서비스·알림 팩토리 │ │ └── network/ApiClient.kt │ ├── domain/ # AppCapabilities·ApiResult (순수 도메인) │ ├── services/ # AdnsTileService·DnsRuntimeService·BootReceiver·ToggleReceiver │ ├── ui/ # screens/·components/·theme/ (Compose) │ └── viewmodel/ # MainViewModel·OnboardingViewModel·SettingsViewModel ├── test/ (단위 테스트 44) └── androidTest/ (계측 테스트)

구조가 말해 주는 핵심: 광고를 막는 dns/ 코어는 아주 작고, 부피의 대부분은 nextdns/(약 45개 파일)라는 NextDNS 웹 콘솔의 앱 내 재구현과 UI다. 즉 "차단"은 단순하고, "관리·편의"가 이 앱의 진짜 노동량이다.

파일(줄수)왜 큰가
SetupScreen.kt (841)Shizuku/ADB/제어전용 등 여러 활성화 경로를 안내하는 온보딩
SetupGuideCatalog.kt (739)플랫폼별 DNS 설정 가이드(애플·MikroTik·CLI 등) 카탈로그
GenericListScreen.kt (578)차단·허용 목록 등 반복되는 리스트 UI의 범용 틀
WifiRulesScreen.kt (516)SSID별 자동 on/off 규칙 편집 화면
LogsScreen.kt (495)NextDNS 실시간 로그(차단/허용/기본/오류 필터)
MainActivity.kt (435)메인 토글·상태 표시·진입점(스플래시 테마)
함정 · 착시
"DNS 광고 차단 앱"이라 검색하면 VpnService 코드가 있을 거라 착각

이 앱엔 VpnService도, 로컬 DNS 리졸버도, 패킷 가로채기 코드도 전혀 없다. 처음 코드를 열면 "차단 로직이 어디 있지?" 하고 헤매기 쉬운데, 정답은 data/dns/AndroidPrivateDnsAdapters.kt4줄짜리 설정 읽기/쓰기다. 나머지는 그 스위치를 감싸는 권한 획득·상태 관찰·NextDNS 관리·UI다. "차단 알고리즘"을 찾지 말고 "설정 조작 + 서버 위임"으로 머릿속 모델을 바꿔야 한다.

6학습 포인트 — 여기서 훔쳐올 설계

OS 설정 우회 · Shizuku 특권 패턴 · 클린 아키텍처 · 안전한 비밀 저장

배울 것 ① — "새 기능을 만들지 말고, 숨은 OS 기능을 꺼내라"

ADNS의 가장 큰 교훈은 기술이 아니라 발상이다. 광고 차단 엔진을 새로 짜는 대신, 이미 OS에 있는 Private DNS를 접근성만 극적으로 높여 제품으로 만들었다. "사용자가 원하는 기능이 이미 OS·플랫폼에 있는데 너무 깊이 묻혀 있진 않은가?"를 묻는 습관은, 앱 아이디어를 찾을 때 강력한 렌즈다.

배울 것 ② — Shizuku로 루팅 없이 특권 얻기

루팅 없이 시스템 설정을 건드리는 Shizuku + AIDL 서비스 + pm grant 패턴은 그대로 재사용 가능한 레시피다. 자동화·백업·시스템 튜닝 앱을 만들 때, VPN이나 접근성 서비스 같은 침습적 우회 없이 특권 작업을 수행하는 정석 경로를 보여 준다. OnboardingViewModel의 "권한 요청 → 서비스 바인드 → grant → 폴링으로 확인" 흐름이 참고 표본이다.

실습 아이디어
Settings.Global 값 하나를 토글하는 미니 앱

WRITE_SECURE_SETTINGS가 필요한 아무 설정(예: 개발자 옵션 관련 플래그)을 Shizuku로 읽고/쓰는 최소 앱을 만들어 보라. ADNS의 PrivilegedService + AIDL을 뼈대로 삼으면, "특권 명령을 앱에서 실행"하는 감각을 가장 빠르게 익힐 수 있다.

배울 것 ③ — 인터페이스/어댑터로 안드로이드를 테스트 가능하게

안드로이드 코드는 Context·ContentResolver 의존 때문에 테스트가 어렵기로 악명 높다. ADNS는 PrivateDnsSettings(순수 인터페이스) ↔ AndroidPrivateDnsSettings(실제 구현)처럼 경계마다 인터페이스를 두어, 컨트롤러·리포지토리를 가짜 구현으로 단위 테스트한다. 봉인 클래스 결과 타입(DnsWriteResult·ApiResult)과 결합해, "성공/실패의 모든 경로"를 테스트로 못박는 법을 보여 준다.

배울 것 ④ — 비밀은 Tink + Keystore로

API 키·토큰을 SharedPreferences평문으로 넣는 실수는 흔하다. ADNS의 TokenManagerGoogle Tink AEAD(AES-256-GCM)로 암호화하고 마스터 키를 Android Keystore에 둔다. "비밀번호는 아예 저장 안 하고 API 키만, 그 키도 암호화해서"라는 최소 보관 + 암호화 원칙이 그대로 담겨 있다.

용어
AEAD · Android Keystore
AEAD(인증된 암호화)는 데이터를 암호화하면서 동시에 위변조 검증까지 하는 방식(AES-256-GCM이 대표). 복호화 때 누가 몰래 바꿨으면 실패한다. Android Keystore는 암호 키를 앱이 아니라 OS/하드웨어 보안 영역에 보관해, 앱 데이터를 통째로 덤프해도 키 자체는 빠져나가지 않게 한다. 둘을 합치면 "털려도 못 읽는" 저장이 된다.

배울 것 ⑤ — 재현 가능한 빌드로 신뢰 만들기

내 DNS를 맡기는 앱은 "배포된 APK가 정말 이 오픈소스에서 나왔나"가 중요하다. ADNS는 빌드 결과에서 dependenciesInfo를 제거하는 등 재현 가능 빌드를 맞추고, rbtlog.dev 배지로 제3자 검증을 노출한다. 오픈소스 신뢰를 주장이 아니라 검증 가능한 사실로 만드는 실전 사례다.

7시스템 / 실행 요구사항

Android 9+ 권장 · 특권 부여 1회 · 빌드엔 최신 JDK+Gradle
용도요구사항
앱 설치·실행minSdk 27(Android 8.1)이나, Private DNS(DoT)는 Android 9(Pie)부터라 9+ 권장. targetSdk 36
특권 부여(둘 중 하나)Shizuku 앱 설치 후 활성화(권장) · ② PC에서 adb shell pm grant … 1회
차단 성능지정한 DNS 서버가 결정 — NextDNS(계정) 또는 AdGuard/Cloudflare/Quad9/OpenDNS 등 공개 DoT
Wi-Fi 규칙 사용 시SSID 식별을 위한 위치 권한(코스/파인/백그라운드) — 선택 기능
소스 빌드Android Studio 또는 ./gradlew. AGP 9.2.1 · Kotlin 2.2.10 · Compose BOM 2026.02.01, 최신 JDK 필요(바이트코드 타깃은 Java 11). aidl=true
GPU불필요 — 로컬 추론·무거운 연산 없음

매니페스트의 권한 목록이 이 앱의 성격을 압축한다: 핵심WRITE_SECURE_SETTINGS, 그 외 INTERNET(NextDNS 통신), POST_NOTIFICATIONS(상태 알림), ACCESS_WIFI_STATE·위치 권한(Wi-Fi 규칙), FOREGROUND_SERVICE·RECEIVE_BOOT_COMPLETED(재부팅 후 상태 유지). 빌드 플레이버는 normal / foss 두 종으로, F-Droid 배포용 fossIS_FOSS=true로 비자유 요소를 배제한다.

주의
특권을 준다는 것의 의미

WRITE_SECURE_SETTINGS강력한 권한이다. ADNS의 PrivilegedService는 자기 패키지에만 부여하도록 방어하지만, 일반론으로 Shizuku로 앱에 특권을 주는 것 = 그 앱을 상당히 신뢰한다는 것이다. 그래서 이 앱이 MIT 오픈소스 + 재현 가능 빌드로 투명성을 강조하는 것이고, 사용자도 "소스가 공개돼 있고 빌드가 검증되는가"를 확인하는 게 옳다.

8직접 해볼 수 있는 실습 과제

클론·빌드부터 Shizuku 특권 앱, NextDNS API 연동까지
과제 1난이도 ★☆☆☆☆

클론해서 "차단 엔진 4줄" 눈으로 확인하기

git clone --depth 1 https://github.com/eyalm2000/adnsapp/src/main/java/com/eyalm/adns/data/dns/AndroidPrivateDnsAdapters.kt를 연다. "광고 차단기"의 핵심이 Settings.Global 읽기/쓰기 4줄임을 직접 확인하고, PrivateDnsController.kt로 넘어가 write-and-verify가 어떻게 결과를 DnsWriteResult 봉인 클래스로 돌려주는지 추적한다.

과제 2난이도 ★★☆☆☆

공개 DoT 서버로 Private DNS 수동 체험

앱 없이 설정 → 개인 DNS(Private DNS)dns.adguard-dns.com(AdGuard) 또는 자신의 <profileId>.dns.nextdns.io를 직접 입력해 광고가 줄어드는지 본다. 그 뒤 ADNS가 이 수동 과정을 어떻게 버튼 하나로 대체하는지 체감한다. "앱이 없어도 되는 기능을, 앱이 왜 가치 있게 만드는가"를 이해하는 과제.

과제 3난이도 ★★★☆☆

Shizuku로 특권 명령 실행하는 최소 앱

Shizuku SDK를 붙여 IPrivilegedService류의 AIDL 서비스를 하나 만들고, 버튼을 누르면 pm list packages 같은 shell 명령을 실행해 결과를 화면에 출력하는 앱을 만든다. ADNS의 OnboardingViewModel(권한요청→바인드→호출→폴링) 흐름을 그대로 축소 복제해 본다.

과제 4난이도 ★★★★☆

NextDNS API로 나의 차단 로그 조회기 만들기

NextDNS 계정의 API 키로 api.nextdns.io의 로그·통계 엔드포인트를 Retrofit으로 호출해, 최근 차단된 도메인 Top 10을 콘솔·간단 UI에 뿌린다. 키는 절대 평문 저장하지 말고 Tink AEAD + Keystore로 암호화(과제 5의 예습). ADNS의 data/nextdns/가 레퍼런스.

과제 5난이도 ★★★★★

write-and-verify + 상태 관찰을 갖춘 설정 토글 라이브러리

임의의 Settings.Global 키를 대상으로 ① 인터페이스/어댑터 분리, ② 쓰고 되읽어 검증, ③ ContentObserverFlow로 노출해 외부 변경까지 실시간 반영, ④ 결과를 봉인 클래스로 반환하는 재사용 라이브러리를 설계한다. 가짜 Settings 구현으로 단위 테스트(MockWebServer 대신 fake)를 붙여, ADNS가 왜 이렇게 테스트 가능한지 스스로 재현한다.

9관련 기술 심화 학습 로드맵

4주 코스 — DNS·DoT 기초 → 안드로이드 특권 → Compose·클린 아키텍처 → 보안·배포
주차주제 · 목표핵심 자료
1주차 DNS·DoT·필터링 DNS 이해. DNS 질의 흐름, DNS-over-TLS vs DNS-over-HTTPS, "DNS 싱크홀" 광고 차단 원리(Pi-hole·NextDNS) RFC 7858(DoT) · NextDNS/AdGuard DNS 문서 · Pi-hole 개념
2주차 안드로이드 특권·시스템 설정. Settings.Global, WRITE_SECURE_SETTINGS, Shizuku + AIDL, ContentObserver data/dns/ · PrivilegedService.kt · Shizuku 공식 문서
3주차 Compose + 클린 아키텍처. 선언형 UI, MVVM, 인터페이스/어댑터 분리, 코루틴 Flow, 봉인 클래스 결과 타입 ui/ · viewmodel/ · DnsRepository.kt · Now in Android 샘플
4주차 보안·배포. Tink AEAD + Keystore로 비밀 저장, OkHttp 인터셉터, 재현 가능 빌드·F-Droid/IzzyOnDroid 배포 TokenManager.kt · ApiClient.kt · Tink 문서 · reproducible-builds.org
곁들이면 좋은 배경
"DNS 차단의 한계"까지 알아야 진짜 이해

DNS 방식은 도메인 단위라 만능이 아니다. 같은 도메인·같은 서버에서 콘텐츠와 광고가 함께 오는 경우(대표적으로 유튜브 인앱 광고)는 못 막는다. 그래서 브라우저 차원의 uBlock Origin(요소 단위)과 DNS 차단(기기 전역)은 상호 보완 관계다. 두 계층의 차이를 이해하면 "어디까지 DNS로 막고, 어디부터 다른 도구가 필요한가"가 보인다.

10핵심 키워드 사전

이 저장소를 읽을 때 반복해서 만나는 말들
키워드
Private DNS · DNS-over-TLS(DoT)
안드로이드 9+ 내장. DNS 질의를 TLS로 암호화해 지정 서버로만 보낸다. ADNS는 이 "지정 서버"를 광고 차단 DNS로 설정하는 앱. 시스템 키 private_dns_mode/private_dns_specifier로 제어.
키워드
WRITE_SECURE_SETTINGS
시스템 보안 설정을 바꾸는 특권 권한. 일반 앱은 못 가진다. ADNS는 Shizuku/ADB로 pm grant를 실행해 자기 자신에게 부여받는다.
키워드
Shizuku
루팅 없이 앱이 ADB(shell) 수준 권한으로 특정 작업을 하도록 중개하는 도구. 사용자가 한 번 ADB로 켜 두면, 앱들이 특권 명령을 실행할 수 있다. AIDL로 앱↔특권 프로세스가 통신.
키워드
NextDNS
클라우드형 필터링 DNS 서비스. 프로필별로 차단목록·허용목록·재작성·로그를 관리한다. ADNS는 프로필 ID로 호스트명(<기기명>-<profileId>.dns.nextdns.io)을 만들어 DoT 서버로 지정하고, 계정 전체를 앱 안에서 관리.
키워드
Settings.Global · ContentObserver
Settings.Global은 기기 전역 설정을 담는 시스템 테이블. ContentObserver는 그 값의 변화를 구독하는 장치로, ADNS는 외부에서 DNS 설정이 바뀌어도 UI가 즉시 따라가게 한다.
키워드
write-and-verify
설정을 쓴 뒤 즉시 되읽어 기대값과 같은지 확인하는 방어 패턴. 외부(OS·타 앱)가 값을 되돌릴 수 있는 공유 상태를 다룰 때 필수. 결과는 DnsWriteResult 봉인 클래스로 표현.
키워드
sealed class(봉인 클래스)
가능한 하위 타입을 미리 못박은 코틀린 타입. when에서 모든 경우를 강제 처리하게 해 "처리 안 한 상태"를 컴파일 단계에서 없앤다. ADNS의 DnsWriteResult·ApiResult가 예시.
키워드
Jetpack Compose · Material 3 Expressive
코틀린 함수로 UI를 선언하면 상태 변화에 맞춰 자동 재구성되는 안드로이드 UI 툴킷. Expressive는 구글의 최신(알파) 디자인 확장으로 물결 진행바 등 표현적 요소 포함.
키워드
Tink · AEAD · Android Keystore
Google Tink는 안전한 암호화 라이브러리. AEAD(AES-256-GCM)로 암호화+위변조검증을 함께 하고, 마스터 키는 Android Keystore(하드웨어 보안영역)에 둔다. ADNS의 NextDNS API 키 보관 방식.
키워드
reproducible build(재현 가능 빌드)
같은 소스를 빌드하면 바이트까지 동일한 APK가 나오게 만드는 것. 배포본이 공개 소스에서 나왔음을 제3자가 검증할 수 있어, 오픈소스 신뢰의 근거가 된다. ADNS는 dependenciesInfo 제거 등으로 이를 맞춤.
키워드
captive portal(캡티브 포털)
카페·공항 Wi-Fi에서 인터넷 전에 뜨는 로그인 페이지. 엄격한 DoT가 이를 막아 연결이 끊길 수 있어, ADNS의 WifiRuleEngine이 해당 SSID에서 Private DNS를 자동 해제한다.

11참고 링크

원본과 배경 기술을 직접 확인하고 싶다면

저장소 · 배포

핵심 배경 기술

안드로이드 개발 스택