AI로 앱을 만들 때 모델 선택이 코드 품질에 얼마나 영향을 미칠까?
동일한 디자인 시안과 동일한 지시 사항으로 두 모델에게 Android 앱을 만들게 했다.
이전 글
클로드 디자인 결과문 13분만에 안드로이드 앱 코드 생성 및 앱 실행까지
https://json8.tistory.com/209 클로드 디자인 결과물 클로드 코드로 작업하기 - 간단 설명클로드 다지인 앱 UI 만들기: https://json8.tistory.com/208 디자인 못하는 개발자가 Claude Design으로 운동 앱 UI 만들어본
json8.tistory.com
1. 들어가며
Claude Code를 쓰다 보면 자연스럽게 드는 의문이 있다.
"Sonnet이랑 Opus, 실제로 코드 품질이 얼마나 다르지?"
비용 차이가 있으니 항상 Opus만 사용할 수는 없다.
그렇다면 실제 개발에서는 어떤 기준으로 모델을 선택해야 할까?
이번 실험은 그 질문에 대한 답을 찾기 위해 진행했다.
동일한 영어 학습 앱 디자인 시안(index.html)을 준비하고, 동일한 지시 사항으로 두 모델에게 각각 Android 앱 구현을 요청했다. 이후 생성된 결과물을 다음 관점에서 비교했다.
- Android 공식 권장 아키텍처 준수 여부
- 클린 아키텍처 적용 수준
- 타입 안전성
- 상태 관리 및 Lifecycle 처리
- 유지보수성과 확장성
- DI(Hilt) 사용 정확성
단순히 “동작하는 코드”가 아니라, 실제 팀 개발에서 유지 가능한 코드인가를 기준으로 분석했다.
2. 실험 설계
프롬프트 (CLAUDE.md 원문)
클로드 디자인으로 만들 앱 화면 index.html이다.
화면에 맞게 안드로이드 앱 만들어줘라
안드로이드 공식 3계층 아키텍처 di 포함 클린 아키텍쳐 포함
공통 조건
- 동일한 index.html 디자인 제공
- 동일한 요구사항
- 추가 수정 프롬프트 없이 1회 생성 결과만 비교
- Jetpack Compose 기반
- Hilt 사용
- Android 공식 3계층 구조(UI / Domain / Data) 요구
평가 기준
| 아키텍처 완성도 | Repository / UseCase / ViewModel 분리가 적절한가 |
| 타입 안전성 | 컴파일 타임 검증이 가능한가 |
| 상태 관리 | StateFlow, Lifecycle 처리 방식이 적절한가 |
| 코드 구조 | 파일 분리, 네이밍, 확장성이 좋은가 |
| 유지보수성 | 기능 확장 시 영향 범위가 작은가 |
| DI 정확성 | Hilt qualifier 및 주입 구조가 명확한가 |
| 툴체인 최신성 | 최신 Android 개발 패턴을 사용하는가 |
3. 결과 비교 — 아키텍처
전체 디렉토리 비교
왼쪽: Sonnet4.6
오른쪽: Opus4.7
UI 계층

Domain 계층

Data 계층

Repository 설계
가장 먼저 눈에 띄는 차이는 Repository 분리 전략이다.

EnglishAI/domain/repository/
└── UserPreferencesRepository
EnglishAI_Opus/domain/repository/
├── UserRepository.kt
├── LearningRepository.kt
└── ChatRepository.kt
Sonnet은 UserPreferencesRepository 하나로 대부분의 상태를 처리했다.
반면 Opus는 기능 도메인 기준으로 Repository를 분리했다.
이 차이는 프로젝트 규모가 커질수록 매우 커진다.
Sonnet 방식의 특징
- 초기 구현 속도가 빠름
- 구조가 단순함
- 작은 프로젝트에는 효율적
하지만 시간이 지나면 Repository 하나에 책임이 몰리기 시작한다.

UserPreferencesRepository
├── 사용자 정보 저장
├── 채팅 상태 관리
├── 학습 진도 저장
├── 설정 저장
└── 통계 관리
결국 “God Repository” 형태로 비대해질 가능성이 높다.
Opus 방식의 특징
반면 Opus는 처음부터 역할을 나눴다.



