BoxOffice 리팩토링 프로젝트.
영화진흥위원회 Open API, Daum 검색 API를 활용해 선택한 날짜의 일일 박스오피스 목록과 선택한 영화의 포스터 이미지 및 상세 정보를 확인할 수 있는 앱
항목 | 사용기술 |
---|---|
Architecture | MVVM |
UI | UIKit |
Data Binding | Observer Pattern (Observable) |
Network Layer | URLSession |
ImageCache | NSCache, FileManager |
2024.02.13 ~ 2024.02.26 | NetworkLayer 구현 |
2024.02.27 ~ 2024.02.28 | OpenAPI Configuration, BoxOffice / ImageURLSearcher 구현 |
2024.03.01 ~ 2024.03.08 | ImageCache, CacheStorage 구현 |
2024.03.09 ~ 2024.03.11 | ImageProvider 구현 |
2024.03.11 ~ 2024.04.09 | NetworkLayer, Cache 리팩토링 및 Unit test 작성 |
2024.04.10 | ImageProvider 리팩토링 및 Unit test 작성 / Unit test 마무리 |
2024.04.11 | UI 구현 시작, Coordinator Pattern / Observer Pattern 적용 |
2024.04.11 ~ 2024.04.18 | DailyBoxOfficeView 구현 |
2024.04.18 ~ 2024.04.22 | MovieDetailsView, CalendarView 구현 |
2024.04.22 ~ 2024.04.23 | CollectionView Mode 변경 기능 구현 |
서드 파티를 사용하지 않고 Data Binding을 진행하기 위해 프로퍼티 옵저버와 클로저를 활용해 Observable
타입을 정의
Network Layer는 외부에서 APIConfigurationType
protocol을 채택한 타입을 통해 endpoint를 구성하고 NetworkProvider
로 네트워크 요청을 보낼 수 있도록 설계
APIConfigurationType
프로토콜 + 제네릭을 통한 값 타입 다형성 제공
모듈화 된 레이어는 아니지만 외부에서 사용할 타입은 NetworkProvider
, APIConfigurationType
, NetworkSessionType
으로 한정
NetworkSessionType
은 ImageProvider
와 같이 URL을 통한 네트워크 통신이 필요한 경우를 위해 외부에서 사용할 수 있도록 함
DataConvertible
프로토콜 테스트NSCache
와 FileManager
를 통한 캐시 저장이 잘 이루어지는가를 테스트메인 화면 | 새로고침 | 날짜선택 |
---|---|---|
화면 모드 변경 | 영화 상세 화면 | 이미지 캐싱 |
---|---|---|
Unit Test를 진행하다 보니 캐시 만료기간이나 자동 삭제 관련된 코드가 전부 하드코딩되어 있어 테스트에서 짧은 시간을 직접 설정해줄 수 없었다.
추가로 DiskStorage의 경우, 외부에서 메서드를 호출할 때 저장할 타입을 Data 타입으로 변환해야만 했다.
CacheExpiration
, ExpirationExtending
타입을 추가 정의하여 만료 기간 관련된 코드의 확장성, 가독성을 높임DataConvertible
프로토콜을 정의해 DiskStorage에 저장할 타입에 채택결과로, 캐시 만료 기간을 사용자 정의할 수 있게 되었고 테스트 시 이전보다 짧은 기간을 설정할 수 있게 됨.
캐시를 활용하고 싶은 타입이 추가된다면 DataConvertible
프로토콜을 준수하게 하여 수월하게 추가 가능.
▶︎ 개선 후
APIConfigurationType
프로토콜에 associatedtype으로 JSONData를 디코딩할 타입을 알려주고 있다.
이 때, enum에 프로토콜을 채택해 baseURL이 같은 endpoint를 함께 관리하고 싶었으나 associatedtype을 특정하게 되면 설정된 타입과 매칭되지 않는 Response가 필요한 요청을 보냈을 때 오류가 발생하게 된다.
디코딩 타입을 강제하는 만큼 이러한 문제가 발생하는 것은 사용할 때 편의성에 좋지 않다고 생각했다.
enum을 사용하지 않고 요청에 맞는 APIConfiguration을 struct로 각각 정의하기로 결정했다.
enum을 통해 baseURL이 같고 path가 다른 API를 case로 관리하는 것이 유용할 것 같다. 추후 Moya를 참고해보고 좋은 방법을 찾아보도록 하자.
화면전환 로직을 전부 Coordinator
에게 맡겨두었다.
Coordinator
에는 parent - child 관계가 있고, child에 대한 참조를 parent에서 배열로 갖는다. 이 때, 새로운 화면을 띄운 다음 해당 화면을 pop
/dismiss
하게 되면 Coordinator의 deinit
이 호출되지 않았고, 인스턴스가 메모리에 그대로 남아있는 것을 확인했다.
메모리 누수가 발생하는 Coordinator
에 finish()
라는 메서드를 정의해 parent가 가지고 있는 참조를 제거할 수 있도록 했다.
Coordinator의 역할은 자신이 관리하는 ViewController가 할당 해제되면 끝나는 것이기 때문에 ViewController의 deinit에서 coordinator의 finish를 호출하여 메모리 누수를 해결했다.
상속 기능이 있는 class의 경우, 상속을 통한 overriding이 가능할 때 프로퍼티, 메서드 dispatch에 static dispatch 보다 성능상 손해가 있는 dynamic dispatch를 이용한다. 이를 최적화하기 위해 상속을 활용하지 않는 class에 대해 final
키워드, private
접근제어를 적극적으로 활용했다.
AlertBuilder
를 정의할 때 struct와 class 중 어떤 것을 선택할지 고민했다.
AlertController를 선언적으로 설정하고 화면에 보여주기 위해 AlertBuilder에는 2개의 프로퍼티가 필요하다. 이를 Struct로 정의하면 아래와 같다.
Builder 패턴의 특성상 메서드에서 자기 자신을 반환해야 한다.
Self
를 반환하면 AlertBuilder가 struct이므로 메모리 영역 중 stack 영역에 인스턴스가 할당된다. 이 때, Builder의 프로퍼티가는 모두 class이기 때문에 heap 영역에 인스턴스가 할당된 상태이며 Builder의 인스턴스가 메모리에 할당될 때마다 참조 overhead가 발생하게 된다.
이러한 overhead를 줄이기 위해 Builder를 class로 정의하고 인스턴스를 하나로 유지하며 Self 반환 시 참조를 반환하도록 했다.
OnDiskCacheStorage
는 FileManager
를 활용해 샌드박스 내부 Caches 폴더에 캐시할 데이터를 저장한다.
캐시 만료기간은 생성된 파일의 attributes
를 통해 관리하고 있다. 이를 활용하기 위해서는 URL 인스턴스에서 제공하는 메서드 resourceValues(forKeys:)
를 이용해야 했다.
이에 따라 OnDiskCacheStorage
인스턴스 메서드 곳곳에 해당 메서드의 호출이 중복되었다.
중복된 코드로 인해 코드가 길어지고 Storage 설정 및 CRUD 외의 다른 기능이 늘어나 코드의 가독성이 떨어졌다.
이를 해결하기 위해 FileMeta
라는 중첩 타입을 정의해 반복되는 코드를 줄이고 가독성을 높여주는 방향으로 리팩토링했다.
결과, OnDiskCacheStorage
에서 만료 기간 설정 및 확인 기능이 FileMeta
로 분리되고 메서드 내부 코드 가독성이 좋아졌다.
▶︎ FileMeta Nested Type
처음 만들었던 Observable 타입은 Observer가 정의되어있지 않아 하나의 구독만 유지할 수 있었다.
하지만 DailyBoxOfficeViewModel의 currentDate에서 구독이 2회 필요하게 되었다.
복수 구독이 가능하도록 Observable을 수정했다.
OnDiskCacheStorage
테스트 코드 작성 중 setUp
, tearDown
을 override 할 때 의문이 생겼다.
FileManager.default
싱글톤 인스턴스를 참조하는 프로퍼티를 갖는 OnDiskCacheStorage
는 이니셜라이저에서 fileManager에 대한 의존성을 주입받는다.
따라서 테스트를 위해 innerStorage에 FileManager.default에 대한 참조를 할당하고 tearDown에서 nil을 할당하려고 했다.
이 때, 뭔가 어색함을 느꼈다. FileManager.default
는 FileManager
의 타입 프로퍼티 싱글톤 인스턴스로 lazy하게 생성되며 런타임에 생성 이후 할당이 해제되지 않는다. 따라서, tearDwon에서 innerStorage에 nil을 할당한다 해도 인스턴스가 해제되지 않을 것이다.
그렇다면 innerStorage를 tearDown해야할 필요가 있을까 라는 고민이 생겼다.
diskStorage를 초기화하면 이니셜라이저 파라미터의 기본값으로 설정된 FileManager.default에 접근하여 1회 생성되므로 테스트가 끝나기 전까지는 default 인스턴스가 유지될 것이라고 생각했다. 왜냐하면 타입 프로퍼티로 생성된 싱글톤 인스턴스의 경우 프로그램이 종료되기 전까지는 메모리에서 해제할 방법이 없기 때문이다.
따라서 setUp, tearDown에서 참조 변수에 nil을 할당할 필요가 없다고 생각해 조금 더 간단히 작성할 수 있도록 수정했다.
innerStorage는 모든 테스트에 공통적으로 필요한 조건으로 생각해 XCTestCase의 타입 메서드인 setUp과 tearDown을 활용해볼 수도 있지만 어차피 tearDown에서 할당 해제할 수 없으므로 한 번 생성해주기만 하기로 결정했다.