Search

만들면서 느껴보는 POP

“let us: Go! 2022 여름 - 만들면서 느껴보는 POP” 영상을 바탕으로 정리를 진행했습니다. 영상을 보고 싶으신 분들은 하단 링크를 참고해주세요.

 들어가며

만약, 우리가 이런 말을 들었을 때 가능하다는 생각이 드나요?
내일 완공 전에 화장실 하나 없애 주세요.
그렇다면, 이건 어떤가요?
내일 배포 전에 메뉴 기능 하나 없애 주세요.
위의 화장실은 절대 가능하지 않지만, 메뉴 기능을 없애는 건 어떻게 하면 가능할거 같습니다.
이게 바로, 하드웨어와 소프트웨어의 차이입니다.
하드웨어는 처음부터 완벽하게 만들어야 합니다. 다 만들고 난 후에 고칠 수 없거든요. 하지만, 소프트웨어는 나중에 고칠 수 있습니다.
“소프트웨어를 소프트웨어답게 만들려면 쉽게 변경할 수 있게 만들어야 합니다.”

 Protocol Oriented Programming

[ ] Oriented Programming 이라는 말은 무언가를 중요하게 우선으로 생각한다는 뜻을 가집니다.
우리가 잘 아는 Object Oriented Programming은 Object를 우선으로 중요하게 사용하겠다는 뜻입니다. 객체를 우선적으로 생각하겠다는 말입니다.
따라서, Protocol Oriented Programming은 Protocol를 우선으로 중요하게 생각하겠다는 뜻이 되겠네요.
Apple이 만들어내는 코드는 객체지향 프로그래밍의 철학에서 벗어나지 않습니다.
근데, 왜 Protocol Oriented Programming를 만들었을까?
왜 만들었는지를 알기 전에 Protocol이 하는 일에 대해서 먼저 알아보는게 좋을 것 같습니다.

Protocol

1.
Delegate 만들기
Protocol를 사용하면 Delegate를 만들 수 있습니다.
protocol JsonFetcherDelegate: AnyObject { func onFetched(json: String) }
Swift
복사
2.
구현을 강제 시킴
Protocol를 적용하면 구현을 강제합니다.
class Main: JsonFetcherDelegate { }
Swift
복사
Main이라는 클래스에 JsonFetcherDelegate를 채택했을 때, 코드를 저렇게 두면 컴파일 에러가 발생합니다.
준수한 Protocol이 가지는 요구 사항들은 꼭 구현을 해줘야 하기 때문이죠.
3.
기본 구현을 넣을 수 있음
Protocol에서는 extension으로 기본 구현을 추가할 수 있습니다. 기본 구현은 해당 Protocol를 준수하는 타입이 해당 요구 사항을 필수적으로 구현하지 않더라도 사용할 수 있게 합니다.
protocol TypePresentable { func typeName() -> String } extension TypePresentable { func typeName() -> String { return "\(type(of: self))" } }
Swift
복사
위에서 만든 TypePresentable 프로토콜을 Int, String 클래스가 준수하도록 만들었습니다.
extension Int: TypePresentable { } extension String: TypePresentable { }
Swift
복사
TypePresentable 프로토콜 내부에 있는 typeName 함수를 구현하지 않아도 사용할 수 있습니다. 기본 구현이 되어있기 때문이죠.
print(42.typeName()) // Int print("Hello world".typeName()) // String
Swift
복사
4.
추상화
Protocol의 특징 중 하나가 바로 추상화 입니다.
Repository 라는 프로토콜을 만들고 구현체로 FileRepository와 MemoryRepository를 만들었습니다. 두 개의 구현체를 Repository로 추상화한 겁니다.
protocol Repository { func save(data: Data) func load() -> Data? } class MemoryRepository: Repository { private var data: Data? func save(data: Data) { self.data = data } func load() -> Data? { return data } } class FileRepository: Repository { private var filePath: URL? func save(data: Data) { try! data.write(to: filePath!) } func load() -> Data? { return try! Data(contentsOf: filePath!) } }
Swift
복사
5.
느슨한 결합
추상화를 이용하면 결합을 느슨하게 만들 수 있습니다.
ViewModel를 하나 만들고 ViewModel 내부에 Repository를 가지게 만들었습니다.
class ViewModel { private let repository: Repository init(repository: Repository) { self.repository = repository } }
Swift
복사
실제로 ViewModel에 넣는 값은 MemoryRepository지만 그냥 Repository를 받는다고 적어둔 겁니다.
let viewModel = ViewModel(repository: MemoryRepository())
Swift
복사
ViewModel은 MemoryRepository를 쓰지만 알고 있는건 Repository 타입으로 알고 있습니다.
MemoryRepository가 Repository를 구현하고 있기 때문에 화살표의 방향이 반대가 됩니다. 즉, 의존 방향이 서로 반대가 되어 버리는 겁니다. 이를 의존성 역전이라고 합니다.

 만들면서 느껴보기

