iOS에서 자주 사용되는 아키텍쳐 패턴들에 대해 어렴풋이 알고 있던 지식들을 정리하고, 부족한 부분들을 학습하기 위해 작성한 글로 개인적인 생각이 많이 포함되어 있습니다.
틀린 내용이나 지적할만한 부분은 댓글로 남겨주시면 감사하겠습니다!
아키텍쳐 패턴이 필요한 이유
혼자서 개발할 땐 코드를 어떻게 짜든 크게 문제되지 않는다.
하지만 회사를 비롯해 여러 사람들과 함께 대규모 어플리케이션을 개발하다보면 비효율이 발생하고 이는 생산성 저하로 이어질 것이다.
팀원마다 코드 작성 방식이 다르다면 일관성 없는 코드 구조가 될 가능성이 높고, UI와 비즈니스 로직이 강하게 결합된 코드가 작성될 우려도 있다.
일관성 없는 코드 구조는 어떤 문제가 발생할까?
- 남이 작성한 코드를 수정하기 위해서 어디를 고쳐야할 지 찾기 위해 많은 리소스가 들어갈 것이다.
- 코드 리뷰 시 코드를 설명하고 이해하기 위해 의사소통 비용이 증가하며 피로도가 증가할 것이다.
UI와 비즈니스 로직이 강하게 결합되면 어떤 문제가 발생할까?
- 새로운 기능을 추가하거나 수정할 때 다른 코드에 영향을 주어 유지보수에 어려움이 발생한다.
- 단위 테스트에 어려움이 발생한다.
위와 같은 문제를 해결하기 위해 관심사 분리(Separation of Concerns, SoC)를 통해 일관된 코드 구조를 만들고 개발 효율성을 높이고자 했는데, 잘 알려진 아키텍쳐 패턴이 아래에서 설명할 MVC, MVVM 패턴이다.
MVC란?
MVC는 관심사(Concern)를 Model, View, Controller로 분리하여 UI와 비즈니스 로직을 독립적으로 구성하는 아키텍처 패턴이다.
Model은 데이터와 비즈니스 로직을 담당하고 UI(View)와 독립적이다.
View는 사용자에게 보여지는 화면을 담당한다.
Controller는 Model과 View 사이에서 중개자 역할을 담당한다.
MVC의 목적
MVC는 Model, View, Controller로 나누어진 구조를 통해 관심사를 분리하고 UI와 비즈니스 로직을 분리해 테스트할 수 있으며, 기능 확장과 코드 수정이 용이한 구조를 만들고, Model의 재사용성을 높여 유지보수성을 향상시키는 것이 목적이라고 할 수 있다.
iOS Cocoa MVC의 단점
애플에선 MVC를 기반으로 Cocoa Application 을 디자인하였는데, 이를 Cocoa MVC라고 부른다.
UIKit을 사용하는 iOS 에서의 Cocoa MVC 구조는 View와 Controller가 결합되어 있는 ViewController 로 인해
Massive View Controller(약자가 MVC라서) 라고도 불리는 Fat ViewController 문제가 발생한다.
ViewController는 View와 Controller의 책임을 모두 가질 수 있는데,
구체적으로 Model과 View간의 데이터를 전달 및 갱신하고 사용자 입력을 처리하는 Controller의 책임과
View의 생명주기에 관여하고 UI를 갱신하는 등의 View의 책임을 동시에 가질 수 있다.
이로 인해 ViewController의 코드가 비대해지고 로직을 테스트하기 어렵게 만든다.
그리고 비단 Cocoa MVC만의 문제는 아닐 수 있지만, View와 Model에 속하지 않는 코드(데이터 변환, 일부 비즈니스 로직 등)를 Controller가 책임지게 되면 SRP(단일 책임 원칙)에 어긋난다는 문제도 발생할 수 있다.
하지만 위 문제들을 해결할 수 없는 것은 아니다.
View의 책임을 덜기 위해서 Custom View를 만들고 UI를 갱신하는 등의 행위는 해당 View에게 시킬 수 있고,
테스트 또한 불편한 것이지 불가능한 것은 아니다.
// ViewController 내부에서 View를 생성하고 layout을 설정하고 있음
class ProfileViewController: UIViewController {
private let nameLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
return label
}()
private let fetchButton: UIButton = {
let button = UIButton()
button.setTitle("Username", for: .normal)
button.backgroundColor = .systemBlue
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupUI()
}
private func setupUI() {
view.addSubview(nameLabel)
view.addSubview(fetchButton)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
fetchButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
nameLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
nameLabel.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -50),
fetchButton.centerXAnchor.constraint(equalTo: centerXAnchor),
fetchButton.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 20)
])
}
}
import UIKit
// View의 책임을 Custom View로 분리
class ProfileViewController: UIViewController {
private let profileView = ProfileView()
override func loadView() {
self.view = profileView // 뷰를 ProfileView로 교체
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// 버튼 액션 연결
profileView.fetchButton.addTarget(self, action: #selector(fetchUsernameTapped), for: .touchUpInside)
}
@objc private func fetchUsernameTapped() {
profileView.nameLabel.text = "Youngkyu"
}
}
class ProfileView: UIView {
let nameLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
return label
}()
let fetchButton: UIButton = {
let button = UIButton()
button.setTitle("Username", for: .normal)
button.backgroundColor = .systemBlue
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
}
private func setupUI() {
addSubview(nameLabel)
addSubview(fetchButton)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
fetchButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
nameLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
nameLabel.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -50),
fetchButton.centerXAnchor.constraint(equalTo: centerXAnchor),
fetchButton.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 20)
])
}
}
MVVM이란?
MVVM은 MVP 패턴의 변형으로, UI와 로직을 분리하며 데이터 바인딩을 활용해 UI 업데이트를 쉽게 하고 테스트 용이성을 높인다.
MVP 패턴은 간단히 설명하자면 View와 Model을 Presenter를 통해 연결한다.
*Cocoa MVC의 Controller와 역할이 비슷하다.
ViewModel은 뷰 로직을 처리하고, Model로부터 데이터를 가져온 다음 뷰에서 데이터를 쉽게 사용할 수 있도록 가공하는 역할을 담당한다.
MVVM의 탄생
2004년, 마틴 파울러는 Presentation Model(PM) 패턴에 관한 글을 발표했다.
PM 패턴은 MVP 패턴과 유사하게 View의 동작 및 상태를 분리한다.
그리고 View의 추상화인 Presentation Model을 만든다.
그러면 View는 Presentation Model을 렌더링하는 역할만 수행하고,
Presentation Model 클래스 내부에 코드를 통해 뷰를 자주 업데이트하는 구조가 된다.
2005년, 마이크로소프트의 WPF, Silverlight 아키텍트인 존 구스만(John Gossman)에 의해 MVVM이 공개되었다.
MVVM 패턴은 파울러의 PM 패턴과 동일한 개념을 가진다.
두 패턴 모두 View의 추상화(PM, ViewModel)가 View의 동작과 상태를 포함한다.
다만, Presentation Model이 플랫폼으로부터 독립적인 View 추상화를 만드는 것을 목표로 했다면,
MVVM은 WPF의 핵심 기능을 활용해 UI를 더 쉽게 만들기 위한 표준화된 방법으로 등장했다.
*WPF의 핵심 기능 = 데이터 바인딩을 말하는 듯
ViewModel과 Presenter의 차이점
MVP의 Presenter는 View를 직접 참조하며, View와 Model 간의 데이터를 연결한다.
이와 다르게 MVVM의 ViewModel은 View를 참조하지 않는다.
View는 ViewModel의 프로퍼티에 데이터 바인딩을 수행하며, ViewModel은 Model의 데이터를 제공하고 뷰 로직을 관리한다.
MVVM의 장점
데이터 바인딩을 통해 UI 업데이트를 직접 관리하지 않게 만든단 장점이 있다.
그리고 View, ViewModel, Model 간의 느슨한 결합을 장점으로 들 수 있다.
View는 Model을 모르며, 반대로 ViewModel과 Model은 View를 알지 못한다.
느슨한 결합은 유지보수성 증가, 재사용성 증가, 테스트에 용이하단 장점을 가진다.
Model은 ViewModel과 View로부터 완전히 독립적이기에 비즈니스 로직을 수정할 때 UI를 고려할 필요가 없어지고,
UI가 변경되더라도 Model을 수정하지 않아도 되기 때문에 유지보수성이 높아진다.
또한 UIKit, SwiftUI, AppKit 과 같이 UI 프레임워크가 변경되어도 Model을 재사용할 수 있다.
UI에 의존하지 않기 때문에 단위 테스트 코드 작성도 쉬워진다.
Clean Architecture란
클린 아키텍쳐(Clean Architecture)란 로버트 C. 마틴이 제안한 소프트웨어 설계 원칙 중 하나로
좋은 아키텍쳐를 설계하기 위한 아키텍쳐 설계 방법론이라고 할 수 있다.
소프트웨어 시스템을 구성하는 요소들을 링 형태의 계층으로 구분하고, 외부 링에서 내부 링 방향으로만 의존성이 흐른다는 특징이 있다.
좋은 아키텍쳐란
좋은 아키텍쳐란 그 안에 담긴 소프트웨어 시스템이 쉽게 개발, 배포, 운영, 유지보수될 수 있는 구조를 말한다.
이런 구조가 되려면 세부사항에 대한 결정을 가장 마지막으로 미룰 수 있어야 하고, 세부사항이 정책과 무관해야 한다.
정책과 세부사항은 무엇을 말하는 걸까?
정책은 시스템의 핵심 요소로 모든 업무 규칙과 절차를 구체화한 것이다.
세부사항이란 입출력 장치, DB, 웹시스템, 서버, 프레임워크, 프로토콜 등 우리가 개발하며 고려하게 되는 세부적인 것들을 말한다.
소프트웨어를 부드럽게 유지하려면 세부사항에 몰두하지 않고, 선택사항을 가능한 많이 오랫동안 열어두어야 한다.
정리하자면 구체적인 것에 의존하지 않도록 해 유연하게 개발할 수 있도록 하는 설계를 말한다.
Clean Architecture의 목표
클린 아키텍쳐의 목표는 '계층 분리를 통한 관심사의 분리' 이다.
계층 분리를 위해 의존성 규칙을 정한다.
의존성 규칙 Dependency Rule
바깥으로 갈수록 저수준의 세부사항이며, 안으로 갈수록 고수준의 정책이 된다.
외부에 위치한 것이 내부에 영향을 주지 않도록 해 세부사항이 정책에 영향을 주지 않도록 한다.
내부에 속한 것은 외부에 대해 아무것도 알지 못한다. (함수, 클래스, 변수 등 모두)
요소들에 대한 설명
각 요소들에 대해 잘 설명된 글이 있어 인용하였습니다.
출처: https://dailyheumsi.tistory.com/239
Entity(엔티티)
- 엔티티는 전사적인 핵심 업무 규칙을 캡슐화하며 메소드를 갖는 객체 혹은 데이터와 함수의 집합일 수도 있음
- 다양한 애플리케이션에서 재사용만 가능하다면 형태는 중요하지 않음
- 외부의 무언가가 변경되더라도 엔티티가 변경될 가능성은 지극히 낮음
- 운영 관점에서 특정 애플리케이션에 무언가 변경이 필요하더라도 엔티티에는 절대로 영향을 주면 안됨
- ex) 엔티티는 핵심 비즈니스 로직을 다루므로, UI 단의 페이징 처리 등이 필요해도 변경이 일어나서는 안됨
유스케이스(UseCase)
- 유스케이스는 애플리케이션에 특화된 업무 규칙을 포함하며, 시스템의 모든 유스케이스를 캡슐화하고 구현함
- 유스케이스는 엔티티로 들어오고 나가는 데이터 흐름을 조정함
- 또한 엔티티가 자신의 핵심 업무 규칙을 사용해서 유스케이스의 목적을 달성하도록 이끔
- 유스케이스의 변경이 엔티티에 영향을 주면 안되며, 외부 요소의 변경이 이 계층에 영향을 주는 것도 안됨
- 하지만 운영 관점에서 애플리케이션이 변경된다면 유스케이스가 영향을 받고, 유스케이스 세부사항이 변하면 일부 코드는 영향을 받음
인터페이스 어댑터(Interface Adapter)
- 프레젠터나 컨트롤러 등과 같은 어댑터들로 구성되며, 컨트롤러에서 유스케이스로 전달된 요청은 다시 컨트롤러로 되돌아 감
- 어댑터는 유스케이스와 엔티티에 맞는 데이터에서 DB나 웹 등과 같은 외부 요소에 맞는 데이터로 변환함
- 또한 데이터를 외부 서비스에 맞는 형식에서 유스케이스나 엔티티에서 사용되는 내부적인 형식으로 변환하는 또 다른 어댑터가 필요함
- 어댑터에서 객체를 변환하는 이유는 어댑터가 특정 기술에 종속되며 때문임
- 만약 유스케이스 계층에서 변환을 한다면 유스케이스가 세부 기술에 의존하게 됨
iOS 에서 클린 아키텍쳐를 사용했던 경험을 생각해보면, UseCase의 결과가 프레젠테이션 레이어에서 원하는 형태와 다른 경우가 존재했다. 이런 경우 ViewModel과 같은 곳에서 도메인 수준의 데이터를 프레젠테이션 수준의 데이터로 변환했는데, ViewModel이 인터페이스 어댑터에 속한다고 볼 수 있겠다.
프레임워크와 드라이버(Framework and Driver)
- 가장 바깥쪽 계층은 데이터베이스나 웹 프레임워크 같은 것들로 구성됨
- 이 계층에서는 안쪽 원과 통신하기 위한 접합 코드 외에는 특별히 더 작성할 게 없음
- 모든 세부사항이 위치하는 곳으로, 웹과 데이터베이스도 세부사항이므로 이를 외부에 위치시켜 피해를 최소화함
Layer는 꼭 4개여야 하는가
꼭 그렇지만은 않다. 가장 바깥은 세부적인 사항이며 안쪽으로 갈수록 고수준의 정책에 가까워진다는 의존성 규칙만 잘 지킨다면 추가적인 레이어(링)를 추가해도 된다.
각 시스템마다 특징이 있는 것이고, 자신의 시스템에서 유지보수에 불편함을 겪는다면 레이어를 추가하거나 제거할 수 있다.
그러나 레이어 간 경계를 구현하는 비용보다 레이어를 나누지 않았을 때 드는 비용이 비싼 경우에 나누는 것이 좋다.
이해하기 쉽게 비유하자면, [ 모듈화를 해서 얻는 이익 > 모듈화를 하지 않았을 때 얻는 이익 ] 일 때 하라는 뜻이다.
참고 자료
https://ko.wikipedia.org/wiki/소프트웨어_구조
https://developer.mozilla.org/ko/docs/Glossary/MVC
https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html
https://woozzang.tistory.com/89
https://eunjin3786.tistory.com/31
https://learn.microsoft.com/en-us/previous-versions/msp-n-p/hh848246(v%3dpandp.10)?ranMID=24542&ranEAID=je6NUbpObpQ&ranSiteID=je6NUbpObpQ-P87ml7WHP9EPR9QcEFV8tQ&epi=je6NUbpObpQ-P87ml7WHP9EPR9QcEFV8tQ&irgwc=1&OCID=AID681541_aff_7593_1243925&tduid=(ir__b6c1wtlvrixh2y0wyefjvj9obu2xmyln3wdncoz000)(7593)(1243925)(je6NUbpObpQ-P87ml7WHP9EPR9QcEFV8tQ)()&irclickid=_b6c1wtlvrixh2y0wyefjvj9obu2xmyln3wdncoz000
https://ko.wikipedia.org/wiki/모델-뷰-뷰모델
https://learn.microsoft.com/en-us/archive/msdn-magazine/2009/february/patterns-wpf-apps-with-the-model-view-viewmodel-design-pattern
https://learn.microsoft.com/en-us/archive/msdn-magazine/2006/august/design-patterns-model-view-presenter
https://dailyheumsi.tistory.com/239
https://mangkyu.tistory.com/276
https://www.techtarget.com/whatis/definition/clean-architecture
https://proandroiddev.com/clean-architecture-data-flow-dependency-rule-615ffdd79e29
'iOS' 카테고리의 다른 글
nohup 명령어를 사용해도 서버가 꺼지는 문제 (2) | 2024.11.08 |
---|---|
Tuist 없이 모듈 만들기 with DemoApp (0) | 2024.11.07 |
네이버 클라우드 VPC 서버에 연결이 되지 않는 문제 (0) | 2024.11.03 |
Swift Concurrency - Sendable (2) | 2024.10.25 |
Swift Concurrency - Task (1) | 2024.10.24 |