AI 시대에 Android 코드 구조화가 더 중요해진 이유
컴파일러가 규칙과 변환을 구조에 위임해 생산성을 높인 방식에서 출발해, AI가 생성한 Android 코드를 안전하게 받아들이기 위한 상태·계약·테스트·모듈 구조를 살펴본다.
AI와 함께 Android 개발을 하다 보면 초안을 얻는 속도는 확실히 빨라졌죠. 요구사항을 설명하면 여러 파일에 걸친 코드도 잠시 뒤에 받아볼 수 있습니다.
문제는 초안의 양이 아니라 그 초안을 안전하게 받아들일 수 있는가에 있죠. 생성된 코드가 컴파일되더라도 기존 상태 모델과 맞지 않거나, 특정 생명주기에서만 발생하는 오류를 숨기고 있을 수 있습니다.
컴파일러가 개발 방식을 바꾼 과정을 떠올려볼 필요가 있습니다. 컴파일러와 고수준 언어는 개발자가 기계어 명령을 하나씩 조립하지 않고 더 높은 수준의 의도와 규칙을 표현하게 했습니다. 컴파일러는 그 구조를 실행 가능한 코드로 바꾸고, 적어도 문법과 타입처럼 명확한 규칙을 벗어난 결과는 거부했습니다.
AI도 같은 방향으로 사용해야 합니다. AI에게 코드를 무제한으로 생성하게 하는 것이 아니라, 상태·계약·의존성·테스트를 먼저 구조화하고 AI는 그 경계 안의 구현을 채우게 해야 합니다. 이 글에서는 구조화된 코드가 AI를 안전한 대량 생성 도구로 바꾸는 과정을 Android 예시로 살펴보겠습니다.
1. 컴파일러의 교훈은 자동 생성보다 규칙의 위임에 있다
컴파일러가 생산성을 높인 이유를 단순히 “코드를 자동으로 만들어 주었기 때문”이라고 설명하기는 어렵습니다. 핵심은 사람이 관리하던 기계적인 변환과 규칙을 도구에 위임하고, 개발자는 더 높은 수준의 구조를 다루게 된 데 있습니다.
“컴파일러 도입으로 생산성이 몇만 배 증가했다”와 같은 표현은 측정 기준을 확인하지 않고 그대로 쓰기 어렵습니다. 기계어와 고수준 언어의 명령 수 비율, 개발자가 처리하는 기능의 양, 실행 성능은 서로 다른 지표이기 때문입니다. 이 글에서 중요한 것은 특정 배수가 아니라, 규칙과 변환을 도구에 맡기면서 생산성이 질적으로 달라졌다는 점입니다.
컴파일러는 요구사항을 이해하거나 새로운 화면 흐름을 설계하지 못합니다. 대신 타입, 가시성, 함수 시그니처와 같이 코드에 정의된 규칙을 일관되게 검사합니다. 규칙을 위반한 코드가 있다면 빌드 단계에서 거부합니다.
AI는 불완전한 요구사항을 바탕으로 구현 방법을 제안하고, 여러 파일에 흩어진 코드를 읽어 의도를 추론합니다. 새로운 코드를 빠르게 작성하는 데는 유용하지만, 생성된 결과가 제품 규칙까지 지킨다고 보장하지는 않습니다.
두 도구가 맡는 역할을 나누어 보면 아래와 같습니다.
| 작업 | 컴파일러와 테스트 | AI |
|---|---|---|
| 타입과 가시성 위반 찾기 | 선언된 규칙으로 거부 | 문맥을 읽고 오류를 추론 |
| 빠진 비즈니스 요구사항 찾기 | 별도 규칙 없이는 알 수 없음 | 요구사항과 코드에서 후보를 제안 |
| 새 구현 만들기 | 하지 못함 | 빠르게 초안 생성 |
| 결과 보장 범위 | 정의된 규칙과 테스트 안에서 확인 | 실행과 검증 전에는 보장할 수 없음 |
| 반복 비용 | 빌드·테스트 실행 자원 | 입력·출력 토큰, 지연 시간, 재시도 |
AI는 구현 후보를 빠르게 만드는 도구이고, 컴파일러와 테스트는 허용하지 않을 결과를 빠르게 제거하는 도구에 가깝습니다.
AI가 컴파일러의 역할까지 대신하는 것은 아닙니다. AI는 작업마다 관련 문맥을 읽고 의도를 추론한 뒤 생성 결과를 다시 확인해야 합니다. 초안을 만드는 속도는 빠르지만, 규칙을 한 번 정의해 두고 이후의 모든 변경에 반복 적용하는 비용까지 자동으로 사라지지는 않습니다.
그래서 AI 시대의 생산성을 코드 생성량으로만 측정하면 안 됩니다. 안전하게 통과한 변경을 얼마나 낮은 비용으로 만들 수 있는가가 더 중요한 지표입니다. 이 지점에서 구조화된 코드와 컴파일러·테스트가 AI의 생성 속도를 실제 제품 속도로 바꿔 줍니다.
어느 한쪽만 선택할 필요는 없습니다. AI가 코드를 탐색하고 생성하도록 하되, 잘못된 결과는 컴파일러와 테스트가 거부하도록 만들면 됩니다.
문제는 컴파일러가 검사할 수 있는 규칙을 우리가 코드에 얼마나 표현했는지입니다. 모든 상태를 Boolean과 nullable 값으로 풀어 놓거나, 모듈의 모든 선언을 public으로 노출하면 컴파일러가 확인할 수 있는 범위도 줄어듭니다.
코드가 구조화되지 않으면 사람과 AI만 어려워지는 것이 아닙니다. 컴파일러 역시 우리를 도와줄 방법이 줄어들게 됩니다.
2. 타입으로 표현하지 않은 규칙은 AI에게 계속 설명해야 한다
예를 들어 결제 화면의 상태를 아래와 같이 정의할 수 있습니다.
1
2
3
4
5
6
data class CheckoutUiState(
val isLoading: Boolean = false,
val isSuccess: Boolean = false,
val orderId: String? = null,
val errorMessage: String? = null,
)
위 코드는 간단하지만 실제로는 허용되는 상태 조합이 너무 많습니다.
isLoading과isSuccess가 동시에true일 수 있음- 결제에 성공했지만
orderId가null일 수 있음 - 오류 메시지와 성공 주문 번호가 함께 존재할 수 있음
이 타입을 AI에게 전달한다면 어떤 조합이 정상이고 어떤 조합이 잘못된 상태인지 프롬프트에 별도로 설명해야 합니다. 설명을 빠뜨리면 AI는 컴파일되는 코드를 만들더라도 실제 제품 규칙과 맞지 않는 상태를 생성할 수 있습니다.
이러한 문제를 줄이기 위해, 화면에서 가질 수 있는 상태를 미리 정해 두고 그 외의 상태는 만들 수 없도록 표현할 수 있습니다. Kotlin에서는 sealed interface가 이 역할을 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@JvmInline
value class OrderId(val value: String) {
init {
require(value.isNotBlank())
}
}
sealed interface CheckoutUiState {
data object Idle : CheckoutUiState
data object Submitting : CheckoutUiState
data class Completed(val orderId: OrderId) : CheckoutUiState
data class Failed(val reason: CheckoutFailure) : CheckoutUiState
}
sealed interface CheckoutFailure {
data object NetworkUnavailable : CheckoutFailure
data object PaymentRejected : CheckoutFailure
}
위와 같이 상태를 구성하면 로딩과 성공이 동시에 존재하는 객체를 만들 수 없습니다. Completed 상태에는 OrderId가 반드시 필요하며, 빈 문자열 검증도 OrderId를 생성하는 경계 한곳에서 처리됩니다. 실패 원인 역시 CheckoutFailure에 정의된 타입 안에서만 선택할 수 있습니다.
화면에서도 가능한 상태를 빠짐없이 처리할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
@Composable
fun CheckoutContent(
state: CheckoutUiState,
onRetry: () -> Unit,
) {
when (state) {
CheckoutUiState.Idle -> CheckoutForm()
CheckoutUiState.Submitting -> CheckoutProgress()
is CheckoutUiState.Completed -> Receipt(state.orderId)
is CheckoutUiState.Failed -> CheckoutError(state.reason, onRetry)
}
}
나중에 CheckoutUiState.Cancelled라는 상태를 추가한다면 위 when은 더 이상 완전하지 않게 됩니다. 이때 Kotlin 컴파일러가 처리되지 않은 분기를 알려줍니다. Kotlin 공식 문서에서도 sealed 타입과 when을 함께 사용하면 가능한 경우를 빠짐없이 검사할 수 있다고 설명합니다.
즉 AI에게 “취소 상태가 추가되었으니 모든 화면에서 처리해 줘”라고 반복해서 설명하지 않아도, 컴파일 오류가 수정해야 할 위치를 알려주는 구조가 됩니다.
컴파일러는 취소 상태의 의미를 이해하지 못합니다. 대신 가능한 분기를 모두 처리해야 한다는 구조적 제약을 반복해서 적용합니다. 비즈니스 규칙이 올바른지는 테스트와 리뷰가 별도로 확인해야 합니다.
3. Android 코드 구조는 AI가 읽어야 할 범위를 결정한다
Android 공식 아키텍처 가이드는 관심사 분리를 메서드, 클래스, 파일, 패키지, 모듈과 레이어에 명확한 책임과 경계를 두는 것으로 설명합니다. 또한 UI 상태를 불변 데이터로 노출하고, 상태는 아래로 흐르고 이벤트는 위로 전달되는 단방향 데이터 흐름을 권장합니다. (앱 아키텍처 가이드, UI 레이어 가이드)
이 원칙은 원래 테스트 가능성과 유지보수성을 높이기 위해 사용해 왔습니다. Android에서는 프로세스 종료, 화면 회전, 폴더블 크기 변경처럼 UI가 다시 만들어지는 일이 흔하기 때문에 상태의 소유자와 복원 경계를 명확히 정하는 일이 특히 중요합니다. AI 에이전트가 코드베이스를 함께 다루기 시작하면서 이 구조에는 한 가지 역할이 더 생겼습니다.
바로 AI가 어디를 읽고 어디를 수정해야 하는지 알려주는 역할입니다.
예를 들어 결제 기능이 아래와 같은 구조를 가진다고 가정해 보겠습니다.
1
2
3
4
5
6
7
8
9
feature/checkout/
├── CheckoutContract.kt
├── CheckoutViewModel.kt
├── CheckoutContent.kt
└── CheckoutViewModelTest.kt
data/payment/
├── PaymentRepository.kt
└── DefaultPaymentRepository.kt
CheckoutContract.kt에는 화면 상태와 이벤트가 있고, CheckoutViewModel은 이벤트를 받아 새로운 상태를 만듭니다. 실제 결제 데이터 접근은 PaymentRepository 경계 뒤에 위치합니다.
이렇게 된다면 UI 상태를 변경할 때는 CheckoutContract와 CheckoutContent를 먼저 확인하면 됩니다. 결제 처리 자체를 수정할 때는 PaymentRepository와 구현체를 확인하면 됩니다.
반대로 파일 이름과 패키지는 나뉘어 있지만 실제 책임이 서로 섞여 있다면, AI는 관련 코드를 찾기 위해 더 많은 파일을 읽어야 합니다. 검색 결과에서 필요한 파일을 놓칠 가능성도 함께 커집니다.
긴 컨텍스트를 지원하는 모델을 사용한다고 해서 이 문제가 완전히 해결되는 것은 아닙니다. Lost in the Middle 연구에서는 긴 입력 안에서 관련 정보의 위치가 달라지면 언어 모델의 성능도 크게 달라질 수 있으며, 컨텍스트를 늘리는 것이 항상 정확도 향상으로 이어지지는 않는다고 설명합니다. (논문)
물론 이 연구가 Android 저장소의 토큰 비용을 직접 측정한 것은 아닙니다. 구조화가 토큰을 항상 줄인다는 뜻도 아닙니다. 인터페이스, 매퍼, 모듈이 늘어나면 오히려 읽어야 할 파일이 많아질 수 있습니다. 다만 책임 경계를 통해 관련 문맥을 좁힐 수 있다면, AI의 검색과 수정 범위를 제한하는 데 도움이 될 수 있다는 설계 가설은 세울 수 있습니다.
잘 구조화된 코드는 AI가 프로젝트를 탐색할 때 사용하는 검색 인덱스와 비슷한 역할을 합니다.
- 이름은 기능의 위치를 알려준다.
- 타입은 가능한 상태를 제한한다.
- 인터페이스는 필요한 의존성만 보여준다.
- 테스트는 변경 후 지켜야 할 동작을 예제로 남긴다.
- 모듈 경계는 읽거나 호출하지 않아야 할 구현을 감춘다.
따라서 전체 저장소를 프롬프트에 포함하지 않고도 작은 작업 단위를 만들 수 있습니다.
토큰을 절약하기 위해 주석이나 변수명을 짧게 만들 필요는 없습니다. 오히려 AI가 읽지 않아도 되는 코드의 범위를 늘리는 것이 중요합니다. 실제 효과는 같은 작업을 구조화된 저장소와 그렇지 않은 저장소에 요청해 검색 파일 수, 입력 토큰, 재시도 횟수, 사람이 수정한 시간을 비교해야 확인할 수 있습니다.
4. 가장 효율적인 프롬프트는 빌드와 테스트에 남아 있다
AI 에이전트가 프로젝트 규칙을 이해할 수 있도록 지침 파일이나 README를 작성하는 것도 중요합니다. 코딩 규칙, 검증 명령어, 수정하면 안 되는 파일 등을 문서로 알려줄 수 있기 때문입니다.
하지만 문서는 코드가 변경된 이후 오래된 정보가 될 수 있고, AI가 필요한 부분을 놓칠 수도 있습니다.
반면 타입과 테스트는 코드와 함께 실행됩니다. AI가 규칙을 놓치더라도 정의된 범위 안의 잘못된 결과가 빌드나 테스트를 통과하지 못하게 만들 수 있습니다. 물론 테스트가 없는 동작이나 잘못 작성된 테스트까지 자동으로 보장해 주는 것은 아닙니다.
SWE-bench에서도 AI가 실제 저장소의 이슈를 해결했는지 평가할 때 결과 설명이 그럴듯한지를 기준으로 삼지 않습니다. 변경 전 실패하고 수정 후 통과해야 하는 테스트를 판정 신호로 사용합니다. (SWE-bench)
Android 프로젝트에서도 아래와 같은 검증 흐름을 만들 수 있습니다.
1
2
3
4
5
요구사항
→ AI가 변경 후보 생성
→ Kotlin 컴파일러와 Lint가 구조 위반 거부
→ 단위 테스트가 상태 전이 검증
→ UI 테스트가 사용자 흐름 검증
위 구조는 AI에게 모든 판단을 맡기는 방식이 아닙니다. 비용이 작은 검증부터 차례로 통과하게 만드는 방식입니다.
잘못된 타입은 컴파일 단계에서 멈추고, 상태 전이 오류는 빠른 단위 테스트에서 확인합니다. 이후 실제 Android UI에서만 확인할 수 있는 동작을 UI 테스트로 검증합니다.
즉 모든 오류를 앱 실행 이후에 발견하는 것보다 훨씬 빠르게 피드백을 받을 수 있습니다.
컴파일러는 한 번 정의한 규칙을 코드가 변경될 때마다 다시 적용합니다. AI에게 동일한 규칙을 프롬프트로 반복해서 설명하고, 생성 결과를 매번 처음부터 확인하는 것보다 안정적인 구조라고 생각합니다.
5. 모듈이 많다고 구조화가 잘된 것은 아니다
여기서 코드 구조화를 “파일과 Gradle 모듈을 최대한 작게 나누는 것”으로 생각하면 오히려 문제가 발생할 수 있습니다.
모듈이 지나치게 많아지면 AI가 실제 비즈니스 로직보다 인터페이스, 매퍼, 의존성 설정 코드를 더 많이 읽어야 할 수 있습니다. 사람 입장에서도 하나의 동작을 확인하기 위해 너무 많은 파일을 이동해야 하는 상황이 발생합니다.
Android 모듈화 가이드에서도 지나치게 세분화하면 빌드 설정과 보일러플레이트 부담이 커질 수 있다고 설명합니다. 작은 코드베이스라면 단일 데이터 모듈이나 패키지 수준의 분리만으로도 충분할 수 있습니다. (Android 모듈화 가이드)
따라서 구조화의 기준은 파일이나 모듈의 개수가 아니라 해당 경계가 실제로 어떤 제약을 만들어 주는가가 되어야 합니다.
- 잘못된 의존 방향을 실제로 막는가
- public API를 줄이고 구현을
internal로 감출 수 있는가 - 상태와 부수 효과의 소유자가 분명해지는가
- 작은 단위로 컴파일하거나 테스트할 수 있는가
- 기능 하나를 수정할 때 읽어야 할 파일이 줄어드는가
위 항목 중 어느 것도 얻지 못하고 이동해야 할 코드만 늘어난다면, 해당 경계는 사람과 AI 모두에게 추가 비용이 됩니다.
6. AI와 함께 개발하기 위한 Android 코드 구조화 기준
예전에는 코드 구조화를 장기적인 유지보수를 위한 투자로 생각하는 경우가 많았습니다. 하지만 AI와 함께 개발한다면 첫 번째 작업부터 효과를 확인할 수 있습니다.
책임이 분리되어 있으면 AI에게 전달해야 하는 파일이 줄어듭니다. 타입이 정교하면 프롬프트에 작성해야 할 예외 조건이 줄어들고, 테스트가 있다면 AI가 만든 결과를 사람이 처음부터 다시 확인하는 시간도 줄일 수 있습니다.
Android 프로젝트에서는 아래 순서로 구조를 점검해볼 수 있습니다.
- 화면의 가능한 상태와 사용자 이벤트를 먼저 타입으로 정의한다.
- UI는 불변 상태를 렌더링하고 이벤트만 전달하게 둔다.
- 비즈니스 규칙은 Android 타입에 의존하지 않는 작은 함수나 클래스로 옮긴다.
- 구현은
internal로 숨기고 필요한 계약만 노출한다. - AI가 코드를 수정한 뒤 실행할 컴파일·테스트 명령을 저장소에 둔다.
- 기능이 실제로 독립적인 배포·소유·빌드 경계를 가질 때 Gradle 모듈로 분리한다.
AI가 더 많은 코드를 작성할수록 개발자의 역할은 없어지는 것이 아니라 다른 위치로 이동합니다. 직접 작성하는 코드의 양보다, 어떤 상태를 허용할지와 어떤 경계를 컴파일러와 테스트가 지키도록 만들지를 결정하는 역할이 중요해집니다. AI가 안전하게 코드를 찍어내는 방식은 더 긴 프롬프트가 아니라, 먼저 정의된 구조 안에서 생성하게 하는 것입니다.
7. UI 조립 규격과 상태 머신은 더 강한 구조를 만들 수 있다
앞에서 살펴본 UiState, 이벤트, 모듈 경계보다 한 단계 더 강한 구조를 만들 수도 있습니다. 예를 들어 화면을 만들 때마다 아래와 같은 형태를 공통 규격으로 둘 수 있습니다.
1
2
3
4
5
@Composable
fun CheckoutScreen(
state: CheckoutUiState,
onEvent: (CheckoutEvent) -> Unit,
)
UI는 상태를 렌더링하고 사용자 행동은 이벤트로만 전달하도록 규격을 정해 두는 방식입니다. 화면마다 ViewModel의 함수를 직접 호출하거나, 상태를 화면 내부에서 수정하는 코드를 줄일 수 있습니다. AI가 새로운 화면을 추가할 때도 어떤 형태로 코드를 조립해야 하는지 더 명확하게 알 수 있습니다.
상태 전이가 복잡한 기능이라면 상태 머신을 도입할 수도 있습니다. 상태, 이벤트, 전이 규칙을 한곳에 모아 두면 “현재 상태에서 이 이벤트가 가능한가”를 코드와 테스트로 확인할 수 있습니다. 결제, 인증, 주문, 파일 업로드처럼 중간 상태와 실패·재시도 흐름이 많은 기능에서는 이러한 구조가 특히 유용할 수 있습니다.
다만 구조가 강해질수록 새로운 요구사항을 넣는 비용도 함께 커집니다.
- UI 조립 규격을 너무 엄격하게 만들면, 단순한 화면 하나를 추가할 때도 규격에 맞추기 위한 이벤트와 어댑터 코드가 늘어날 수 있음
- 상태 머신은 상태·이벤트·전이 함수·테스트를 함께 수정해야 하므로, 아직 흐름이 자주 바뀌는 기능에서는 변경 속도를 늦출 수 있음
- 처음부터 모든 화면을 같은 상태 머신으로 만들면, 실제 비즈니스 규칙보다 구조를 유지하기 위한 코드가 더 많아질 수 있음
즉 상태 머신이나 UI 규격은 확장성을 무조건 높여 주는 도구라기보다, 변경하면 안 되는 규칙이 분명할 때 그 규칙을 강제하는 도구라고 보는 편이 맞습니다.
저는 아래와 같은 경우에 조금 더 강한 구조를 고려해볼 수 있다고 생각합니다.
- 허용되는 상태와 전이 규칙이 비교적 명확한가
- 잘못된 상태 전이나 중복 실행의 비용이 큰가
- 같은 화면 흐름을 여러 개발자나 AI 에이전트가 반복해서 수정하는가
- 상태 전이를 단위 테스트로 빠르게 검증할 수 있는가
반대로 아직 화면 구성이 자주 바뀌거나, 단순히 데이터를 보여 주는 UI라면 불변 UiState와 이벤트 전달 정도만으로도 충분할 수 있습니다. 실제로 막아야 할 오류가 생겼을 때 상태 머신이나 더 엄격한 UI 규격으로 올리는 편이 확장성을 유지하기 좋습니다.
정리
컴파일러가 바꾼 것은 개발자가 관리해야 하는 기계적인 세부사항의 양과 규칙을 표현하는 수준이었습니다. AI가 바꾸고 있는 것은 구현 후보를 만드는 속도입니다. 둘 사이의 빈틈을 메우는 것이 구조화입니다.
AI에게 긴 프롬프트와 전체 저장소를 전달해 결과를 고르는 방식은 생성량은 늘려도 안전성을 보장하지 않습니다. 상태 타입과 계약이 허용 범위를 좁히고, 모듈 경계가 수정 범위를 제한하며, 컴파일러와 테스트가 결과를 반복해서 확인해야 합니다.
저라면 다음 AI 작업을 요청하기 전에 해당 화면의 UiState, 이벤트, 검증 명령부터 확인해 봅니다. 존재하면 안 되는 상태를 지금도 만들 수 있다면 프롬프트를 길게 작성하기보다 타입과 테스트를 먼저 수정하는 편이 낫습니다.
긴 글 읽어주셔서 감사합니다.