아키텍처 패턴
아키텍처 패턴은 디자인 패턴과 소프트웨어공학에서 발생하는 문제를 해결한다는 점에서 비슷합니다.
하지만 디자인 패턴은 특정 문제를 해결하기 위한 방법이고, 아키텍처 패턴은 전체적인 소프트웨어에서 발생하는 문제들을 해결하기 위한 방법입니다.
앱 아키텍처 패턴에는 MVC, MVP, MVVM, MVI 등 다양한 패턴이 존재합니다.
각 앱의 특성에 맞게 알맞은 패턴을 적용해 앱을 설계해야 합니다.
MVC 패턴
MVC 패턴은 Model, View, Controller로 구성되어 있습니다.
- Model : 앱의 데이터를 저장하고 처리하는 역할을 담당합니다.
- SQLite, File, Content Provider 등이 포함됩니다.
- View : 화면 구성을 담당하는 영역입니다.
- 이 영역은 View 클래스를 상속하는 클래스를 사용해서 구성할 수 있습니다.
- Controller : Model과 View를 서로 연결하고 제어하는 영역입니다.
- Activity, Fragment, Service, Broadcast Receiver 등이 포함됩니다.
class MvcActivity : AppCompatActivity() {
private lateinit var binding: ActivityMvcBinding
private val model = ImageCountModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.loadImageButton.setOnClickListener {
// Controller에서 Model 업데이트
model.update()
// Controller에서 View 수정
with(binding) {
imageView.run {
setBackgroundColor(Color.parseColor(color))
load(url)
}
imageCountTextView.text = "이미지 수 : ${model.count}"
}
}
}
}
MVC 패턴의 장점
- 가장 구현하기 쉽습니다.
- 가장 단순한 패턴이기 때문에 러닝 커브가 낮고 널리 사용됩니다.
MVC 패턴의 단점
- Model과 View 사이의 의존성이 높기 때문에 앱이 커질수록 스파게티 코드가 될 위험이 높아집니다.
- Controller에 많은 코드가 모이게 되어 결국 Activity가 비대해집니다.
- Activity에서 View와 Controller가 결합되어 있기 때문에 UnitTest에 어려움이 있습니다.
MVP 패턴
MVP 패턴은 Model, View, Presenter로 구성되어 있습니다.
- Model : 앱의 데이터를 저장하고 처리하는 역할을 담당합니다.
- MVC 패턴의 Model과 동일합니다.
- View : 화면 구성을 담당하는 영역입니다.
- 이벤트가 발생하면 Presenter로 알린 후 반환받은 것에 따라 UI를 갱신합니다.
- Presenter : View에서 전달받은 이벤트를 처리하여 다시 View로 반환합니다.
- Model에 데이터 요청이 필요한 이벤트가 들어오는 경우, Model에 요청한 후 받은 데이터를 View에 전달합니다.
class MvpActivity : AppCompatActivity(), MvpContractor.View {
private lateinit var binding: ActivityMvpBinding
private lateinit var presenter: MvpContractor.Presenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMvpBinding.inflate(layoutInflater)
setContentView(binding.root)
presenter = MvpPresenter(ImageCountModel())
presenter.attachView(this)
// View에서 Presenter에 요청
binding.loadImageButton.setOnClickListener {
presenter.loadRandomImage()
}
}
override fun onDestroy() {
presenter.detachView()
super.onDestroy()
}
// Presenter에서 제공한 데이터를 이용하여 View 수정
override fun showImage(imageUrl: String, color: String) {
binding.imageView.run {
setBackgroundColor(Color.parseColor(color))
load(imageUrl) {
crossfade(300)
}
}
}
// Presenter에서 제공한 데이터를 이용하여 View 수정
override fun showImageCountText(count: Int) {
binding.imageCountTextView.text = "이미지 수 : $count"
}
}
class MvpPresenter(
private val model: ImageCountModel
) : MvpContractor.Presenter {
private var view: MvpContractor.View? = null
override fun attachView(view: MvpContractor.View) {
this.view = view
}
override fun detachView() {
view = null
}
override fun loadRandomImage() {
// Presenter에서 Model 업데이트
model.update()
// Presenter에서 View에 데이터 전달
view?.showImage(url, color)
view?.showImageCountText(model.count)
}
}
interface MvpContractor {
interface View {
fun showImage(imageUrl : String, color: String)
fun showImageCountText(count : Int)
}
interface Presenter {
fun attachView(view: View)
fun detachView()
fun loadRandomImage()
}
}
MVP 패턴의 장점
- Presenter를 통해서만 데이터를 전달받기 때문에 Model과 View의 의존성이 없습니다.
- 따라서, MVC 패턴의 단점을 해결할 수 있습니다.
MVP 패턴의 단점
- View와 Presenter가 1:1로 강한 의존성을 가지게 됩니다.
- 앱이 커질수록 Presenter 또한 비례해서 커지므로 유지보수가 힘들어집니다.
MVMM 패턴
MVVM 패턴은 Model, View, ViewModel로 구성되어 있습니다.
- Model : 앱의 데이터를 저장하고 처리하는 역할을 담당합니다.
- MVC 패턴의 Model과 동일합니다.
- 데이터가 변경되면 ViewModel을 거쳐서 View로 전달됩니다. (LiveData 또는 RxJava 사용)
- View : 화면 구성을 담당하는 영역입니다.
- View는 Model은 알지 못합니다.
- 따라서, ViewModel을 옵저빙하고 있다가 상태 변화가 전달되면 화면을 갱신합니다.
- ViewModel : View에 연결할 데이터와 명령으로 구성되어 있습니다.
- 상태 변경 알림을 통해 View에게 상태 변화를 전달합니다.
- ViewModel은 Model은 알지만 View는 알지 못합니다.
class MvvmActivity : AppCompatActivity() {
private val viewModel: MvvmViewModel by viewModels {
MvvmViewModel.MvvmViewModelFactory(ImageRepository())
}
private lateinit var binding: ActivityMvvmBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMvvmBinding.inflate(layoutInflater).also {
setContentView(it.root)
it.lifecycleOwner = this
it.view = this
it.viewModel = viewModel
}
// View에서 ViewModel에 요청
binding.loadImageButton.setOnClickListener {
viewModel.loadRandomImage()
}
}
}
class MvvmViewModel(private val imageRepository: ImageRepository) : ViewModel() {
class MvvmViewModelFactory(private val imageRepository: ImageRepository) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MvvmViewModel(imageRepository) as T
}
}
private val _countLiveData = MutableLiveData<String>()
val countLiveData: LiveData<String> by lazy { _countLiveData }
private val _imageLiveData = MutableLiveData<Image>()
val imageLiveData: LiveData<Image> by lazy { _imageLiveData }
private var disposable: CompositeDisposable? = CompositeDisposable()
private var imageCount = 0
// ViewModel에서 Model 업데이트 및 View 수정
fun loadRandomImage() {
disposable?.add(imageRepository.getRandomImage()
.doOnSuccess {
imageCount++
}
.subscribe { item ->
_imageLiveData.value = item
_countLiveData.value = "이미지 수 : $imageCount"
})
}
override fun onCleared() {
super.onCleared()
disposable?.dispose()
disposable = null
}
}
MVMM 패턴의 장점
- Model과 View 사이에 의존성이 없다.
- View는 ViewModel을 알지만, ViewModel은 View를 알지 못합니다.
- ViewModel은 Model을 알지만, Model은 ViewModel을 알지 못합니다.
- 즉, 한쪽 방향으로만 의존 관계가 있어서 각 모듈별로 분리하여 개발을 할 수 있습니다.
- 따라서, 모듈화 개발에 적합한 만큼 UnitTest를 쉽게 할 수 있습니다.
MVMM 패턴의 단점
- 장점이 많은 만큼 러닝 커브가 높습니다.
- 데이터 바인딩이 필수적으로 요구됩니다.
- 간단한 UI를 만들 경우에도 ViewModel을 설계해야 하므로 과도한 엔지니어링이 일어날 수 있습니다.
MVI 패턴
MVI 패턴은 Model, View, Intent로 구성되어 있습니다.
- Model : 다른 Model과 다르게 상태(State)를 의미합니다.
- Intent로 전달받은 객체에 맞추어 새로운 불변 객체를 Model로 생성합니다.
- View : 화면 구성을 담당하는 영역입니다.
- Model의 결과물인 상태를 옵저빙하고 있다가 변경 시 UI 업데이트를 진행하게 됩니다.
- Intent : 앱의 상태를 바꾸려는 의도를 의미합니다.
- Model에게 앱의 상태를 전달합니다.
class MviActivity : AppCompatActivity() {
private val viewModel: MviViewModel by viewModels {
MviViewModel.MviViewModelFactory(ImageRepository())
}
private lateinit var binding: ActivityMviBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMviBinding.inflate(layoutInflater).also {
setContentView(it.root)
it.view = this
}
// View에서 Intent를 전달
binding.loadImageButton.setOnClickListener {
lifecycleScope.launch {
viewModel.channel.send(MviIntent.LoadImage)
}
}
observeViewModel()
}
// state가 변할 때마다 호출
private fun observeViewModel() {
lifecycleScope.launch {
viewModel.state.collectLatest { state ->
when (state) {
is MviState.Idle -> {
// Side Effect 관리
binding.progressView.isVisible = false
}
is MviState.Loading -> {
// Side Effect 관리
binding.progressView.isVisible = true
}
is MviState.LoadedImage -> {
// Side Effect 관리
binding.progressView.isVisible = false
// View 수정
binding.imageView.run {
setBackgroundColor(Color.parseColor(state.image.color))
load(state.image.url) {
crossfade(300)
}
}
binding.imageCountTextView.text = "이미지 수 : ${state.count}"
}
}
}
}
}
}
class MviViewModel(private val imageRepository: ImageRepository) : ViewModel() {
class MviViewModelFactory(private val imageRepository: ImageRepository) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MviViewModel(imageRepository) as T
}
}
val channel = Channel<MviIntent>()
private val _state = MutableStateFlow<MviState>(MviState.Idle)
val state: StateFlow<MviState> get() = _state
private var count = 0
init {
handleIntent()
}
// Intent 처리 과정
private fun handleIntent() {
viewModelScope.launch {
channel.consumeAsFlow().collectLatest {
when (it) {
MviIntent.LoadImage -> {
loadImage()
}
}
}
}
}
// Model 업데이트 및 상태 변경
private fun loadImage() {
viewModelScope.launch {
_state.value = MviState.Loading
val image = imageRepository.getRandomImage()
count++
_state.value = MviState.LoadedImage(image, count)
}
}
}
sealed class MviState {
object Idle : MviState()
object Loading : MviState()
data class LoadedImage(val image: Image, val count: Int) : MviState()
}
MVI 패턴의 장점
- 하나의 State 객체만을 바라보기 때문에 상태 충돌이 일어나기 어렵습니다.
- 데이터의 흐름을 파악하기 쉽습니다.
- Model은 불변 객체이기 때문에 스레드에 대해 안전합니다.
MVI 패턴의 단점
- 기본적으로 MVP와 MVMM을 적용하는 등 러닝 커브가 매우 높습니다.
- Model 업데이트를 위해 새로운 인스턴스를 생성하기 때문에 리소스가 낭비될 수 있습니다.
- View의 작은 변경에도 Intent를 거져야 합니다.
728x90
반응형
'안드로이드 > 개념' 카테고리의 다른 글
[Android] 코루틴 (0) | 2023.11.27 |
---|---|
[Android] Rx와 Observable (0) | 2023.11.03 |
[Android] 정규표현식 (0) | 2023.10.11 |
[Android] LiveData (0) | 2023.10.09 |
[Android] ViewBinding과 DataBinding (0) | 2023.10.08 |