ViewController OOP, POP를 향하여 (상속과 프로토콜)

LUYAN·2021년 12월 27일
0
post-thumbnail

코드로 UI를 구성하는 과정에서 반복된 코드를 줄이고, 보다 일관된 방법으로 ViewController을 구성하는 방법은 없을까에 대한 고민을 했다.

각 뷰에서 공통으로 사용하는 부분을 공통의 컨테이너로 만들어서 사용하고, 기본 UI 컨트롤러를 Sub-Classing 해서 가독성을 높일 수 있을 거라고 생각됐다.

이 생각을 하게 된 것은 ViewController가 많아지게 되면 많아진 View들을 어떻게 관리해야할까? 라는 것이 시작점이었다.
공통된 작업을 미리 구현할 수 있으면 좋겠다. 라는 생각이 들었고, 린생(갓생)👍으로부터 받은 피드백이 크게 작용했다.
분명 그 과정에선 내가 작성한 코드에 대해 의구심도 많이 들었다.

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

일반적으로 ViewController 파일을 만들면 다음과 같이 UIViewController을 상속 받고 있는 것을 확인할 수 있다.
특정한 뷰를 사용하지 않고서는 보통 위와 같은 상속 구조를 가지게 되고, UIViewController이 아닌 다른 뷰를 상속 받았다고 하더라도 실질적으로는 UIViewController를 상속한 구조를 확인할 수 있었다.

1. UIViewController을 상속한 기본 뷰 컨트롤러를 만들기

import UIKit

class RootView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
        setupUI()
    }

    required init?(coder: NSCoder) {
        fatalError()
    }

    func setup() { }
    func setupUI() {
        backgroundColor = .clear
    }
}

class RootViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
        bind()
    }

    func setup() { }
    func bind() { }
}

먼저 view를 viewController를 따로 나눠 뷰를 구성할 수 있도록 UIView를 상속한 RootView
UIViewController을 상속한 RootViewController을 선언했다.

내부에 선언된 메서드는 제 스타일의 코드이니 원하는 방향으로 코드를 작성하면 되겠다. 하지만, 위처럼 상속을 활용하게 된다면 RootView, RootViewController는 모든 뷰의 Root같은 역할을 할 것 같다는 느낌을 줘야겠지만, 일뷰 뷰에서는 공통된 기능을 필수적으로 요구하지 않을 수도 있기 때문에 커스텀 클래스를 상속할 필요가 있을까? 하는 의심 또한 충분히 해볼 수 있다.
또한 코드를 override하는 과정에서 추가로 생각해볼 수 있는 개념이라고 한다면, SOLID 원칙 중 OCP를 위반하고 있다고 말할 수 있다.

개방 폐쇄의 원칙(OCP)은 작성된 소스 코드가 확장에 대해 개방되어 있고, 수정에 대해 폐쇄되어 있는 원칙을 말한다.

OCP에 만족되는 설계를 하려면 변경점을 파악하고, 수정 시 영향 받지 않게 해야한다.

그렇다면 상속을 이용하지 않는 방법은 없을까?

프로토콜을 활용한 구현 방법에 대해서도 생각해보았다.

2. 프로토콜을 선언해 ViewController에 역할 부여하기

ViewConfiguration 이라는 프로토콜을 선언하고, protocol을 따를 경우, 하단의 3개의 함수를 구현하도록 했다.
이중 configureViews()의 경우엔 선택적인 구현이 가능하도록 extension을 활용해 기본 구현에서 아무런 작업이 이뤄지지 않도록 했다.


import Foundation

protocol ViewConfiguration {
    func buildHierarchy()
    func setupConstraints()
    func configureViews()
}

extension ViewConfiguration {
    func configureViews() {}
    
    func applyViewSettings() {
        buildHierarchy()
        setupConstraints()
        configureViews()
    }
}

결과적으로 봤을 때, 상단처럼 구현했을 때의 장점은 수평 구조로의 확장이 가능했다.
또한 뷰를 구현하는 코드 작성에 대해 선언한 프로토콜을 따르게 하여 일관된 코드 작성이 가능해졌다.

실제로 POP를 활용한 방법으로 ViewConfiguration을 작성해서 구현했을 경우엔 하단과 같이 extension으로 View의 구성을 담당하는 메소드들을 일관된 형식으로 볼 수 있었다.


// MARK: ViewConfigutation
extension AdministerViewController: ViewConfiguration {
    func buildHierarchy() {
        view.addSubviews(userTableView)
    }
    
    func setupConstraints() {
        NSLayoutConstraint.activate([
            userTableView.topAnchor.constraint(equalTo: self.view.topAnchor),
            userTableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
            userTableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            userTableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
        ])
    }
    
    func configureViews() {
        userTableView.dataSource = self
        userTableView.register(UserTableViewCell.self,
                           forCellReuseIdentifier: UserTableViewCell.reuseIdentifier)
    }
}

프로토콜을 사용한 덕분에 뷰의 구성에 대해서 개발하는 나 또한 그 역할에 맞게 코드 위치를 결정하려고 노력했던 것 같다.
아직 정확히 POP에 대해 완벽히 이해하고 있다고 생각하지 않는다.
앞으로 프로토콜에 대해서 좀더 알아보도록 하자

(+ 이후 디자인 패턴을 공부하다가 알게 됐지만, 단순 코드의 형식이 일관되서 좋다. 라고 생각만 했었는데, 이 형식이 템플릿 메소드 패턴의 전형적인 형태임을 알 수 있었다.)

Reference

profile
인생후르츠 #인내는 쓰고 그 열매는 달다

0개의 댓글