클린 아키텍처를 적용하면서, 가장 저수준 객체인 View에서 TextField를 통해 입력된 텍스트 데이터를
고수준 객체인 Interactor에 강결합 없이 전달하는 방법에 대해 고민했습니다.
이번 글에서는 이 문제를 해결하기 위해 제가 고민했던 여러 가지 방법과, 최종적으로 선택한 방법에 대해 정리해 보려고 합니다.
데이터의 흐름
현재 데이터의 흐름은 View → ViewController → Interactor 순으로 진행되고 있습니다.
이를 기반으로 조사한 결과, 총 3가지 방법을 생각해 볼 수 있었습니다.
오늘은 그중 제가 선택한 방법에 대해 설명해 보겠습니다.
첫 번째 방법: 직관적인 접근법
이 방법은 제가 실제로 프로젝트에 적용한 방법입니다.
장점
구현 난이도가 낮고 코드가 직관적입니다.
계층 간 데이터 흐름이 명확합니다.
단점
View와 ViewController 간의 강결합이 발생합니다.
큰 프로젝트에서는 유지보수에 어려움이 발생합니다.
구체적인 방식
1. View에 있는 TextField의 text 속성을 public으로 설정합니다.
2. 데이터 흐름이 발생할 때 ViewController에서 View의 text 속성에 접근합니다.
3. 이를 통해 데이터를 Interactor에 전달합니다.
선택한 이유
이 방법은 View와 ViewController의 강결합이라는 단점이 있지만, 현재 프로젝트에서는 적절하다고 판단했습니다.
그 이유는 다음과 같습니다
1. 현재 사용하는 클린 아키텍처에서는 View와 ViewController가 동일한 UI 레이어에 속합니다.
2. View가 재사용될 경우에도 ViewController와 함께 활용된다고 보았습니다.
결론적으로, 단점이 존재하지만 현재 상황에서는 이 방법이 가장 간단하고 실용적인 선택이라고 판단했습니다.
예제코드
TextView는 사용자로부터 데이터를 입력받고 UI를 관리합니다.
// TextView.swift
import UIKit
protocol TextViewDelegate: AnyObject {
func didTapSaveButton()
}
final class TextView: UIView {
weak var delegate: TextViewDelegate?
private let titleLabel: UILabel = {
let label = UILabel()
label.text = "Title"
return label
}()
private let titleTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter Title"
textField.borderStyle = .roundedRect
return textField
}()
private let descriptionLabel: UILabel = {
let label = UILabel()
label.text = "Description"
return label
}()
private let descriptionTextView: UITextView = {
let textView = UITextView()
textView.layer.borderWidth = 1
textView.layer.borderColor = UIColor.lightGray.cgColor
textView.layer.cornerRadius = 8
return textView
}()
private lazy var saveButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Save", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .blue
button.layer.cornerRadius = 8
button.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
addSubview(titleLabel)
addSubview(titleTextField)
addSubview(descriptionLabel)
addSubview(descriptionTextView)
addSubview(saveButton)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleTextField.translatesAutoresizingMaskIntoConstraints = false
descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
descriptionTextView.translatesAutoresizingMaskIntoConstraints = false
saveButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
titleTextField.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
titleTextField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
titleTextField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
descriptionLabel.topAnchor.constraint(equalTo: titleTextField.bottomAnchor, constant: 20),
descriptionLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
descriptionTextView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 10),
descriptionTextView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
descriptionTextView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
descriptionTextView.heightAnchor.constraint(equalToConstant: 100),
saveButton.topAnchor.constraint(equalTo: descriptionTextView.bottomAnchor, constant: 20),
saveButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
saveButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
saveButton.heightAnchor.constraint(equalToConstant: 50)
])
}
@objc private func saveButtonTapped() {
delegate?.didTapSaveButton()
}
var titleText: String {
return titleTextField.text ?? ""
}
var descriptionText: String {
return descriptionTextView.text
}
func setTitle(_ title: String) {
titleTextField.text = title
}
func setDescription(_ description: String) {
descriptionTextView.text = description
}
}
코드 설명:
- saveButtonTapped는 사용자가 Save 버튼을 눌렀을 때 호출됩니다. 이 메서드는 delegate를 통해 ViewController로 이벤트를 전달합니다.
- titleText와 descriptionText는 View에서 현재 입력된 텍스트를 ViewController가 가져갈 수 있도록 제공합니다.
TextViewController는 View와 Interactor를 연결하며, 데이터의 흐름을 조정합니다.
// TextViewController.swift
import UIKit
final class TextViewController: UIViewController {
private let contentView = TextView()
private let interactor: TextInteractor
init(interactor: TextInteractor) {
self.interactor = interactor
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
view = contentView
}
override func viewDidLoad() {
super.viewDidLoad()
contentView.delegate = self
interactor.loadTextData()
}
}
extension TextViewController: TextViewDelegate {
func didTapSaveButton() {
let title = contentView.titleText
let description = contentView.descriptionText
interactor.updateTitle(title)
interactor.updateDescription(description)
interactor.saveTextData()
}
}
extension TextViewController: TextInteractorDelegate {
func didLoadTextData(title: String, description: String) {
contentView.setTitle(title)
contentView.setDescription(description)
}
}
코드 설명:
- didTapSaveButton은 TextView에서 전달받은 이벤트를 처리하고, 데이터를 Interactor에 전달합니다.
- didLoadTextData는 Interactor로부터 전달받은 데이터를 View에 설정합니다.
Interactor는 텍스트 데이터를 관리하며, 데이터를 업데이트하거나 저장하는 역할을 담당합니다.
// TextInteractor.swift
import Foundation
protocol TextInteractorDelegate: AnyObject {
func didLoadTextData(title: String, description: String)
}
protocol TextInteractor {
func updateTitle(_ title: String)
func updateDescription(_ description: String)
func loadTextData()
func saveTextData()
}
final class TextInteractorImpl: TextInteractor {
private var title: String = ""
private var description: String = ""
weak var delegate: TextInteractorDelegate?
func updateTitle(_ title: String) {
self.title = title
}
func updateDescription(_ description: String) {
self.description = description
}
func loadTextData() {
delegate?.didLoadTextData(title: title, description: description)
}
func saveTextData() {
print("Saving data...")
print("Title: \(title)")
print("Description: \(description)")
}
}
코드 설명:
- updateTitle과 updateDescription 메서드는 ViewController로부터 전달받은 데이터를 내부 상태로 저장합니다.
- loadTextData는 저장된 데이터를 delegate를 통해 ViewController로 전달합니다.
- saveTextData는 데이터를 저장하는 로직을 담당하며, 현재는 콘솔에 출력하는 방식으로 구현되어 있습니다
- Interactor에서 데이터 상태를 관리하면서 데이터의 일관성을 보장할 수 있습니다.
- 도메인레이어인 Interactor에서 데이터의 처리, UI레이어인 화면만 책임을 지기 때문에 좀 더 명확한 책임 분리가 일어납니다.
마무리
오늘은 클린 아키텍처를 설계하며 발생한 View → ViewController → Interactor의 데이터 흐름을 어떻게 설계하고 활용했는지 정리해 보았습니다.
비록 View와 ViewController 간에 강결합이라는 단점이 있지만,
현재 프로젝트에서는 두 계층이 동일한 UI 레이어에 속하며, 이 강결합이 큰 문제가 되지 않을 것이라 판단했습니다.
처음 클린 아키텍처를 설계하고 활용하다 보니, 직관적인 데이터 흐름이 구현 난이도가 낮고 현재 상황에 적합하다고 판단하여 이 방법을 선택하게 되었습니다.
다음 게시물에서는 delegate 패턴을 활용해 강결합을 완화하는 방법과, ViewModel을 활용하여 데이터 흐름을 개선하는 방법에 대해 정리해 보겠습니다.
더 나은 구조를 고민하며 개선해 나가는 과정을 공유할 예정이니 기대해 주세요!
'Swift' 카테고리의 다른 글
방치된 앱을 리팩토링해 앱스토어에 출시하기까지 (0) | 2025.02.26 |
---|---|
클린아키텍처 적용해보기 in iOS - 2 (0) | 2024.08.27 |
Swift - Closures (0) | 2024.08.18 |
MVC패턴과 MVVM 패턴에 대한 나의 생각 (0) | 2024.07.31 |
F-Lab iOS 수료 후기 (3) | 2024.07.27 |