초기의 안드로이드 개발은 한 파일에 모든 코드가 작성 되었다. (UI 코드는 .xml)
시간이 지나면서 비지니스의 규모는 성장하고 그에 따라 사용자들이 요구하는 기능도 많아지기 시작했다. 해당 비지니스 요구사항을 만족하는 기능을 만들기 위해 개발자들은 열심히 일하였고, 1개의 파일에 모든 코드가 작성되었다. 그로인해 유지보수하기가 굉장히 힘들어지기 시작했다.
위의 이유로 코드 구조화, 분업화, 유지보수에 유연하게 대응할수 있는 개념인 디자인 패턴이 만들어 지게 되었다.
디자인 패턴의 발전 순서
MVC ⇒ MVP ⇒ MVVM
모바일 개발자라면, “디자인패턴은 뭐가 아쉬워서 이렇게 발전되어 왔는가?” 에 대한 고찰이 필요하며, 몇년전에 공부하고 기록으로 남겨야지 했는데, 이제야 적는다.
MVC = Model + View + Contorller
Model
비지니스 모델 개념과 유사하다.
다음의 3가지 개념이 Model 의 핵심이다.(데이터, 상태, 비지니스로직)
만약 당신이 예약 플랫폼 앱을 개발한다면, 예약이 제일 중요한 Model 이 될것이다.
이 예약에는
- 예약시간
- 예약하는 사람수
- 선주문 내역
- 등등..
이러한 데이터들을 포함 할것이다.
위의 중 ‘선주문 내역’은 할수도(true), 안할수도(false) 있다. = 상태를 가지고 있다.
위의 중 ‘선주문 내역’은 할지 안할지 물어본다.(비지니스로직)
View
유저가 보는 화면. UI 라고 한다.
View는 레이아웃 파일인 activity_main.xml과 같은 형태이며, 단순히 화면을 보여주는 역할을 하며 해당 컴포넌트에 입력이 있을 때 onClick 속성을 통해 어떤 함수가 호출되어야 하는지 정도를 설정할 수 있다. 실제 동작 처리는 호출된 함수에서 한다.
Controller
Model과 View의 중재자다.
View가 입력이벤트A를 받으면, 입력이벤트A가 왔다고 Controller에게 알린다. Controller는 코드에 작성된 대로 입력이벤트A 에 대한 동작을 한다.
예를 들면 Model과 상호작용하여 UI 상태를 갱신할 수 있다. 안드로이드에서 이런 역할을 하는 것은 Activity 또는 Fragment이다.
View(레이아웃 파일)와 실제로 연결되고 onClickListener 등을 통해 입력을 받았을 때 처리할 동작을 구현할 수 있다. 이때 Model 인스턴스를 생성하여 필요한 비즈니스 로직을 요청하고 결과 데이터를 가져오면 View와 연결되어 있기 때문에 UI 갱신까지 Controller에서 가능하다.
Model은 종속되는 곳이 없기 때문에 테스트하기 쉽고 재사용하기 용이하다.
그러나 Controller 입장에서는 Model에 대한 의존성이 생기고 View와 매우 강하게 결합된다.
⇒ controller 에 다양한 입력이벤트에 따른 중개코드가 엄청 쌓인다.(controller 엄청 바쁨)
실제 코드작성을 통해 이해해보자
콜라 수량에 따른 총가격 변동 앱 (-,+ 버튼)
[View]
먼저 View를 구성한다. View는 activity_order.xml이다. 사용자와 상호작용하는 것은 두 개의 버튼이고 UI 상태 변경이 필요한 텍스트뷰 또한 두 개이다.
[Controller]
View 통해서 입력을 받은 후 Controller가 처리해야 될 동작은 다음과 같다.
class OrderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_order)
// 콜라 Model과 합계 가격 Model에 대한 인스턴스를 생성 = (의존성 생김)
val colaModel = Cola()
val totalPriceModel = TotalPrice()
// View 인스턴스 생성 (의존성 생김)
val colaDeleteButton = findViewById<Button>(R.id.colaDeleteButton)
val colaAddButton = findViewById<Button>(R.id.colaAddButton)
val colaCountText = findViewById<TextView>(R.id.colaCountText)
val totalPriceText = findViewById<TextView>(R.id.totalPriceText)
colaAddButton.setOnClickListener {
colaModel.add() // 메뉴(아메리카노) 수량 증감 요청 (to Model)
colaCountText.text = "${colaModel.quantity}" // 수량 변경에 따른 UI 갱신 (to View)
totalPriceModel.increaseTotalPrice(colaModel.price) // 수량 증감에 따른 합계 가격 증감 요청 (to Model)
totalPriceText.text = "${totalPriceModel.totalPrice}" // 수량 변경에 따른 합계 가격 UI 갱신 (to View)
}
colaDeleteButton.setOnClickListener {
colaModel.delete() // 감소 버튼 입력을 받으면 수량 데이터를 변경하도록 Model에 요청한다.
colaCountText.text = "${colaModel.quantity}" // 변경된 수량 데이터(UI 상태 변경)를 View에 반영(UI 갱신)한다.
totalPriceModel.decreaseTotalPrice(colaModel.price) // 합계 가격 데이터 변경 요청
totalPriceText.text = "${totalPriceModel.totalPrice}" // 변경된 합계 가격 데이터 View 반영
}
}
}
Controller 내부(OrderActivity)에는 직접적인 데이터 변경 작업이나 비즈니스 로직이 없다. 모두 Model이 처리하고 있고 변경된 데이터를 반영할 UI 요소를 지정해주는 역할을 한다.
[Model]
Controller 동작을 정리하면 Model에 필요한 것들도 정리된다.
Beverage.kt
// Beverage = 부모클래스
open class Beverage(price: Int) {
var price = price // 메뉴 가격 (데이터)
var quantity = 0 // 메뉴 수량 (데이터)
open fun add() {} // 메뉴 수량 증감 (비즈니스 로직)
open fun delete() {}// 메뉴 수량 증감 (비즈니스 로직)
}
Cola.kt
// 부모로 부터 데이터, 비지니스 로직 상속 받음
class Cola : Beverage(800) {
override fun add() {
++quantity
}
override fun delete() {
--quantity
if (quantity < 0) {
quantity = 0
}
}
}
TotalPrice.kt
class TotalPrice {
var totalPrice = 0 // 합계 가격 (데이터)
// 합계 가격 증감 (비즈니스 로직)
fun increaseTotalPrice(price: Int) {
totalPrice += price
}
fun decreaseTotalPrice(price: Int) {
totalPrice -= price
if (totalPrice < 0) {
totalPrice = 0
}
}
}
Model을 보면 View와 연결된 부분이 전혀 없다. 그 외에도 의존성 없이 독립적이다. 대신 데이터(totalPrice)를 가지며 데이터 변경(비즈니스 로직)이 가능한 함수로 구성되어 있다. 이렇게 데이터와 가공을 담당하고 있기 때문에 Controller에서는 해당 임무를 Model에 완전히 맡길 수 있고 맡겨야 한다.
위 예제에 CafeLatte 메뉴를 추가한다고 생각해보자. (실제로 추가해보면 더 좋다.) 그러면 Controller 부분에 추가되는 View와 Model 관련 코드가 늘어날 것이다. 이런 식으로 Controller에 코드가 쌓여 비대해질 수 있는 것이 MVC의 큰 단점이다.