ChatRepository → 채팅 관련
LearningRepository → 학습 관련
UserRepository → 사용자 정보
이 구조는 다음 장점이 있다.
- 기능별 독립 개발 가능
- 테스트 범위 최소화
- 팀 협업 충돌 감소
- 기능 제거/교체 용이
특히 실제 현업에서는 이 구조 차이가 유지보수 비용으로 직결된다.
Feature별 ViewModel 존재 여부

Sonnet은 Chat 기능만 ViewModel이 존재하고 나머지 화면은 Screen 내부 상태 처리 중심이다.
shadowing/
└── ShadowingScreen.kt ❌
반면 Opus는 모든 Feature에 ViewModel을 생성했다.
shadowing/
├── ShadowingViewModel.kt ✅
└── ShadowingScreen.kt
이 차이는 단순 파일 개수 문제가 아니다.
Sonnet 방식 문제점
Screen 내부에서 상태를 직접 관리하면:
- 상태 저장 위치가 분산됨
- 화면 회전 대응이 어려움
- 테스트 작성이 어려움
- 비즈니스 로직이 UI에 침투
결국 Compose 함수가 거대해진다.
Opus 방식 장점
모든 Feature에 ViewModel이 있으면:
- 상태 관리 위치가 명확
- 화면은 “렌더링 역할”에 집중
- Preview/Test 작성 용이
- 재사용성 증가
즉, Opus는 “UI는 상태를 보여주기만 한다”는 Android 권장 방향에 더 가깝다.
NavGraph 구조

Sonnet
fun NavGraph(...) {
NavHost(...) {
composable(...)
composable(...)
composable(...)
// 모든 화면
}
}
178줄짜리 단일 함수 구조다.
Opus
fun AppNavGraph(...) {
NavHost(...) {
welcomeFlow(navController)
onboardingFlow(navController)
homeFlow(navController)
}
}
Flow 단위 확장 함수로 분리했다.
왜 중요한가?
초기에는 큰 차이가 없어 보인다.
하지만 화면이 10개, 20개로 늘어나면 유지보수 난이도가 완전히 달라진다.
Sonnet 구조에서는:
- merge conflict 증가
- 특정 Flow 수정 시 전체 NavGraph 영향
- 탐색 비용 증가
반면 Opus 구조는:
- Flow 단위 독립 수정 가능
- 기능별 파일 분리 가능
- Dynamic Feature 적용도 쉬움
실무에서는 후자가 훨씬 유리하다.
4. 결과 비교 — 코드 품질 편
도메인 모델: 타입 안전성
흥미롭게도 이 영역에서는 Sonnet이 더 좋은 선택을 했다.
Sonnet
val goal: GoalType = GoalType.NONE
Opus
val goalId: String? = null
왜 enum 방식이 중요한가?
Sonnet 방식은:
GoalType.TOEIC
GoalType.BUSINESS
처럼 컴파일 타임 검증이 가능하다.
잘못된 값이 들어가면 즉시 오류가 발생한다.
반면 Opus 방식은:
goalId = "toeicc"
같은 오타도 빌드가 통과한다.
즉:
| enum | 컴파일 타임 |
| String ID | 런타임 |
실제 운영 환경에서는 런타임 오류가 훨씬 치명적이다.
하지만 Opus가 String ID를 선택한 이유
이 선택이 무조건 잘못된 것은 아니다.
String 기반 구조는:
- 서버 응답 매핑 유리
- 동적 데이터 처리 가능
- Remote Config 연동 편리
- 다국어/확장성 유리
즉:
- Sonnet → 안정성 중심
- Opus → 유연성 중심
이라는 설계 철학 차이가 보인다.
🐸 개인적으로는 Sonnet 방식, 즉 enum 기반 도메인 모델을 선택한다. 유연성보다 컴파일 타임 안전성이 우선이기 때문이다. String ID 방식은 나중에 허용값이 뭔지 추적하러 코드 전체를 뒤져야 하는 상황이 생긴다. 그 비용이 초기에 enum 하나 더 정의하는 비용보다 훨씬 크다.
DI(Hilt) 정확성
Sonnet

@ApplicationContext private val context: Context
Opus

