Ver3.5 앱 개발의 설계도,MVC · MVP · MVVM 완전 정복 🗺️

앱 개발의 설계도,
MVC · MVP · MVVM 완전 정복 🗺️
복잡한 패턴도 친구처럼 쉽게 — 실전 예제와 함께 파헤쳐보자!
🏗️ 아키텍처 패턴, 왜 필요한 거야?
앱을 처음 만들 때는 그냥 막 짜도 돌아가잖아? 근데 기능이 늘어나고, 팀원이 생기고, 유지보수를 해야 하는 순간이 오면... 😱
"이 코드 누가 짠 거야? 아, 나야..." 라는 공포의 순간을 경험하게 돼.
그래서 등장한 게 바로 아키텍처 패턴(Architecture Pattern)이야.
쉽게 말하면 앱을 어떻게 구조적으로 나눌지에 대한 설계 원칙이야.
집 지을 때 설계도 없이 짓는 사람 없잖아? 앱도 마찬가지야! 🏠
2. 테스트 용이성 — 각 레이어를 독립적으로 테스트 가능
3. 유지보수성 — 코드 수정 시 다른 부분에 영향 최소화
4. 협업 효율 — 팀원들이 각자 맡은 레이어에 집중 가능
5. 재사용성 — 비즈니스 로직을 다른 플랫폼에서도 재활용
모바일 앱 개발에서 가장 많이 쓰이는 3대 패턴이 바로 MVC, MVP, MVVM이야.
각각 어떤 특징이 있고, 언제 어떤 걸 써야 하는지 지금부터 제대로 파헤쳐보자! 🔍
🎯 MVC 패턴 — 아키텍처의 할아버지
MVC는 Model-View-Controller의 약자야.
1970년대에 Smalltalk 언어에서 처음 등장했으니까 진짜 오래된 패턴이지.
근데 아직도 많이 쓰인다는 게 포인트! 고전은 이유가 있어 🎖️
📌 MVC의 각 구성요소 역할
Model은 View나 Controller에 대해 전혀 몰라도 돼 — 완전히 독립적이야.
🖼️ View (뷰)
사용자에게 보이는 UI 화면이야. 버튼, 텍스트, 이미지 등 시각적 요소들을 담당해.
View는 데이터를 직접 처리하지 않고, 그냥 보여주는 역할만 해.
🎮 Controller (컨트롤러)
Model과 View 사이의 중간 관리자야. 사용자 입력을 받아서 Model에 전달하고,
Model의 변경사항을 View에 반영하는 역할을 해.
📱 iOS에서의 MVC — 현실은 좀 달라
Apple이 공식적으로 권장하는 패턴이 MVC야. 근데 iOS 개발을 해본 사람이라면 알겠지만...
UIViewController가 Controller 역할을 하는데, 실제로는 View 생명주기 관리까지 담당하다 보니
View와 Controller가 너무 강하게 결합되는 문제가 생겨. 😅
결국 ViewController 파일이 수천 줄짜리 괴물이 되는 Massive View Controller 현상이 발생하는 거야.
이 문제를 해결하려고 MVP, MVVM 같은 패턴들이 등장한 거라고 보면 돼!
🤖 Android에서의 MVC
Android에서는 Activity나 Fragment가 Controller 역할을 하는 경우가 많아.
마찬가지로 UI 로직과 비즈니스 로직이 뒤섞이는 문제가 발생하기 쉬워.
그래서 Google도 나중에 MVVM을 공식 권장 패턴으로 밀게 된 거야.
- 소규모 프로젝트에 적합
- 웹 프레임워크(Spring, Rails 등)와 친숙한 구조
❌ MVC 단점
- Controller가 비대해지는 경향 (Massive VC 문제)
- View와 Controller의 강한 결합으로 테스트 어려움
- 대규모 프로젝트에서 유지보수 복잡도 증가
💻 MVC 코드 예시 (Android)
// Model: 데이터와 비즈니스 로직
class UserModel {
private String name;
private String email;
public UserModel(String name, String email) {
this.name = name;
this.email = email;
}
// 비즈니스 로직: 이메일 유효성 검사
public boolean isValidEmail() {
return email != null && email.contains("@");
}
public String getName() { return name; }
public String getEmail() { return email; }
}
// View: XML 레이아웃 (activity_main.xml)
// <TextView android:id="@+id/tv_name" ... />
// <EditText android:id="@+id/et_email" ... />
// <Button android:id="@+id/btn_save" ... />
// Controller: Activity가 Controller 역할
public class MainActivity extends AppCompatActivity {
private UserModel userModel;
private TextView tvName;
private EditText etEmail;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvName = findViewById(R.id.tv_name);
etEmail = findViewById(R.id.et_email);
// Controller가 Model 생성
userModel = new UserModel("홍길동", "hong@example.com");
// View 업데이트
updateView();
// 사용자 이벤트 처리
findViewById(R.id.btn_save).setOnClickListener(v -> {
String newEmail = etEmail.getText().toString();
userModel = new UserModel(userModel.getName(), newEmail);
if (userModel.isValidEmail()) {
updateView();
}
});
}
private void updateView() {
tvName.setText(userModel.getName());
etEmail.setText(userModel.getEmail());
}
}
🎪 MVP 패턴 — 테스트의 왕
MVP는 Model-View-Presenter의 약자야.
MVC의 Controller 대신 Presenter가 등장했어.
"뭐가 달라?" 싶겠지만, 이 차이가 엄청나게 중요해! 🔑
MVP의 핵심은 View와 Model이 서로 완전히 모른다는 거야.
모든 소통은 반드시 Presenter를 통해서만 이루어져.
🔑 MVP의 핵심 — Interface의 마법
MVP에서 가장 중요한 개념은 View Interface야.
Presenter는 실제 View(Activity, Fragment)를 직접 알지 못하고,
오직 IView 인터페이스를 통해서만 View와 소통해.
이게 왜 중요하냐고? 테스트할 때 실제 View 대신 Mock View를 넣을 수 있거든!
즉, Android 에뮬레이터 없이도 Presenter 로직을 순수 JUnit으로 테스트할 수 있어. 🧪
💻 MVP 코드 예시 (Android)
// View 인터페이스 정의
interface IUserView {
void showUserName(String name);
void showUserEmail(String email);
void showError(String message);
void showLoading(boolean isLoading);
}
// Model (Repository 패턴 적용)
class UserRepository {
public UserModel getUser(int userId) {
// API 호출 또는 DB 조회
return new UserModel("홍길동", "hong@example.com");
}
public boolean updateEmail(int userId, String newEmail) {
// 이메일 업데이트 로직
return newEmail.contains("@");
}
}
// Presenter: View를 인터페이스로만 참조!
class UserPresenter {
private IUserView view; // 인터페이스로 참조
private UserRepository repository;
public UserPresenter(IUserView view, UserRepository repository) {
this.view = view;
this.repository = repository;
}
public void loadUser(int userId) {
view.showLoading(true);
UserModel user = repository.getUser(userId);
if (user != null) {
view.showUserName(user.getName());
view.showUserEmail(user.getEmail());
} else {
view.showError("사용자를 찾을 수 없습니다");
}
view.showLoading(false);
}
public void updateEmail(int userId, String newEmail) {
boolean success = repository.updateEmail(userId, newEmail);
if (success) {
view.showUserEmail(newEmail);
} else {
view.showError("유효하지 않은 이메일입니다");
}
}
}
// View: IUserView 인터페이스 구현
public class UserActivity extends AppCompatActivity implements IUserView {
private UserPresenter presenter;
private TextView tvName, tvEmail;
private ProgressBar progressBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user);
tvName = findViewById(R.id.tv_name);
tvEmail = findViewById(R.id.tv_email);
progressBar = findViewById(R.id.progress_bar);
presenter = new UserPresenter(this, new UserRepository());
presenter.loadUser(1);
}
@Override
public void showUserName(String name) {
tvName.setText(name);
}
@Override
public void showUserEmail(String email) {
tvEmail.setText(email);
}
@Override
public void showError(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
@Override
public void showLoading(boolean isLoading) {
progressBar.setVisibility(isLoading ? View.VISIBLE : View.GONE);
}
}
// 테스트 코드 (에뮬레이터 없이 순수 JUnit!)
class UserPresenterTest {
@Test
public void testLoadUser_success() {
MockUserView mockView = new MockUserView();
UserPresenter presenter = new UserPresenter(mockView, new UserRepository());
presenter.loadUser(1);
assertEquals("홍길동", mockView.displayedName);
assertEquals("hong@example.com", mockView.displayedEmail);
}
}
- Presenter를 단위 테스트하기 매우 쉬움
- 역할이 명확하게 분리되어 협업에 유리
- MVC보다 유지보수성이 높음
❌ MVP 단점
- View마다 Presenter와 Interface를 만들어야 해서 보일러플레이트 코드 증가
- Presenter가 View를 직접 참조하므로 메모리 누수 주의 필요
- 복잡한 UI에서 Presenter가 비대해질 수 있음
⚡ MVVM 패턴 — 현대 앱의 표준
MVVM은 Model-View-ViewModel의 약자야.
Microsoft의 Ken Cooper와 Ted Peters가 2005년에 WPF를 위해 고안한 패턴인데,
지금은 모바일 앱 개발의 사실상 표준이 됐어! 🏆
Android에서는 Google이 공식 권장 패턴으로 채택했고,
iOS에서는 SwiftUI와 함께 자연스럽게 MVVM이 주류가 됐어.
Flutter, React Native에서도 MVVM 개념이 광범위하게 활용돼.
🌟 MVVM의 핵심 — 데이터 바인딩과 관찰
MVVM의 가장 큰 특징은 ViewModel이 View를 전혀 모른다는 거야.
MVP에서 Presenter는 IView 인터페이스를 통해서라도 View를 참조했잖아?
MVVM의 ViewModel은 그것조차 없어. 완전히 독립적이야! 🙅
대신 View가 ViewModel의 데이터를 관찰(Observe)해.
ViewModel의 데이터가 바뀌면, View가 알아서 자동으로 업데이트되는 구조야.
이게 바로 반응형 프로그래밍(Reactive Programming)의 핵심이야.
💻 MVVM 코드 예시 (Android + Jetpack)
// Model: Repository 패턴
class UserRepository {
private UserApiService apiService;
private UserDao userDao; // Room DB
public UserRepository(UserApiService apiService, UserDao userDao) {
this.apiService = apiService;
this.userDao = userDao;
}
// LiveData로 반환 (자동 업데이트)
public LiveData<UserModel> getUser(int userId) {
return userDao.getUserById(userId);
}
public void fetchUserFromApi(int userId) {
// API 호출 후 DB에 저장 (Single Source of Truth 패턴)
apiService.getUser(userId).enqueue(new Callback<UserModel>() {
@Override
public void onResponse(Call<UserModel> call, Response<UserModel> response) {
if (response.isSuccessful()) {
userDao.insert(response.body());
}
}
@Override
public void onFailure(Call<UserModel> call, Throwable t) {
// 에러 처리
}
});
}
}
// ViewModel: View를 전혀 모름!
public class UserViewModel extends ViewModel {
private UserRepository repository;
// LiveData: View가 관찰할 데이터
private MutableLiveData<UserModel> userLiveData = new MutableLiveData<>();
private MutableLiveData<Boolean> isLoading = new MutableLiveData<>();
private MutableLiveData<String> errorMessage = new MutableLiveData<>();
public UserViewModel(UserRepository repository) {
this.repository = repository;
}
// View가 관찰할 LiveData (읽기 전용으로 노출)
public LiveData<UserModel> getUser() { return userLiveData; }
public LiveData<Boolean> getIsLoading() { return isLoading; }
public LiveData<String> getErrorMessage() { return errorMessage; }
// View에서 호출하는 메서드
public void loadUser(int userId) {
isLoading.setValue(true);
repository.fetchUserFromApi(userId);
repository.getUser(userId).observeForever(user -> {
if (user != null) {
userLiveData.setValue(user);
} else {
errorMessage.setValue("사용자를 찾을 수 없습니다");
}
isLoading.setValue(false);
});
}
public void updateEmail(String newEmail) {
if (newEmail.contains("@")) {
// 이메일 업데이트 로직
} else {
errorMessage.setValue("유효하지 않은 이메일입니다");
}
}
}
// View: ViewModel을 관찰만 함
public class UserActivity extends AppCompatActivity {
private UserViewModel viewModel;
private TextView tvName, tvEmail;
private ProgressBar progressBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user);
tvName = findViewById(R.id.tv_name);
tvEmail = findViewById(R.id.tv_email);
progressBar = findViewById(R.id.progress_bar);
// ViewModelProvider로 ViewModel 생성 (생명주기 관리)
viewModel = new ViewModelProvider(this).get(UserViewModel.class);
// LiveData 관찰 - 데이터 변경 시 자동 UI 업데이트
viewModel.getUser().observe(this, user -> {
tvName.setText(user.getName());
tvEmail.setText(user.getEmail());
});
viewModel.getIsLoading().observe(this, isLoading -> {
progressBar.setVisibility(isLoading ? View.VISIBLE : View.GONE);
});
viewModel.getErrorMessage().observe(this, error -> {
Toast.makeText(this, error, Toast.LENGTH_SHORT).show();
});
viewModel.loadUser(1);
}
}
🔥 Kotlin + StateFlow로 더 현대적으로!
요즘 Android 개발에서는 LiveData 대신 StateFlow나 SharedFlow를 많이 써.
Kotlin Coroutines와 함께 쓰면 비동기 처리도 훨씬 깔끔해지거든! ✨
// Kotlin + StateFlow + Coroutines 버전
class UserViewModel(private val repository: UserRepository) : ViewModel() {
// UI 상태를 하나의 data class로 관리
data class UiState(
val user: UserModel? = null,
val isLoading: Boolean = false,
val errorMessage: String? = null
)
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadUser(userId: Int) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
try {
val user = repository.getUser(userId)
_uiState.update { it.copy(user = user, isLoading = false) }
} catch (e: Exception) {
_uiState.update {
it.copy(errorMessage = e.message, isLoading = false)
}
}
}
}
}
// View (Fragment)
class UserFragment : Fragment() {
private val viewModel: UserViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// StateFlow 수집
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState.collect { state ->
binding.progressBar.isVisible = state.isLoading
state.user?.let { user ->
binding.tvName.text = user.name
binding.tvEmail.text = user.email
}
state.errorMessage?.let { error ->
showSnackbar(error)
}
}
}
viewModel.loadUser(1)
}
}
- 데이터 바인딩으로 보일러플레이트 코드 감소
- 화면 회전 등 생명주기 변화에 강함
- Google/Apple 공식 권장 패턴
- 반응형 프로그래밍과 자연스럽게 결합
❌ MVVM 단점
- 초보자에게 학습 곡선이 있음
- 간단한 앱에는 오버엔지니어링이 될 수 있음
- 데이터 바인딩 디버깅이 어려울 수 있음
🔍 세 패턴 완전 비교 분석
자, 이제 세 패턴을 나란히 놓고 비교해보자! 🥊
어떤 상황에서 어떤 패턴을 선택해야 하는지 명확하게 정리해줄게.
🎯 어떤 패턴을 선택해야 할까?
팀원이 패턴에 익숙하지 않을 때
웹 백엔드와 코드 공유가 필요할 때
학습용 프로젝트
레거시 코드를 점진적으로 개선할 때
팀이 인터페이스 기반 설계에 익숙할 때
중규모 앱
Jetpack Compose / SwiftUI 사용 시
복잡한 UI 상태 관리가 필요할 때
장기 유지보수 프로젝트
🍎 iOS에서의 아키텍처 패턴
Android 얘기만 했으니까 iOS도 빠질 수 없지! 🍎
iOS 개발에서도 MVC, MVP, MVVM이 모두 쓰이는데, 각각 어떻게 적용되는지 알아보자.
📱 iOS MVC — Apple의 공식 패턴
Apple은 공식적으로 MVC를 권장하고, UIKit 자체가 MVC 구조로 설계되어 있어.
UIViewController가 Controller 역할을 하는데, 문제는 View 생명주기 관리까지 담당하다 보니
View와 Controller가 너무 강하게 결합된다는 거야.
그래서 iOS 개발자들 사이에서 MVC를 "Massive View Controller"라고 부르는 농담이 있어. 😅
// iOS MVC 예시 (Swift)
// Model
struct User {
let name: String
let email: String
var isValidEmail: Bool {
return email.contains("@")
}
}
// View (UIViewController가 View + Controller 역할)
class UserViewController: UIViewController {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var emailLabel: UILabel!
var user: User?
override func viewDidLoad() {
super.viewDidLoad()
loadUser()
}
private func loadUser() {
// 실제로는 API 호출
user = User(name: "홍길동", email: "hong@example.com")
updateUI()
}
private func updateUI() {
guard let user = user else { return }
nameLabel.text = user.name
emailLabel.text = user.email
}
@IBAction func updateEmailTapped(_ sender: UIButton) {
// 이벤트 처리도 여기서...
// 점점 파일이 커지는 문제 발생 😱
}
}
⚡ iOS MVVM — SwiftUI와의 찰떡궁합
SwiftUI가 등장하면서 iOS에서도 MVVM이 사실상 표준이 됐어.
SwiftUI의 @ObservableObject, @Published, @StateObject 같은 프로퍼티 래퍼들이
MVVM 패턴을 자연스럽게 구현할 수 있도록 설계되어 있거든! 🎯
// iOS MVVM + SwiftUI 예시 (Swift)
// Model
struct User: Codable {
let name: String
let email: String
}
// ViewModel
class UserViewModel: ObservableObject {
@Published var user: User?
@Published var isLoading = false
@Published var errorMessage: String?
private let repository: UserRepository
init(repository: UserRepository = UserRepository()) {
self.repository = repository
}
@MainActor
func loadUser(userId: Int) async {
isLoading = true
defer { isLoading = false }
do {
user = try await repository.getUser(userId: userId)
} catch {
errorMessage = error.localizedDescription
}
}
}
// View (SwiftUI)
struct UserView: View {
@StateObject private var viewModel = UserViewModel()
var body: some View {
VStack(spacing: 16) {
if viewModel.isLoading {
ProgressView()
} else if let user = viewModel.user {
Text(user.name)
.font(.title2)
.fontWeight(.bold)
Text(user.email)
.foregroundColor(.secondary)
} else if let error = viewModel.errorMessage {
Text(error)
.foregroundColor(.red)
}
}
.task {
await viewModel.loadUser(userId: 1)
}
}
}
SwiftUI 시대: MVVM이 사실상 표준, TCA(The Composable Architecture)도 주목받는 중
최신 트렌드: Swift Concurrency(async/await) + MVVM 조합이 대세
🚀 Clean Architecture와의 결합
실제 대규모 앱에서는 MVC/MVP/MVVM 단독으로 쓰는 경우보다
Clean Architecture와 결합해서 쓰는 경우가 훨씬 많아.
Clean Architecture는 Robert C. Martin(Uncle Bob)이 제안한 아키텍처로,
앱을 여러 레이어로 나누고 의존성 방향을 안쪽으로만 향하게 하는 원칙이야.
Clean Architecture를 MVVM과 결합하면 이런 구조가 돼:
→ Activity/Fragment/Composable (View)
→ ViewModel (UI 상태 관리, UseCase 호출)
Domain Layer (순수 비즈니스 로직)
→ UseCase (단일 비즈니스 로직 단위)
→ Repository Interface (추상화)
→ Entity (순수 데이터 모델)
Data Layer (데이터 소스 관리)
→ Repository 구현체
→ Remote DataSource (API)
→ Local DataSource (Room DB, SharedPreferences)
// Clean Architecture + MVVM 예시
// Domain Layer: UseCase
class GetUserUseCase(private val userRepository: UserRepository) {
suspend operator fun invoke(userId: Int): Result<User> {
return try {
val user = userRepository.getUser(userId)
Result.success(user)
} catch (e: Exception) {
Result.failure(e)
}
}
}
// Domain Layer: Repository Interface
interface UserRepository {
suspend fun getUser(userId: Int): User
suspend fun updateEmail(userId: Int, email: String): Boolean
}
// Data Layer: Repository 구현체
class UserRepositoryImpl(
private val apiService: UserApiService,
private val userDao: UserDao
) : UserRepository {
override suspend fun getUser(userId: Int): User {
return try {
val response = apiService.getUser(userId)
userDao.insert(response.toEntity())
response.toDomain()
} catch (e: Exception) {
userDao.getUserById(userId)?.toDomain()
?: throw e
}
}
override suspend fun updateEmail(userId: Int, email: String): Boolean {
return apiService.updateEmail(userId, email).isSuccessful
}
}
// Presentation Layer: ViewModel
class UserViewModel(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState = _uiState.asStateFlow()
fun loadUser(userId: Int) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
getUserUseCase(userId)
.onSuccess { user ->
_uiState.update {
it.copy(user = user, isLoading = false)
}
}
.onFailure { error ->
_uiState.update {
it.copy(error = error.message, isLoading = false)
}
}
}
}
}
🌐 Flutter와 React Native에서의 패턴
크로스플랫폼 개발도 빠질 수 없지! 🌍
Flutter와 React Native에서는 어떤 패턴을 쓰는지 알아보자.
🦋 Flutter의 상태 관리 패턴
Flutter는 자체적인 상태 관리 솔루션이 없어서 다양한 패턴이 공존해.
가장 많이 쓰이는 건 Provider, Riverpod, BLoC, GetX 등이야.
이 중 BLoC(Business Logic Component)는 MVVM과 유사한 개념이야.
// Flutter BLoC 패턴 예시 (MVVM과 유사)
// BLoC = ViewModel 역할
// Event (사용자 액션)
abstract class UserEvent {}
class LoadUserEvent extends UserEvent {
final int userId;
LoadUserEvent(this.userId);
}
// State (UI 상태)
abstract class UserState {}
class UserLoadingState extends UserState {}
class UserLoadedState extends UserState {
final User user;
UserLoadedState(this.user);
}
class UserErrorState extends UserState {
final String message;
UserErrorState(this.message);
}
// BLoC (비즈니스 로직)
class UserBloc extends Bloc<UserEvent, UserState> {
final UserRepository repository;
UserBloc(this.repository) : super(UserLoadingState()) {
on<LoadUserEvent>((event, emit) async {
emit(UserLoadingState());
try {
final user = await repository.getUser(event.userId);
emit(UserLoadedState(user));
} catch (e) {
emit(UserErrorState(e.toString()));
}
});
}
}
// View (Widget)
class UserScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => UserBloc(UserRepository())
..add(LoadUserEvent(1)),
child: BlocBuilder<UserBloc, UserState>(
builder: (context, state) {
if (state is UserLoadingState) {
return CircularProgressIndicator();
} else if (state is UserLoadedState) {
return Column(
children: [
Text(state.user.name),
Text(state.user.email),
],
);
} else if (state is UserErrorState) {
return Text(state.message);
}
return Container();
},
),
);
}
}
⚛️ React Native의 패턴
React Native는 React의 패러다임을 따르기 때문에 전통적인 MVC/MVP/MVVM보다는
Flux, Redux, MobX, Recoil 같은 상태 관리 패턴이 주로 쓰여.
그 중 Redux는 단방향 데이터 흐름을 강조하는데, MVVM의 개념과 유사한 부분이 있어.
React Native: Redux, MobX, Recoil, Zustand (Flux 아키텍처 기반)
공통점: 모두 UI와 비즈니스 로직의 분리, 단방향 데이터 흐름을 지향
🛠️ 실전 적용 팁 & 안티패턴 주의!
패턴을 알았다고 끝이 아니야! 실전에서 자주 저지르는 실수들을 알아두면
훨씬 더 깔끔한 코드를 짤 수 있어. 🎯
⚠️ 자주 저지르는 안티패턴들
파일이 1000줄이 넘어가면 뭔가 잘못된 거야. UseCase로 분리하자!
❌ 안티패턴 2: View에서 비즈니스 로직 처리
Activity/Fragment에서 API 호출하거나 데이터 가공하는 것.
View는 오직 UI 표시만 담당해야 해!
❌ 안티패턴 3: ViewModel에서 Context 참조
Android ViewModel에서 Activity Context를 직접 참조하면 메모리 누수 발생!
꼭 필요하다면 ApplicationContext를 사용하거나 AndroidViewModel을 써.
❌ 안티패턴 4: Repository 없이 ViewModel에서 직접 API 호출
ViewModel이 Retrofit 서비스를 직접 호출하면 테스트도 어렵고 재사용도 안 돼.
반드시 Repository 레이어를 통해 데이터에 접근하자!
✅ 실전 적용 베스트 프랙티스
데이터의 원본은 항상 하나여야 해. Room DB를 원본으로 하고, API 결과를 DB에 저장한 후 DB에서 읽는 방식을 권장해.
isLoading, error, data를 따로 LiveData로 관리하면 복잡해져. data class UiState로 묶어서 관리하면 훨씬 깔끔해!
Hilt(Android), Koin, Dagger 같은 DI 프레임워크를 사용하면 테스트할 때 Mock 객체 주입이 쉬워져.
"사용자 로그인", "상품 검색" 같은 단일 비즈니스 로직을 UseCase 클래스로 분리하면 재사용성과 테스트 용이성이 높아져.
Result<T> 타입이나 sealed class를 활용해서 성공/실패 케이스를 명시적으로 처리하자. try-catch를 남발하면 에러 처리가 누락되기 쉬워.
🧪 테스트 전략
아키텍처 패턴을 잘 적용했다면 테스트도 체계적으로 할 수 있어!
일반적으로 테스트 피라미드 전략을 따르는 게 좋아.
📚 학습 로드맵 & 추천 리소스
자, 이제 어떻게 공부해야 할지 로드맵을 그려볼게! 🗺️
처음 시작하는 사람부터 심화 학습을 원하는 사람까지 단계별로 정리했어.
📖 추천 학습 자료
Jetpack 라이브러리 문서 (ViewModel, LiveData, Room, Hilt)
🍎 iOS/Swift 공식 문서
developer.apple.com — SwiftUI + MVVM 공식 튜토리얼
Swift Concurrency 가이드 (async/await)
📚 추천 도서
"Clean Architecture" — Robert C. Martin (Uncle Bob)
"Android Programming: The Big Nerd Ranch Guide"
"iOS Programming: The Big Nerd Ranch Guide"
🎓 온라인 강의
Udemy, Coursera의 Android/iOS 아키텍처 강의
YouTube: Philipp Lackner (Android), Sean Allen (iOS) 채널 강추!
그리고 실제로 앱을 만들어보고 싶다면, 재능넷(https://www.jaenung.net) 같은 플랫폼에서
실력 있는 개발자에게 코드 리뷰나 멘토링을 받아보는 것도 정말 좋은 방법이야! 🙌
이론을 배운 후 실제 프로젝트에 적용하면서 피드백을 받으면 성장 속도가 훨씬 빨라지거든.
🔮 최신 트렌드 — 아키텍처의 미래
아키텍처 패턴도 계속 진화하고 있어! 최신 트렌드를 알아두면
앞으로의 개발 방향을 잡는 데 도움이 될 거야. 🚀
🎯 MVI (Model-View-Intent) 패턴
최근 Android 커뮤니티에서 주목받는 패턴이야.
MVVM의 발전형으로, 단방향 데이터 흐름(Unidirectional Data Flow)을 더 엄격하게 적용해.
Intent: 사용자의 의도(액션)
Model: 불변(Immutable) UI 상태
View: 상태를 렌더링
상태가 항상 예측 가능하고, 디버깅이 쉽다는 장점이 있어.
Jetpack Compose와 함께 쓸 때 특히 잘 맞아.
🏗️ TCA (The Composable Architecture)
iOS/SwiftUI 생태계에서 Point-Free가 만든 아키텍처야.
Redux 영향을 받았으며, 상태 관리, 사이드 이펙트, 테스트를 체계적으로 다룰 수 있어.
복잡한 앱에서 특히 강력한 힘을 발휘해.
⚡ Jetpack Compose + MVVM
Android의 선언형 UI 프레임워크인 Jetpack Compose는 MVVM과 찰떡궁합이야.
@Composable 함수가 ViewModel의 상태를 관찰하고 자동으로 리컴포지션되는 구조가
MVVM의 데이터 바인딩 개념을 더 자연스럽게 구현해줘.
// Jetpack Compose + MVVM 최신 패턴
@Composable
fun UserScreen(
viewModel: UserViewModel = hiltViewModel() // Hilt DI
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when {
uiState.isLoading -> {
Box(modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
uiState.user != null -> {
UserContent(
user = uiState.user!!,
onEmailUpdate = { newEmail ->
viewModel.updateEmail(newEmail)
}
)
}
uiState.error != null -> {
ErrorScreen(message = uiState.error!!)
}
}
LaunchedEffect(Unit) {
viewModel.loadUser(1)
}
}
@Composable
private fun UserContent(
user: User,
onEmailUpdate: (String) -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = user.name,
style = MaterialTheme.typography.headlineMedium
)
Text(
text = user.email,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
iOS: MVVM + SwiftUI + Swift Concurrency (async/await) + TCA
Flutter: BLoC + Clean Architecture 또는 Riverpod
공통 키워드: 선언형 UI, 단방향 데이터 흐름, 불변 상태, 반응형 프로그래밍
🎓 마무리 — 패턴은 도구일 뿐이야!
여기까지 MVC, MVP, MVVM을 완전히 파헤쳐봤어! 🎉
마지막으로 가장 중요한 말을 해줄게.
MVC, MVP, MVVM 모두 장단점이 있고, 프로젝트 상황에 따라 최적의 선택이 달라져.
중요한 건 패턴 자체가 아니라 왜 이 패턴을 선택했는지를 이해하는 거야.
패턴은 목적이 아니라 수단이야. 코드를 더 읽기 쉽고, 테스트하기 쉽고,
유지보수하기 쉽게 만들기 위한 도구일 뿐이야.
처음에는 MVC로 시작해서 문제를 직접 경험하고,
그 문제를 해결하기 위해 MVP나 MVVM으로 발전시켜 나가는 게
가장 자연스럽고 효과적인 학습 방법이야! 💪
그리고 혼자 공부하다가 막히는 부분이 있다면, 재능넷에서 경험 많은 개발자에게
직접 도움을 받아보는 것도 좋은 방법이야. 실제 프로젝트 코드 리뷰나 1:1 멘토링을 통해
이론과 실전의 간극을 빠르게 좁힐 수 있거든! 🚀
클래식 & 심플
테스트 친화적
현대적 표준
대규모 앱
미래 트렌드
자, 이제 직접 코드를 짜봐! 이론은 읽는 것만으로는 절대 늘지 않아.
간단한 To-Do 앱이라도 MVC → MVP → MVVM 순서로 리팩토링해보면
각 패턴의 차이를 몸으로 느낄 수 있을 거야. 화이팅! 🔥
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.