Protocol이 기본적으로 하는 일이 뭔지는 알았습니다. 그러면 Todo 앱을 직접 만들면서 POP를 느껴봅시다.
우리는 만들면서 이 두가지 키워드를 함께 느낄 겁니다.
⓵ “있다 치고” ⓶ “알게 뭐야”
이게 바로 POP를 하는 방법입니다.
TodoViewController를 구성했습니다. TODO 주석 부분에 뭔가를 호출을 해야하는데, 우리는 저 곳에 뭘 넣어야 하는지 모릅니다.
class TodoViewController: UITableViewController { let service: TodoService override func viewDidLoad() { super.viewDidLoad() } @IBAction func onCreateTodo(title: String) { // TODO: - create TODO self.tableView.reloadData() } } extension TodoViewController { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // TODO: - get TODOs count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: TodoTableViewCell.identifier, for: indexPath) let index = indexPath.row let item = // TODO: - get TODO cell.todo = item return cell } }
Swift
복사
일단, “있다 치고” 호출을 해봅시다.
TodoService Protocol 안에 요구사항들을 넣어볼게요.
protocol TodoService { func create(title: String) func count() -> Int func item(at: Int) -> Todo }
Swift
복사
그리고 사용해봅시다.
class TodoViewController: UITableViewController { let service: TodoService override func viewDidLoad() { super.viewDidLoad() } @IBAction func onCreateTodo(title: String) { self.service.create(title: title) self.tableView.reloadData() } } extension TodoViewController { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.service.count() } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: TodoTableViewCell.identifier, for: indexPath) let index = indexPath.row let item = self.service.item(at: index) cell.todo = item return cell } }
Swift
복사
우리는 “여기에 이런게 있으면 좋을 것 같아!”라고 생각하면서 “있다 치고” 호출하면 됩니다.
그럼 이런 궁금증이 들겠죠?
저 프로토콜에 대한 구현체는 누가 만들죠?
누군가 만들겠죠? “알게 뭐야” 입니다. 프로토콜에 대한 구현체는 알 바가 아닙니다.
이번에는 TodoTableViewCell 내부를 보겠습니다.
class TodoTableViewCell: UITableViewCell { static let identifier = "TodoTableViewCell" @IBOutlet weak var isDone: UISwitch! @IBOutlet weak var itemTitle: UILabel! @IBOutlet weak var updatedAt: UILabel! var toggable: Toggable? override func awakeFromNib() { super.awakeFromNib() } @IBAction func onToggle(_ sender: Any) { // TODO: - toggle done } }
Swift
복사
토글에 대한 것을 Toggable이라는 프로토콜로 처리합니다. 이번에도 “있다 치고” 코드를 작성합니다.
Toggable Protocol 안에 요구사항들을 넣어볼게요.
protocol Toggable { func toggle(withId id: String) }
Swift
복사
그리고 사용해봅시다.
class TodoTableViewCell: UITableViewCell { static let identifier = "TodoTableViewCell" @IBOutlet weak var isDone: UISwitch! @IBOutlet weak var itemTitle: UILabel! @IBOutlet weak var updatedAt: UILabel! var toggable: Toggable? override func awakeFromNib() { super.awakeFromNib() } @IBAction func onToggle(_ sender: Any) { self.toggable?.toggle(withId: todo.id) } }
Swift
복사
이제 TodoServiceImpl를 본격적으로 구현해봅시다.
TodoServiceImpl는 TodoService와 Toggable를 준수합니다.
class TodoServiceImpl: TodoService { private var todoItems: [Todo] = [] func create(title: String) { let todo = Todo(id: UUID().uuidString, title: title, done: false, createAt: Date()) self.todoItems.append(todo) } func count() -> Int { return self.todoItems.count } func item(at index: Int) -> Todo { return self.todoItems[index] } } extension TodoServiceImpl: Toggable { func toggle(withId id: String) { if let found = self.todoItems.enumerated().first(where: { $0.element.id == id}) { let index = found.offset var todo = found.element todo.done = !todo.done self.todoItems[index] = todo } } }
Swift
복사
“알게 뭐야” 였던 TodoService와 Toggable를 TodoServiceImpl가 구현하게 되었습니다.
다시 ViewController로 돌아갈게요. 그리고 Service를 수정해주겠습니다.
TodoService 프로토콜과 Toggable 프로토콜을 모두 준수하는 값이 들어올 수 있도록 말이죠.
class TodoViewController: UITableViewController { typealias TodoToggableServie = TodoService & Toggable let service: TodoToggableServie override func viewDidLoad() { super.viewDidLoad() } @IBAction func onCreateTodo(title: String) { self.service.create(title: title) self.tableView.reloadData() } } extension TodoViewController { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.service.count() } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: TodoTableViewCell.identifier, for: indexPath) as? TodoTableViewCell else { return UITableViewCell() } let index = indexPath.row let item = self.service.item(at: index) cell.todo = item cell.toggable = service return cell } }
Swift
복사
ViewController는 service로 들어오는 값이 어떤 구현체인지 상관하지 않습니다. TodoService와 Toggable 프로토콜을 모두 준수하면 됩니다.
이번에는 TodoServiceImpl에 저장하는 기능을 만들어 보겠습니다.
현재는 저장할 공간이 없었기 때문에 런타임에만 저장한 Todo를 볼 수 있었습니다. 저장할 수 있는 Repository라는 인스턴스를 만들고 load와 save를 할 겁니다.
class TodoServiceImpl: TodoService { // MARK: - property private var todoItems: [Todo] = [] private let repository: Repository // MARK: - init init(repository: Repository) { self.repository = repository // TODO: - load todos } deinit { // TODO: - save todos } }
Swift
복사
일단, “있다 치고” 호출을 해봅시다. Repository Protocol 안에 요구사항들을 넣어볼게요.
protocol Repository { func load() -> [Todo] func save(todos: [Todo]) }
Swift
복사
이제, TodoServiceImpl에서 사용해보겠습니다.
class TodoServiceImpl: TodoService { // MARK: - property private var todoItems: [Todo] = [] private let repository: Repository // MARK: - init init(repository: Repository) { self.repository = repository self.todoItems = repository.load() } deinit { self.repository.save(todos: self.todoItems) } }
Swift
복사
우리는 Todo를 File에 저장할 수도 있고 서버에 저장할 수도 있습니다.
그중에서도 사용하기 쉬운 UserDefault에 저장해보겠습니다.
UserDefaultRepository 클래스에서 Repository 프로토콜을 준수하도록 합니다. 그리고 내부 코드를 구현해줄게요.
class UserDefaultRepository: Repository { private let TodoKey = "todos" func load() -> [Todo] { guard let json = UserDefaults.standard.string(forKey: TodoKey), let data = json.data(using: .utf8) else { return [] } return (try? JSONDecoder().decode([Todo].self, from: data)) ?? [] } func save(todos: [Todo]) { guard let data = try? JSONEncoder().encode(todos), let json = String(data: data, encoding: .utf8) else { return } UserDefaults.standard.set(json, forKey: TodoKey) } }
Swift
복사
우리가 모델로 사용하고 있는 Todo는 Encode, Decode를 해야하기 때문에 Codable를 준수해야 합니다. Codable를 준수하면 Decodable, Encodable에 대한 구현을 하지 않아도 내부에 있는 encode, decode 호출을 할 수 있습니다.
struct Todo: Identifiable, Codable { let id: String let title: String var done: Bool let createAt: Date }
Swift
복사
그렇다면 누가 Decodable, Encodable의 요구 사항을 구현해주나요?
“알게 뭐야” 입니다. “구현되어 있는걸 우린 지원하겠다”라고 정의만 해주면 됩니다.
여태까지 만들어 놓은걸 정리하면 이렇게 볼 수 있습니다.
1.
TodoViewController는 UITableViewController를 상속받고 있습니다.
2.
UITableViewController는 TodoTableViewCell를 사용하고 있습니다.
3.
TodoTableViewCell은 Toggable를, TodoViewController는 TodoService를 사용하고 있습니다.
4.
TodoServiceImpl은 TodoService와 Toggle를 준수하고 있습니다.
5.
TodoServiceImpl은 Repository를 사용해서 저장하고 있습니다.
6.
UserDefaultRepository는 Repository의 구현체입니다.

