-Today's Learning Content-

  • Swift 아키텍처 패턴
  • Combine

1. Swift 아키텍처 패턴

개념 정리

Architecture Patterns이란 앱을 설계할 때 코드의 구조와 흐름을 체계적으로 정리하기 위한 방식이다. 아키텍처 패턴을 통해 유지보수성과 확장성을 높이고, 모듈화를 촉진하며, 코드의 재사용성을 극대화할 수 있다.

1) MVC(Model-View-Controller)

MVC 아키텍처 패턴은 Model, View, Controller 로 구성되어 있다. 간단하고 익숙하며, iOS SDK에서도 기본으로 사용하는 아키텍처로, 주로 소규모 프로젝트에 적합하다.
MVC 아키텍처
각 구성요소가 맡는 역할은 아래와 같다.

  1. Model
    • 데이터와 비즈니스 로직을 담당
    • 데이터베이스에서 데이터를 가져오거나 네트워크 요청 결과를 처리
    • ex) User, Product 같은 모델 클래스
  2. View
    • UI 구성 요소를 포함하며 사용자와 상호작용
    • 데이터를 표시하거나 사용자 입력을 받음
    • ex) Storyboard, UIKit의 UIButton, UILabel 등
  3. Controller
    • Model과 View 사이를 중재
    • Model에서 데이터를 받아 View에 전달하거나 사용자의 입력을 Model로 전달.
    • ex) UIViewController

MVC 아키텍처 패턴은 구조가 간단하고 쉽게 사용할 수 있지만, ViewController가 비대해지고 역할이 중대해지는 문제가 발생한다. 이 때문에 사람들이 MVC(Massive ViewController) 라고 부르기도 했다고 한다...

게다가 각 요소들끼리의 의존성이 높아지기 때문에 테스트가 어렵고 유지보수가 어려워 아키텍처를 사용하는 목적과 멀어지기 때문에 작고 간단한 프로젝트가 아니라면 사용하기 어려운 아키텍처 패턴이다.

2) MVVM(Model-View-ViewModel)

MVVM 아키텍처 패턴은 Model, View, ViewModel로 구성되어 있다. 반응형 프로그래밍이 가능하며, 테스트 가능성이 높아지고, MVC와 달리 ViewController의 역할이 단순화되어 더 작고 관리하기 쉽다(유지보수 용이)

각 구성요소의 역할은 아래와 같다

  1. Model
    • MVC와 동일하게 데이터와 비즈니스 로직을 처리
    • API 호출, 데이터베이스 작업 등
  2. View
    • 사용자와 상호작용하며 UI를 표시
    • ViewModel과 데이터를 바인딩하여 변경 사항을 자동으로 업데이트
    • ex) SwiftUI, UIKit
  3. ViewModel
    • View에 필요한 데이터를 준비하고 가공
    • Model에서 데이터를 가져와 View에서 사용하기 적합한 형식으로 변환
    • View와 Model 간의 의존성을 제거하여 테스트 가능성을 높임

MVVM 아키텍처 패턴은 데이터 바인딩을 활용하여 UI 업데이트를 자동으로 할 수 있는데, 데이터 바인딩이란 Combine, RxSwift 등의 라이브러리를 사용하여 ViewModel이 데이터를 가공하고 가공된 데이터를 View가 관측하여 UI에 반영하는 것을 말한다.

또, View는 ViewModel의 값을 관측할 뿐 서로 독립적이기 때문에 ViewModel에서 UI에 영향을 주지 않고 테스트를 할 수 있게 된다.

MVVM은 MVC에 비해 아키텍처 패턴을 사용하는 목적에 더욱 가깝고 많은 장점을 지녔지만, 데이터 바인딩을 구현하려면 추가적인 라이브러리(Combine, RxSwift 등)가 필요할 수도 있다는 한계점과 작은 프로젝트에서 적용하면 오히려 코드가 복잡해질 수 있다는 문제점이 있다.

3) Clean Architecture

Clean Architecture는 의존성 역전 원칙(Dependency Inversion Principle)에 기반하며, 비즈니스 로직을 중심으로 계층을 나눠 의존성을 제어한다. 이 때, 내부 계층은 외부 계층에 의존하지 않는다는 원칙을 준수한다.

