Aug 02, 2021, TIL (Today I Learned) - Design Patterns on IOS

Inwoo Hwang·2021년 8월 26일
1
post-thumbnail

본 글은 raywenderlich의 Design Patterns on iOS using Swift - Part 1/2, Design Patterns on iOS using Swift - Part 2/ 2을 읽고 이해한 바탕으로 필요한 부분을 정리한 글입니다.

Design Patterns on iOS


iOS Design Pattern

소프트웨어 디자인에서 자주 직면하게 되는 문제를 해결하기 위한 재사용가능한 해결법입니다.

The Singleton Pattern

싱글톤 패턴은 주어진 클래스의 하나의 인스턴스가 존재하고 해당 인스턴스 접근이 전역으로 가능하게 하는 패턴입니다.

final class LibraryAPI {

 // 1
 static let shared = LibraryAPI()

 // 2
 private init() {
 }
}

shared 라는 static 상수를 통해서 해당 클래스의 싱글턴 객체를 전역에서 접근이 가능해집니다.

싱글톤 패턴은 주어진 클래스의 하나의 인스턴스가 존재하고 해당 인스턴스 접근이 전역으로 가능하게 하는 패턴입니다.

final class LibraryAPI {
  // 1
  static let shared = LibraryAPI()
  // 2
  private init() {

  }
}
  • shared 라는 static 상수를 통해서 해당 클래스의 싱글턴 객체를 전역에서 접근이 가능해집니다.
  • private 이니셜라이저를 설정하여 해당 클래스가 외부에서 새로운 인스턴스로 만들어지는 것을 방지합니다.

전역으로 접근할 수 있는 싱글턴과 같은 공유 리소스의 인스턴스는 하나로 제한 하여 해당 리소스 접근 을 thread-safe하게 구현할 수 있습니다. 여러 인스턴스를 통해 동시다발적으로 싱글턴객체를 수정한다면 문제가 생길 수 있으니 이를 방지하는 것이죠. 해당 객체는 lazy하게 로딩됩니다. 즉 해당 객체가 필요 없을 때는 메모리에 올라와 있지 않고 호출시 메모리에 로딩 되는 것이지요.

싱글턴의 가장 큰 문제점: Testing

싱글턴 객체를 테스트하려면 테스트의 순서 또한 굉장히 중요 해 집니다 왜냐하면 전역적으로 호출 되기 때문에 상태변화의 순서를 잘 알아야지 정상적인 테스트가 가능한 것이지요. 그리고 이 점은 해당 객체를 mock하기 굉장히 어렵게 만듭니다. 프로젝트 단위가 커지면 커질 수록 테스트가 힘들어집니다.

The Facade Design Pattern

Facade 디자인 패턴은 복잡한 서브시스템을 하나의 인터페이스로 제공하는 패턴입니다. 여러 개의 클래스와 api를 들어내는 것이 아니라 하나의 통합된 API를 유저에게 보여주는 것이지요.

해당 API를 사용하는 사람은 인터페이스 안의 복잡한 세부사항을 신경쓰지 않아도 됩니다. 때문에 많은 양의 이해하기 어려운 클래스를 가지고 프로그램을 구현할 때 해당 패턴을 사용하는 것이 이상적입니다.

final class LibraryAPI {
 static let shared = LibraryAPI()
 private let persistencyManager = PersistencyManager()
 private let httpClient = HTTPClient()
 private let isOnline = false

 private init() { }

 

 func getAlbums() -> [Album] {
  return persistencyManager.getAlbums()

 }

 

 func addAlbum(_ album: Album, at index: Int) {
  persistencyManager.addAlbum(album, at: index)

  if isOnline {
   httpClient.postRequest("api/addAlbum", body: album.description)

  }
 }

 

 func deleteAlbum(at index: Int) {
  persistencyManager.deleteAlbum(at: index)
   
  if isOnline {
   httpClient.postRequest("/api/deleteAlbum", body: "\(index)")
  }
 }
}
final class LibraryAPI {
  static let shared = LibraryAPI()
  private let persistencyManager = PersistencyManager()
  private let httpClient = HTTPClient()
  private let isOnline = false
  private init() { }
  