 하나만 더 해보자

Swinject라는 라이브러리를 추가했습니다. 이 라이브러리는 Container를 제공해줍니다.
AppDelegate에서 Swinject를 import하고 코드를 작성해줍시다.
class AppDelegate: UIResponder, UIApplicationDelegate { typealias TodoToggableService = Toggable & TodoService let container = Container() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { self.container.register(Repository.self) { _ in UserDefaultRepository() } self.container.register(TodoToggableService.self) { let repository = $0.resolve(Repository.self)! return TodoServiceImpl(repository: repository) } return true } }
Swift
복사
1.
Container에 Repository라는 타입으로 UserDefaultRepository를 register할 겁니다.
2.
Container에 TodoToggableService 타입으로 TodoServiceImpl를 register할 겁니다.
TodoServiceImpl에 필요한 Repository 타입의 구현체는 이전에 등록했기 때문에 등록해둔걸 꺼내서(resolve) 사용합니다.
그리고 AppDelegate에 있는 Container를 꺼내서 해당 Service 타입을 가져오는 API를 AppDelegate 외부에 작성해줍니다.
func Inject<Service>(_ serviceType: Service.Type) -> Service? { (UIApplication.shared.delegate as? AppDelegate)?.container.resolve(serviceType) }
Swift
복사
TodoViewController에서 Inject 함수를 사용해서 TodoToggableService 타입의 값을 달라고 합니다.
let service: TodoToggableServie = Inject(TodoToggableServie.self)!
Swift
복사
물론, 의존성 주입을 생성자 주입으로 사용하면 되는데, 그렇게 못한다면 Swinject를 써서라도 하면 좋습니다.
TodoViewController에 있는 service는 TodoToggableService를 만족하면 되기에 구현체가 무엇이 되었든 신경쓰지 않습니다.
만약, 저장하는 방식을 서버로 옮긴다고 하면, UserDefaultRepository를 ServerRepository로만 수정하면 되겠죠?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { self.container.register(Repository.self) { _ in ServerRepository() } self.container.register(TodoToggableService.self) { let repository = $0.resolve(Repository.self)! return TodoServiceImpl(repository: repository) } return true }
Swift
복사
그 외의 변경은 없습니다. 나머지 코드는 바꾸지 않아도 됩니다. 기존 코드를 건드리지 않고 새로운 걸 추가하는 것도 가능해집니다.
즉, 소프트웨어가 변경하기 쉽게 만들어 집니다.

 마치며

여태까지 개발한 방식이 객체 지향으로 개발하는 방식이었습니다.
즉, 클래스 다이어그램이나 의존성 주입, 의존성 역전이라고 하는 것들이 다 객체지향에 있던 기법이었습니다. 이 기법을 사용할 때 Protocol의 역할이 컸습니다. Protocol를 더 적극적으로 사용한다면 객체지향 프로그래밍을 더 잘 할 수 있지 않을까요?
class, struct, enum에 전부 Protocol를 적용할 수 있게 Protocol의 범위를 확장해보는 겁니다.
그렇게 되면, Protocol이 진짜 중요해지는 겁니다.
즉, Protocol 중심으로 코딩을 하게 됩니다. Protocol Oriented Programming이라는 거죠.
Protocol를 좀 더 적극적으로 써서 프로그래밍을 해봅시다. ⸜( ˙ ˘ ˙)⸝♡