각 구성요소의 역할은 아래와 같다.

  1. Entity
    • 애플리케이션의 핵심 비즈니스 규칙과 데이터를 나타냄
    • ex) User, Product 등의 모델
  2. Use Case (Interactor)
    • 특정 비즈니스 로직을 처리하는 계층
    • 애플리케이션의 동작을 정의하며 Entity를 사용해 작업 수행
    • ex) 사용자의 로그인 로직
  3. Interface Adapters (Presenter, ViewModel)
    • Use Case와 UI 사이의 변환 작업
    • Use Case에서 데이터를 받아 UI가 사용할 형식으로 변환
    • ex) Presenter는 View에 데이터를 표시하고 ViewModel은 MVVM 스타일로 View와 데이터를 바인딩
  4. Frameworks & Drivers (View, Controller, Gateway)
    • 가장 바깥쪽 계층으로 구체적인 구현
    • UI, 네트워크 요청, 데이터베이스 접근과 같은 외부 의존성을 처리

클린아키텍처는 UI에서 사용자의 요청이 들어오면 Use Case로 전달되어 알맞은 비즈니스 로직을 실행한다. 그리고 그 결과 데이터를 Entity를 통해 조작한 후, Interface Adapter를 통해 UI로 전달하는 과정을 가진다.

클린아키텍처는 의존성 관리가 용이하고, UI와 비즈니스 로직이 독립적이기 때문에 개별적으로 수정이 가능하여 유지보수성이 좋다.
또, 새로운 기능을 추가하더라도 다른 계층에 주는 영향이 적기 때문에 확장성이 좋다.
MVVM처럼 UI와 비즈니스 로직이 분리되어 있기 때문에 테스트를 하는 것도 용이하다.

하지만, 클린아키텍처는 초기 설계와 구현이 복잡하고, 작은 프로젝트에서는 너무 과한 구조이기 때문에 적절한 판단이 필요하다.
그리고 각 계층을 구현하기 위해서 코딩 작업이 많아지기 때문에 작업 시간이 오래 걸린다는 단점이 있다.

4) 아키텍처 패턴 비교

특징MVCMVVMClean Architecture
구조의 단순성쉬움중간복잡함
ViewController비대해질 가능성 높음단순화됨완전한 역할 분리
테스트 용이성낮음높음매우 높음
유지보수성작은 프로젝트에서 용이중간대규모 프로젝트에 특출
데이터 바인딩없음Combine, RxSwift 활용의존성 없음(계층적 설계로 분리 가능)

2. Combine

개념 정리