  func getAlbums() -> [Album] {
    return persistencyManager.getAlbums()
  }
  
  func addAlbum(_ album: Album, at index: Int) {
    persistencyManager.addAlbum(album, at: index)
    if isOnline {
      httpClient.postRequest("api/addAlbum", body: album.description)
    }
  }
  
  func deleteAlbum(at index: Int) {
    persistencyManager.deleteAlbum(at: index)
    
    if isOnline {
      httpClient.postRequest("/api/deleteAlbum", body: "\(index)")
    }
  }
}

위 코드를 보면 LibraryAPI 는 Facade Class로서 PersistencyManagerHTTPClient 인스턴스를 갖게되고. 해당 Library는 이 두 인스턴스를 외부로부터 숨겨주기 때문에 해당 프로젝트에 있는 다른 클래스들은 해당 객체들로부터 분리됩니다.

The Decorator Design Pattern

Decorator 패턴은 동적으로 객체에게 책임과 행동을 코드의 수정 없이 부여하게 해 줍니다. 부모객체를 상속받는 것의 대안이죠. 스위프트에는 2가지 방법을 통해서 Decorator Design Pattern을 구현할 수 있습니다.

1. Extension

존재하는 클래스, 구조체, 그리고 열거형과 같은 타입을 수정하지 않고도 extension 을 활용하여 새로운 기능을 부여할 수 있습니다. 기존 decorator와 extension의 개념은 조금 다릅니다. 왜냐하면 extension은 자기가 확장하는 클래스의 인스턴스를 갖지 않기 때문이죠.

2. Delegation

Delegation은 하나의 객체가 다른 객체의 기능 일부분을 대신 처리 해 주는 메커니즘을 가진 패턴입니다. UITableView 가 자주 예시로 나오는걸 볼 수 있어요. UITableView 는 datasource와 delegate 총 두 가지의 delegate 타입의 프로퍼티를 갖고 있습니다. datasource는 tableview에 몇 개의 row 그리고 몇 개의 section이 있어야 하는지 tableView에게 알려주는 역할을 합니다. 이 과정을 통해서 UITable은 UITable의 데이터를 보여주는 클래스(datasource를 채택한 클래스)로부터 독립적이게 구현될 수 있습니다. delegate 는 tableView의 row를 클릭했을 때 어떤 작업을 해야 하는지 tableView에게 알려줄 수 있습니다.

The Adapter Pattern

Adapter 패턴은 호환이 안되는 인터페이스간의 협업을 가능하게 하는 패턴입니다. UITableViewDelegate, UIScrollViewDelegate, UIScrollViewDelegate, NSCoding 그리고 NSCopying 이 애플이 사용하는 adapter 패턴의 좋은 예시입니다.

*자세한 코드는 맨 아래 링크를 타고 보시면 확인하실 수 있습니다

The Observer Pattern

Observer 패턴을 활용하면 하나의 객체는 상태 변화사항을 감지하고 다른 객체에게 알려줍니다. 이 과정에서 객체들은 서로를 몰라도 됩니다 그리고 이 부분은 코드간의 결합성을 낮춰주는 장점이 있습니다.

메커니즘은 이와 같습니다: observer 객체가 어떠한 객체의 변화에 대한 관심을 등록하게 되면 등록된 객체의 프로퍼티에 변화가 일어나게 되면 이 변화사항을 관심있어하는 객체들은 알림을 받을 수 있게 됩니다.

MVC 패턴을 기반으로 이런 코드를 짜기 위해서는 Model과 View간의 소통은 가능해야 하지만 다이랙트한 소통은 지양되어야 합니다.

Album cover scroller

Pop Music을 보여주는 앱이 있을 때 상단의 수평 스크롤뷰의 사진을 선택할 때마다 사용자는 해당 음악의 상세 내용을 확인할 수 있습니다. 여기서 각 음악의 커버사진을 구현할 때 Notification을 사용 해 볼 수 있습니다.