private val context: Context
Opus도 실제로는 동작한다.
하지만 코드만 보면 어떤 Context인지 명확하지 않다.
왜 qualifier가 중요한가?
Android에는 여러 종류의 Context가 존재한다.
- Application Context
- Activity Context
- Service Context
Qualifier 없이 주입하면:
- 잘못된 Context 주입 가능성
- 메모리 누수 위험
- 코드 가독성 저하
특히 팀 개발에서는 “명시성”이 중요하다.
Sonnet은 Android/Hilt 관례를 더 정확히 지켰다.
Lifecycle 인식 차이
Sonnet

collectAsState()
Opus

collectAsStateWithLifecycle()
왜 중요한가?
collectAsState()는 화면이 보이지 않아도 계속 Flow를 수집할 수 있다.
즉:
- 불필요한 CPU 사용
- 백그라운드 연산 증가
- 배터리 소모 증가
반면 collectAsStateWithLifecycle()은 Lifecycle 상태를 인식한다.
STARTED 상태일 때만 collect
최근 Android 공식 권장 패턴도 후자다.
이 부분은 Opus가 최신 Android 트렌드를 더 잘 반영했다.
5. 핵심 코드 스니펫 비교
ChatViewModel — 가장 큰 차이

Sonnet
private val CHAT_SCRIPT = listOf(...)
ViewModel 내부에 데이터가 하드코딩돼 있다.
Repository도 없고 UseCase도 없다.
즉:
UI + 상태 + 데이터
가 한 클래스 안에 섞여 있다.
Opus
observeChat: ObserveChatUseCase
loadInitial: LoadInitialChatUseCase
sendMessage: SendChatMessageUseCase
UseCase 기반 구조다.
왜 차이가 큰가?
Sonnet 구조는 빠르게 화면을 만드는 데는 좋다.
하지만:
- API 연결
- 테스트 작성
- 캐시 추가
- 오프라인 처리
가 들어가는 순간 구조 변경이 필요하다.
반면 Opus는 이미 계층 분리가 되어 있다.
UI
↓
ViewModel
↓
UseCase
↓
Repository
↓
DataSource
즉, 실제 서비스 확장에 훨씬 유리하다.
Onboarding 상태 처리 방식

Sonnet — 명시적 저장
setGoal()
saveGoal()
Opus — 즉시 저장
onGoalSelected() {
saveGoal(goalId)
}
설계 철학 차이
Sonnet
- 상태를 메모리에 유지
- 사용자가 완료 시 저장
장점:
- Undo/Cancel 구현 쉬움
- 저장 시점 제어 가능
단점:
- 상태 불일치 가능성
- 저장 누락 가능성
Opus
- 변경 즉시 영속화
장점:
- 상태 일관성 높음
- 앱 종료에도 안전
단점:
- 저장 호출 빈도 증가
- Undo 구현 복잡
둘 다 장단점이 있으며, 앱 특성에 따라 정답이 달라진다.
Sonnet 방식은 "선택 → 저장" 두 단계가 분리되어 있어 타이밍 버그 가능성이 생긴다. Opus 방식은 선택 즉시 저장하므로 상태 불일치가 없다.
- 보완 방향: 무조건 Opus의 '즉시 저장(write-through)'이 정답은 아닙니다. UI에서 사용자가 마음을 바꿔 이것저것 누를 때마다 DataStore 디스크 쓰기(I/O)가 일어나거나 네트워크 API를 친다면 오히려 리소스 낭비일 수 있습니다. Sonnet의 '임시 메모리 저장 후 최종 저장' 방식이 클라이언트 앱에서는 더 일반적이고 안전할 때가 있습니다.
- 단, 사용자가 선택을 자주 바꿀 수 있는 온보딩 특성상, Sonnet처럼 메모리에 유지하다가 최종 버튼을 누를 때 묵어서 저장하는 것이 불필요한 디스크 I/O를 줄이는 장점도 있습니다."
6. 전체 평가 요약
| 아키텍처 완성도 | ★★★☆☆ | ★★★★★ |
| 레이어 분리 | ★★☆☆☆ | ★★★★★ |
| 타입 안전성 | ★★★★★ | ★★★☆☆ |
| Lifecycle 대응 | ★★★☆☆ | ★★★★★ |
| 코드 구조화 | ★★★☆☆ | ★★★★★ |
| 유지보수성 | ★★★☆☆ | ★★★★★ |
| DI 정확도 | ★★★★★ | ★★★★☆ |
| 최신 Android 패턴 반영 | ★★★☆☆ | ★★★★★ |
| 프로토타이핑 속도 | ★★★★★ | ★★★☆☆ |