Combine은 비동기 이벤트 처리와 데이터 스트림을 처리하기 위한 프레임워크이다. RxSwift와 유사하지만, Apple` 생태계에 최적화되어 있고, Swift 표준 문법과 잘 통합되어 있어 더욱 사용이 용이하다.

1) Combine의 주요기능

CombinePublisherSubscriber 등으로 데이터 바인딩을 구현할 수 있는데, 주요 기능들은 아래와 같다.

  1. Publisher (퍼블리셔)
    • 데이터 스트림의 소스 역할
    • 데이터를 발행하고, 이를 구독(subscribe)한 구독자에게 이벤트 전달
    • ex) URLSession, NotificationCenter, Timer
  2. Subscriber (구독자)
    • 퍼블리셔로부터 방출된 값을 수신하고 처리
    • 퍼블리셔와 연결되며 이벤트를 처리하는 끝단
    • Combine에서 제공하는 sink, assign로 구현
  3. Operator (연산자)
    • 퍼블리셔에서 전달된 데이터 스트림을 변환하거나 필터링하는 메소드들
    • 체인 형태로 사용되며 데이터 처리를 선언적으로 표현 가능
  4. Cancellable (구독 취소)
    • 퍼블리셔와 구독자 간의 연결을 나타내는 객체
    • 메모리 누수를 방지하기 위해 필요할 때 구독을 취소 가능

2) Combine 활용 예제

Combine의 주요 기능을 활용하면 데이터 바인딩을 통해 MVVM 아키텍처 패턴처럼 사용할 수 있다.
먼저 유저의 입력을 받고 화면의 출력을 담당할 View를 생성해준다.

import UIKit
import Combine

class ViewController: UIViewController {
    
    private let countingLabel: UILabel = UILabel()
    
    private let button: UIButton = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        setupLablel()
        setupButton()
    }

    private func setupLablel() {
        self.countingLabel.text = "0"
        self.countingLabel.textAlignment = .center
        self.countingLabel.backgroundColor = UIColor.orange
        self.countingLabel.textColor = UIColor.white
        self.countingLabel.font = UIFont.systemFont(ofSize: 50, weight: .bold)
        self.countingLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(countingLabel)
        
        NSLayoutConstraint.activate([
            self.countingLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            self.countingLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            self.countingLabel.widthAnchor.constraint(equalToConstant: 100)
        ])
    }
    
    private func setupButton() {
        self.button.setTitle("Up Count", for: .normal)
        self.button.titleLabel?.font = UIFont.systemFont(ofSize: 20, weight: .black)
        self.button.backgroundColor = UIColor.blue
        self.button.layer.cornerRadius = 20
        self.button.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button)
        
        NSLayoutConstraint.activate([
            self.button.topAnchor.constraint(equalTo: countingLabel.bottomAnchor, constant: 50),
            self.button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            self.button.widthAnchor.constraint(equalToConstant: 150),
            self.button.heightAnchor.constraint(equalToConstant: 80)
        ])
    }
}

이제 위에서 만든 View가 데이터를 바인딩할 ViewModel을 만들어준다.

import UIKit
import Combine

class CountingViewModel: ObservableObject {
    
    @Published var count: Int = 0
    
}

ObservableObject@Published로 선언한 객체가 변경되었을 때 이벤트를 발생시키기 위해 필요한 프로토콜이다.
이제 기본적인 세팅이 끝났으니 제대로 데이터 바인딩을 진행해보자

먼저 CountingViewModel에 새로운 메소드를 추가한다.

func countUp() {
	self.count += 1
}

메소드가 실행되면 내부 프로퍼티인 count의 값을 변경하는 메소드이다.

그리고 CountingViewModel 클래스를 타입으로 가지는 프로퍼티를 ViewController에 생성해준다.

private var countBinding: CountingViewModel = CountingViewModel()

다음으로는 버튼 액션 메소드를 추가하고 이를 버튼에 적용시키는 것이다.

self.button.addTarget(self, action: #selector(buttonAction), for: .touchDown)

// 버튼을 누르면 CountingViewModel의 카운트가 1 증가
@objc private func buttonAction() {
	self.bindingCount.countUp()
}

버튼액션에서는 위에서 만든 CountingViewModel타입의 인스턴스 메소드를 실행시키도록 한다. 이렇게 하면 사용자가 이벤트를 발생(input) 시켰을 때, ViewController에서는 이벤트가 발생했다는 것만 CountingViewModel에 알리는 것이 된다.

이제 데이터 바인딩을 위한 작업을 진행한다.
먼저 Subscriber를 생성해준다.

private var subscriber = Set<AnyCancellable>()

subscriber는 구독을 관리하기 위해 사용하는 구독 취소 관리 컬렉션이다.

Combine에서 퍼블리셔와 구독자를 연결하면 이 연결을 표현하는 Cancellable 객체가 생성되는데, 이 객체를 통해 구독을 취소하거나 메모리를 관리할 수 있게 된다.
Combine에서는 퍼블리셔와 구독자가 연결되었을 때, 구독 관계는 명시적으로 관리되지 않으면 계속 유지된다. 만약 구독 관리를 하지 않으면 메모리 누수나 불필요한 작업 실행 문제가 발생할 수 있기 때문에 반드시 지정해 주어야 한다.

이제 구독자를 관리하는 컬렉션까지 이용하여 ViewController에서 값의 변화를 감지하고 화면에 표현하도록 메소드를 구현해준다.

func setupBinding() {
	self.bindingCount.$count.sink { upCount in
		print(upCount)
		self.countingLabel.text = "\(upCount)"
	}
}.store(in: &subscriber)

이렇게 하면 버튼을 눌렀을 때, CountingViewModel의 인스턴스 메소드가 실행되고, count의 값이 변한다. 그리고 이를 퍼블리셔가 감지하여 이벤트를 발생시키면 sink를 통해 ViewController에 값을 전달하고, 레이블의 값을 업데이트하게 된다. 그리고 이 때 생성된 구독자 객체는 store 키워드를 통해 subscriber 컬렉션에 저장된다.
이후 화면에는 변경된 레이블 값이 적용되어 무사히 출력되는 모습을 확인할 수 있다.

구현 결과

이처럼 Combine은 데이터 바인딩을 통해 MVVM 아키텍처 패턴을 구현할 수 있다. 이번엔 간단한 예제로 비슷하게 흉내를 냈을 뿐이지만, 프로젝트가 커진다면 유용히 사용할 수 있는 기능이다.


-Today's Lesson Review-

오늘은 Swift의 대표적인 아키텍처 패턴에 대해 학습하고,
Combine 프레임워크를 사용하여 간단하게 MVVM 패턴을 실습해보았다.
아직은 어렵고 낯선 개념이지만, 큰 프로젝트에서는 반드시 활용해야 하는 것이 아키텍처 패턴이기 때문에
앞으로도 많은 공부를 해야겠다고 느꼈다.
profile
이유있는 코드를 쓰자!!

3개의 댓글

comment-user-thumbnail
2024년 11월 18일

난 오늘도 이렇게 상경님 블로그로 마무리 공부를 한다...

2개의 답글