그러기 위해서는 이미지를 다운로드 받아야 하는데요. 이 과정에서 유의해야 할 점은 아래와 같습니다.

  1. AlbumViewAPI 모델과 직접적으로 소통하면 안됩니다. View와 모델의 소통은 지양해야 하기 때문이죠. 또한 View의 로직과 소통의 로직이 섞이면 바람직한 모델이 될 수 없습니다.
  2. 위와 같은 이유로 API 또한 AlbumView 를 알면 안됩니다.
  3. API 는 이미지가 다운이 완료가 되면 AlbumView에게 알려줘야 합니다.AlbumView 가 사용자에게 음악의 커버사진을 알려줘야 하기 때문이죠

Notifications

Notification은 구독-출판 model에 기반을둡니다.

출판(publish)하는 객체가 구독하는 객체에게 메세지를 보낼 수 있게 됩니다.

출판 객체는 구독자 객체에 대해서 몰라도 됩니다.

AlbumView: 출판하는 객체

class AlbumView: UIView {
  
  private var coverImageView: UIImageView!
  private var indicatorView: UIActivityIndicatorView!
  
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    commonInit()
  }
  
  init(frame: CGRect, coverUrl: String) {
    super.init(frame: frame)
    commonInit()
    NotificationCenter.default.post(name: Notification.Name.BLDownloadImage, object: self, userInfo: ["imageView" : coverImageView, "coverULR" : coverUrl])
  }
}
init(frame: CGRect, coverUrl: String) {
  super.init(frame: frame)
  
  NotificationCenter.default.post(name: .BLDownloadImage, object: self, userInfo: ["imageView": coverImageView, "coverUrl" : coverUrl])
}

위 코드는 NotificationCenter 싱글톤에게 notification을 보내는 코드 입니다. 그리고 이 notification은 UIImageView 와 해당 이미지를 다운로드 받아야 하는 URL을 갖고 있습니다. 해당 코드를 AlbumView 가 초기화 되는 시점에 포함하여 AlbumView 의 객체가 생성될 때 notification을 보낼 수 있개 설정합니다.

LibraryAPI: 구독하는 객체(관찰자)

final class LibraryAPI {
  private init() {
    NotificationCenter.default.addObserver(self, selector: #selector(downloadImage(with:)), name: .BLDownloadImage, object: nil)
  }
  
  @objc func downloadImage(with notification: Notification) {
    guard let userInfo = notification.userInfo,
      let imageView = userInfo["imageView"] as? UIImageView,
      let coverUrl = userInfo["coverUrl"] as? String,
      let filename = URL(string: coverUrl)?.lastPathComponent else {
        return
    }
    
    DispatchQueue.global().async {
      let downloadedImage = self.httpClient.downloadImage(coverUrl) ?? UIImage()
      DispatchQueue.main.async {
        imageView.image = downloadedImage
      }
    }
  }
}
private init() {
    NotificationCenter.default.addObserver(self, selector: #selector(downloadImage(with:)), name: .BLDownloadImage, object: nil)
  }

AlbumView 가 매 번 BLDownloadImage notification을 post하게될 때 LibraryAPIBLDownloadImage notification을 구독하고있게됩니다. 그리고 위 코드에 따라서 LibraryAPIdownloadImage(with:) 메서드를 호출하여 응답하게 됩니다.

The Adapter Pattern

Adapter 패턴은 호환이 안되는 인터페이스간의 협업을 가능하게 하는 패턴입니다. UITableViewDelegate, UIScrollViewDelegate, UIScrollViewDelegate, NSCoding 그리고 NSCopying

[참고]:

Design Patterns on iOS using Swift - Part 1/ 2

Design Patterns on iOS using Swift - Part 2/ 2

Design Patterns implemented in Swift 5.0

profile
james, the enthusiastic developer

0개의 댓글