7. 최종 결론 — 실제 개발에서는 어떻게 쓰면 좋을까
이번 실험에서 가장 흥미로운 부분은 두 모델이 “잘하는 영역”이 완전히 달랐다는 점이다.
Sonnet 4.6 특징
Sonnet은:
- 빠른 UI 생성
- 간단한 상태 처리
- 타입 안전한 모델링
에는 강했다.
특히 enum 기반 모델링은 꽤 인상적이었다.
하지만 아키텍처 분리는 부분적으로만 구현됐다.
즉:
"동작하는 앱" 생성에는 강하지만
"오래 유지할 구조"까지는 부족했다.
Opus 4.7 특징
Opus는:
- 계층 분리
- UseCase 구조
- Flow 기반 설계
- Lifecycle 처리
등에서 훨씬 성숙한 결과를 보여줬다.
실제 Android 시니어 개발자가 초기에 잡는 구조와 유사했다.
반면 타입 모델링에서는 지나치게 유연성을 선택했다.
8. 가장 현실적인 활용 전략
실제로는 둘 중 하나만 고집하는 것보다 역할 분리가 효율적이다.
추천 전략
Opus
- 프로젝트 초기 생성
- 아키텍처 설계
- DI 구조 생성
- NavGraph 설계
- Repository / UseCase 생성
Sonnet
- 화면 추가 작업
- 반복 UI 구현
- 단순 Feature 생성
- 리팩토링 보조
즉:
Opus = 구조 설계 담당
Sonnet = 생산성 담당
조합이 가장 효율적이었다.
🐸 현실적으로 Opus 4.7은 토큰 비용 부담이 크다. 매 작업마다 Opus를 쓰는 건 개인 프로젝트 기준으로 오래 유지하기 어렵다.
그래서 내가 택한 방식은 이렇다. 최초 구조 설계는 Opus, 이후 설계 검토와 실제 개발은 Sonnet. 구조가 한 번 잡히면 Sonnet도 그 틀 안에서 충분히 일관된 코드를 만들어낸다.
한 가지 덧붙이자면, 어떤 모델을 쓰든 생성된 코드를 그대로 믿는 건 위험하다. 이번 실험에서 봤듯 두 모델 모두 각자 다른 방식으로 실수를 했다. AI가 만든 코드의 품질을 판단하려면 결국 본인의 개발 실력이 뒷받침되어야 한다. 도구에 의존하는 것과 도구를 제대로 쓰는 것은 다르다.
9. 마무리
같은 프롬프트를 넣어도 모델마다 “코드를 바라보는 관점” 자체가 달랐다.
- Sonnet은 빠르게 결과를 만드는 데 집중했고
- Opus는 구조적 완성도를 우선시했다
AI 코드 생성 시대에는 이제 프롬프트만 중요한 것이 아니다.
어떤 모델을 선택하느냐 자체가 아키텍처 결정이다.
추가로 비교해보면 재미있는 항목들
이번 글에서는 다루지 않았지만, 다음 항목도 비교해보면 흥미롭다.
- 테스트 코드 생성 품질
- Compose Preview 활용도
- Error Handling 패턴
- Coroutine Scope 관리
- 멀티모듈 구조 생성 능력
- Retrofit/Room 통합 품질
- 성능 최적화 패턴
- 접근성(Accessibility) 대응
아마 모델별 철학 차이가 더 극명하게 드러날 가능성이 높다.
테스트 코드: https://github.com/JsonCorp/claude-android-architecture-benchmark.git
'AI 관련 자료' 카테고리의 다른 글
| 클로드 디자인 결과문 13분만에 안드로이드 앱 코드 생성 및 앱 실행까지 (0) | 2026.05.16 |
|---|---|
| 클로드 디자인 결과물 클로드 코드로 작업하기 - 간단 설명 (0) | 2026.05.12 |
| 디자인 못하는 개발자가 Claude Design으로 운동 앱 UI 만들어본 솔직 후기 (0) | 2026.05.09 |
| Claude Cowork 시작하기: 개발자를 위한 입문 가이드 (0) | 2026.05.09 |
| Karpathy의 LLM Wiki 완전 정복: 초보자도 따라 할 수 있는 개념 정리 (0) | 2026.05.08 |