본문 바로가기

iOS

나의 iOS 아키텍처 패턴 일대기: MVC

이번 글에서는 그동안 경험했던 아키텍처 패턴들을 시간 순서대로 나열하면서 왜 사용하게 되었는지, 각 패턴들의 장단점이 무엇이였는지 정리를 해보려고 해요. 패턴들을 적극적으로 사용하기만 하고 패턴별로 어떤점이 좋았는지, 불편했는지에 대해서 코딩을 직접 하면서 느끼기만 했지, 명시적으로 정리를 해본적이 없어서 머릿속에 흐릿하게만 기억되고 있었어요. 이번 글을 통해서 선명하게 기억을 남겨보려고 합니다!

아래에 기록될 내용들은 제가 직접 사용해보면서 느꼈던 장단점입니다.
즉, 제 개인적인 견해일뿐 정답은 아닙니다! 더 좋은 장점이 있다던지, 불편한 점을 해결한 방법이 있다면 같이 공유하면 더 좋을 것 같아요!

아키텍처 패턴 사용 순서

사용해본 아키텍처 패턴들이 많지는 않습니다! 위에 그려진 것처럼 3개만 사용했는데요. 각 패턴들을 사용했던 이유와 패턴들마다 느꼈던 장단점을 간단한 예시와 함께 정리하려고 합니다.

 

1. MVC

스탠포드 iOS강의를 들어보셨다면 매우 낯익은 그림일 것 같아요!
제가 처음 iOS 입문할때는 당연히 이런강의들이나 책들을 보고 배웠기 때문에 자연스럽게 가장 단순한 구조인 MVC패턴부터 익히게 되었습니다.
MVCModel, View, Controller를 의미하는 단어로 사용자에게 보여지는 View에서 이벤트(ex. 터치, 드래그, 등등...)을 발생 시키면 이벤트가 Controller로 전달되고 Controller에서는 우리가 정의한 로직을 실행시키고 그에따라 변경되는 값(모델)을 저장하고 사용자에게 변경된 값을 보여주거나 다른 이벤트(ex. Alert, pushViewController..)등을 실행시킵니다. 

이미 어떤 패턴인지 알고 계신 분들은 글을 읽자마자 이해할 수 있지만 처음 본다면 띄용? 할수도 있을 것 같아요.
그래서 좀 더 이해하기 쉽게 간단한 예제를 사용해볼께요!

예시 화면에는 UILabel과 UIButton이 하나씩 있고 버튼을 누를 때마다 UILabel에 터치 횟수를 출력해줍니다.
3번을 초과한 버튼 터치가 있을 경우 터치 한도를 초과했다는 Alert을 띄워주는 간단한 앱입니다!
이 화면을 MVC로 표현한다면 다음처럼 표현할 수 있을 것 같아요.

class ViewController: UIViewController {
  @IBOutlet weak var titleLabel: UILabel!
  @IBOutlet weak var button: UIButton!
  
  private let maxTouchCount = 3
  private var currentTouchCount = 0
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    self.button.addTarget(self, action: #selector(onTapButton), for: .touchUpInside)
  }
  
  @objc private func onTapButton() {
    if self.isUnderMaxTouch(count: self.currentTouchCount) {
      self.increaseTouchCount()
    } else {
      self.showErrorAlert()
    }
  }
  
  private func isUnderMaxTouch(count: Int) -> Bool {
    return count < self.maxTouchCount
  }
  
  private func increaseTouchCount() {
    self.currentTouchCount += 1
    self.titleLabel.text = "\(self.currentTouchCount)"
  }
  
  /// Alert 출력
  private func showErrorAlert() { 
    ...
  }
}

단순한 기능을 제공하는 앱이기 때문에 위 코드처럼 버튼에 대한 이벤트를 수신했을 경우, 터치 카운트에 대한 validation을 진행한 후, 카운트와 뷰를 업데이트 시켜주거나 오류 alert을 띄워주는 로직을 실행시킵니다. 뷰컨트롤러에서 이벤트를 수신 받아서 유효성 검사, 뷰 업데이트, Alert 생성등 모든 로직을 정의했어요. 뷰컨트롤러에서 모든 역할을 하기 때문에 처음에 배우기 쉬웠고 이해하기도 쉬웠지만 여러 프로젝트를 진행하면서 단점들을 깨닳게 되었어요.

