Mvx의 대원칙 (MVC)

MVC 플로우

MVC 요약정리본

여기서 의존성이란 내부에 해당 코드가 있는지 여부로 쉽게 이해 가능

역할
의존성 허용 대상
의존성 금지 대상
비고

Model

없음

Controller, View

Model은 순수 데이터 및 비즈니스 로직만 포함

View

Model

Controller

View는 Model의 데이터만 사용, Controller 직접 접근 금지

Controller

Model, View

없음

Controller는 Model과 View 모두 접근 가능

  • View가 Model로부터 데이터를 받을 때: 사용자마다 다르게 보여주는 데이터만 받아야 함.

  • View가 Model로부터 데이터를 받을 때: 반드시 Controller를 통해 받아야 함.

예제 코드 (Flutter/Dart 기반)

아래는 위 원칙을 적용한 간단한 예제입니다.

1. Model

  • Model은 Controller와 View에 의존하지 않아야 한다.

    (Model 내부에 Controller와 View에 관련된 코드가 있으면 안 된다.)

class User {
  final String name;
  final int age;

  User({required this.name, required this.age});
}

2. View

  • View는 Model에만 의존해야 하고, Controller에는 의존하면 안 된다.

    (View 내부에 Model의 코드만 있을 수 있고, Controller의 코드가 있으면 안 된다.)

  • View가 Model로부터 데이터를 받을 때는, 사용자마다 다르게 보여주어야 하는 데이터에 대해서만 받아야 한다.

import 'package:flutter/material.dart';

class UserView extends StatelessWidget {
  final String displayName;

  const UserView({required this.displayName, super.key});

  @override
  Widget build(BuildContext context) {
    return Text('안녕하세요, $displayName님!');
  }
}
  • View는 오직 사용자에게 보여줄 데이터만 받음 (displayName).

3. Controller

  • controller는 Model과 View에 의존해도 된다.

    (Controller 내부에는 Model과 View의 코드가 있을 수 있다.)

class UserController {
  final User user;

  UserController(this.user);

  // 사용자마다 다르게 보여줄 데이터만 가공
  String getDisplayName() {
    return '${user.name} (${user.age}세)';
  }
}

4. 연결 예시 (Flutter 위젯 트리에서)

User user = User(name: '홍길동', age: 30);
UserController controller = UserController(user);

UserView(displayName: controller.getDisplayName());
  • View는 Controller를 통해 Model의 데이터를 받음.

  • View는 Controller와 직접적으로 의존하지 않고, 필요한 데이터만 생성자 파라미터로 받음.

  • Controller는 Model과 View 모두를 사용할 수 있음.

요약

  • Model: 데이터와 비즈니스 로직만 담당, View/Controller와 분리

  • View: 사용자에게 보여줄 데이터만 받고, Controller 직접 참조 금지

  • Controller: Model과 View 모두 접근 가능, View에 필요한 데이터를 가공해 전달

다만, 왜 Android 환경에서 MVC 패턴이 잘 적용되지 않는가

1. 모바일 환경의 문제

  • 복잡한 비동기 처리 모바일 앱은 네트워크, 센서 등 다양한 비동기 작업이 많아, 단순한 MVC 구조로는 이런 이벤트를 깔끔하게 분리하기 어렵습니다.

  • 라이프 사이클 처리 모바일 앱의 Activity/Fragment는 생명 주기(lifecycle)가 복잡해서, 뷰와 컨트롤러의 역할을 명확히 나누기가 어렵습니다.

  • UI 로직 분리의 어려움 웹의 HTML은 뷰와 컨트롤러가 완전히 분리되지만, 모바일은 뷰(UI)와 로직(Controller)이 자연스럽게 섞이기 쉽습니다.

예시

  • 웹: 버튼 클릭 시 JS 컨트롤러에서만 로직 처리

  • 모바일: 버튼 클릭 이벤트가 Activity/Fragment에 직접 구현됨

2. Android의 구조적 한계

  • 뷰-컨트롤러 분리의 애매함

    • Android의 XML은 레이아웃(뷰)만 정의할 수 있고, UI 로직(컨트롤러)을 넣을 공간이 없습니다.

    • 컨트롤러 역할을 Activity/Fragment가 담당해야 하는데, 이들이 뷰와 컨트롤러 역할을 동시에 하게 됩니다.

예시

// Activity가 뷰와 컨트롤러 역할을 모두 담당
class UserActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)
        // 뷰 초기화 + 버튼 클릭 이벤트 처리(컨트롤러 역할)
        findViewById<Button>(R.id.button).setOnClickListener {
            // 비즈니스 로직까지 여기서 처리
        }
    }
}
  • 위 코드처럼 Activity에 UI 코드와 로직 코드가 섞여 있음

  • MVC의 Controller와 View가 명확히 분리되지 않음

3. 결과적 문제점

  • Fat Activity/Fragment Activity/Fragment에 로직이 집중되어 코드가 비대해짐 (유지보수, 테스트, 확장성 모두 저하)

  • 테스트 어려움 대부분의 테스트 케이스에서 Android의 Context가 필요해 유닛 테스트가 어렵고, 관심사의 분리가 무너짐

예시

  • Activity에서 직접 데이터 로딩, 화면 갱신, 네트워크 호출까지 처리

  • 뷰와 로직이 섞여 테스트 코드 작성이 복잡해짐

안드로이드에서의 해결책

참고

Last updated