하나의 화면에서 처리해야할 이벤트의 종류가 다양해지고 실행해야하는 함수들이 많아지면서 뷰컨트롤러가 하는 역할이 너무나도 거대해 졌어요. 거대해지는 역할에 비례해서 코드의 양도 어마어마해졌어요. 위 예시는 단순하게 버튼 이벤트 하나만 처리하기 때문에 간결해보이지만 만약 처리해야할 버튼의 이벤트가 5~6개로 늘어나고, 버튼에 따라서 API통신을 해야하거나 디비에 데이터를 저장해야한다면 뷰컨트롤러의 코드는 순식간에 몇백줄이 넘어갈꺼에요...🙈 코드가 어마어마하게 길어진다면 작성한 나는 그나마 이해도가 있어서 코드를 수정하면서 유지보수하기 쉽겠지만 코드를 처음보는 사람은 내가 작성한 로직을 이해하는데도 꽤나 오랜 시간이 걸릴겁니다. (질문 펀치 엄청나게 맞을듯...)

그리고 또 하나의 큰 불편한 점도 있었어요. 대부분의 로직이 뷰컨트롤러에 있기 때문에 의존성도 뷰컨트롤러 혹은 뷰에에 묵여버려요.
MVC를 사용할 당시에는 테스트코드를 모를때라 크게 와닿지는 않았지만, 테스트코드를 구성하면서 개발을 하고 있는 현재에서 MVC를 다시 바라보았을 때 테스트 코드를 어떻게 구성해야할지 여전히 감이 잘 잡히지 않는 것 같아요.. 그런데 문제는 테스트코드를 공부하면서도 어떻게 구성을 해야할지 감이 안잡혔어요.. 테스트 코드의 필요성을 알게되고 나서 유닛테스트라는 것에 대해 조사를 해보았는데 가장 먼저 만나게 되는 튜토리얼은 아래와 같은 코드였어요.

func testSum() {
    let a = 1
    let b = 2
    
    AssertEquals(a + b, sum(a, b))
}

위와 같이 앱에서 사용되는 함수에 대해서 입력 값을 주고 그에 맞는 결과값이 결과로 나오는지 확인하는 코드가 대부분이였는데, 실제로 제가 개발하고 있는 앱에서는 저렇게 앱에서만 동작하는 로직의 수는 생각보다 적었고 대부분은 화면이 로드되었을 경우 API호출을 통해 데이터를 받아오거나 버튼을 눌렀을 때 유효성 검사 이후 데이터를 서버에 저장하는 로직들이였어요. 즉 이벤트의 출발점이 사용자의 입력이므로 뷰와 의존성이 연결되어있어요. 대부분은 서버와 통신을 했고 내부에서만 동작하는 로직은 많지 않아서 유닛테스트를 어떻게 구성해야하나에 대한 감이 잡히지 않았어요!

위에서 구현한 테스트 앱에서도 유닛 테스트를 작성할만한 함수는 func isUnderMaxTouch(count: Int) -> Bool 뿐이고 나머지 함수들은 모두 뷰와 연동이 되어있어서 어떻게 테스트를 할지 모르겠네요..

위에 언급한 불편한점들을 해결하고자 MVC에서 다른 아키텍처 패턴이 어떤 것들이 있는지 찾아보게되었고 그중에 그나마 다음 레벨로 배워보기 쉬울 것 같은 MVVM패턴을 발견했어요. 
이번 글에서는 MVC에 대해서만 정리를 하고 다음 글에서 MVVM에 대해서 경험한 것들을 적어볼께요.

 


간단 요약

MVC

장점

  • 다른 아키텍처 패턴에 비해서 단순한 구조이므로 처음 배우는 아키텍처 패턴으로 적합해 보인다.
  • 모든 로직이 뷰컨트롤러에 위치하므로 위의 예제와 같은 단순한 화면을 구성할 경우에 코드를 읽기 쉽다.

단점

  • 모든 로직이 뷰컨트롤러에 위치하므로 화면의 복잡도가 올라갈수록 뷰컨트롤러의 역할이 어마어마하게 커진다(장점이자 단점쓰..)
  • 코드의 양이 많아지면 코드의 가독성도 떨어지기 마련이다.
  • 대부분의 로직이 뷰컨트롤러 혹은 뷰에 의존성이 존재해서 테스트를 어떻게 구성해야할지 감이 잡히지 않는다 😭