본문 바로가기
Project/뿌대식: 부산대학교 학식 알리미

뿌대식: 네트워크 관련 개선

by songmoro 2024. 8. 23.
728x90

네트워크 분석 글에서 언급했듯 이번 리팩토링에서는 아래 두 가지를 개선했습니다.

  1. 네트워크 호출 로직
  2. 네트워크 요청 속도 개선

 

 

네트워크 호출 로직 관련

개선 전 뿌대식의 네트워크 호출은 DB가 백업 중인지 검사 후, 학생 식당 혹은 기숙사 DB 데이터를 불러옵니다.

네트워크 체이닝 때문에 arrowHead 코드가 생기고, 캠퍼스 전환 시 네트워크 호출이 중첩되는 문제가 있었습니다.

 

 

arrowHead 코드 문제

네트워크 호출 관련 코드는 긴 편인데, 데이터베이스 백업을 검사와 식당 데이터가 다른 모델을 사용해 중복되는 코드가 존재했습니다.

 

 

 

네트워크 호출 중첩 문제

네트워크 호출이 중첩되면, 쌓인 네트워크 호출이 완료될 때마다 현재 식당이 호출 횟수만큼 증가하는 이슈가 있었습니다.

 

(캠퍼스 탭을 전환하는 만큼 쌓인 네트워크 호출들)

 

 

네트워크 요청 속도 관련

네트워크 요청부터 응답까지 시간은 이전 글에서 사용한 분석과 같이 각 노션 응답 당 500ms ~ 1s 내외로 모든 요청이 완료되기까지 평균적으로 (500ms ~ 1s) * 3회, 즉 1.5 ~ 3초의 시간이 소요됩니다.

 

 

 

개선

arrowHead → Generics, Async & Await

기존 코드를 제네릭을 제대로 활용하도록 수정한 뒤, 컴플리션 핸들러를 Async & Await으로 래핑 했습니다.

 

기존 코드

class RequestManager {
    static private let provider = MoyaProvider<API>()
    
    static func request(_ target: API, completion: @escaping ([[String: String]]) -> ()) {
        provider.request(target) { result in
            switch result {
            case .success(let response):
                if (200..<300).contains(response.statusCode) {
                    if let decodedData = try? JSONDecoder().decode(NotionResponse<DeploymentProperties>.self, from: response.data) {
                        let status = decodedData.results.compactMap {
                            $0.properties.toDict()
                        }
                        
                        completion(status)
                    }
                }
            case .failure(let error):
                fatalError(error.localizedDescription)
            }
        }
    }
    
    static func request<T: Serializable>(_ target: API, _ responseType: T.Type, completion: @escaping ([T]) -> (Void)) {
        provider.request(target) { result in
            switch result {
            case .success(let response):
                if (200..<300).contains(response.statusCode) {
                    if let decodedData = try? JSONDecoder().decode(NotionResponse<DomitoryProperties>.self, from: response.data) {
                        let mappedValue = decodedData.results.compactMap {
                            T($0.properties)
                        }
                        
                        completion(mappedValue)
                    }
                    
                    if let decodedData = try? JSONDecoder().decode(NotionResponse<RestaurantProperties>.self, from: response.data) {
                        let mappedValue = decodedData.results.compactMap {
                            T($0.properties)
                        }
                        
                        completion(mappedValue)
                    }
                }
            case .failure(let error):
                fatalError(error.localizedDescription)
            }
        }
    }
}

 

개선 코드

class RequestManager {
    // ...
    
    func request<T: Codable>(_ target: API, _ responseType: T.Type) async -> T? {
        await withCheckedContinuation { continuation in
            self.request(target, responseType) { result in
                switch result {
                case .some(let result):
                    continuation.resume(returning: result)
                default:
                    continuation.resume(returning: nil)
                }
            }
        }
    }
    
    private func request<T: Codable>(_ target: API, _ responseType: T.Type, completion: @escaping (T?) -> (Void)) {
        let request = provider.request(target) { result in
            switch result {
            case .success(let response) where (200..<300).contains(response.statusCode):
                guard let decodedData = try? JSONDecoder().decode(T.self, from: response.data)
                else { return }
                completion(decodedData)
            default:
                completion(nil)
            }
        }
    }
    
    // ...
}

 

 

네트워크 호출 중첩 → Cancellable 활용, disabled(condition) 사용

네트워크 호출이 중첩되는 건 네트워크 요청을 관리되지 않기 때문으로 네트워크 요청을 따로 저장하고, 캠퍼스 전환 시 기존 네트워크 요청을 취소하도록 했습니다.

 

추가로, 같은 캠퍼스 탭 등 네트워크 요청을 추가로 할 필요가 없는 경우(부산 → 부산)를 위해 같은 캠퍼스를 탭 하지 못하도록 disabled를 사용했습니다.

 

개선 코드

class RequestManager {
    // ...
    private var requestArray: [Cancellable] = []
    
    // ...
    
    private func request<T: Codable>(_ target: API, _ responseType: T.Type, completion: @escaping (T?) -> (Void)) {
        let request = provider.request(target) { result in
    // ...
        }
        
        requestArray.append(request)
    }
    
    func cancleAllRequest() {
        requestArray.forEach { $0.cancel() }
        requestArray.removeAll()
    }
}

 

(네트워크 응답을 받기 전 캠퍼스를 막무가내로 전환해도 네트워크 호출이 중첩되지 않는 모습)

 

 

네트워크 호출 속도 → 로딩 뷰(스켈레톤) 적용

네트워크 호출은 결국 식당 데이터를 확인하기 위한 것으로, 기술적인 측면에서 개선 방법은 아래 사항을 고려해 볼 수 있습니다.

  • 네트워크 데이터 캐싱(로컬 캐시로 저장)
  • 네트워크 호출 로직 변경
  • 서버(노션) 변경

 

네트워크 속도 자체가 느린 상황이라 물리적인 서버 교체 외에는 응답 속도를 빠르게 할 방법이 없었고, 네트워크 응답을 로컬 캐시로 저장했다가 불러오는 방식을 고려했습니다.

 

그에 따라 NSCache를 학습했지만, 최종적으로 채택하진 않았습니다.

그 이유는 앱이 제공하는 데이터에 비해 너무 오버스펙이고, 단기간에 바로 적용하기엔 학습이 충분하지 않다고 판단했기 때문입니다.

 

그래서 문제를 다시 정의해봤을 때, 아래와 같은 흐름으로 결론이 나왔습니다.

  • 네트워크 속도가 느리다. → 사용자가 식단을 확인하지 못한다. → 식단이 존재하는지 모른다. → 식단이 현재 보이지 않기 때문에, 기다리는 시간 동안 지루함을 느낀다.
  • 최종 결론: 식단이 로딩되는 동안 지루하지 않게 만든다.

로딩되는 동안 제공할 수 있는 방법 중 로딩 뷰(흔히 말하는 스켈레톤 혹은 스피너)를 제공하기로 결론지었습니다.

 

 

/* 사담

이에 관해 개인적으로 좋아하는 엘리베이터 이야기를 의사 결정에 녹여냈습니다.

 

개발자라고 한다면 스스로를 코딩하는 사람으로 정의하지 않았으면 좋겠다.

때로는 문제를 해결하는 가장 좋은 방법이 정책을 바꾸고 프로그래밍을 안 하는 것일 수도 있다.

 

고객들이 엘리베이터를 기다리는 시간이 오래 걸려 지루함을 느낀다는 걸 깨달았다.
A 회사는 100억을 들여, 속도가 20% 빠른 엘리베이터를 설치했다.
B 회사는 엘리베이터 앞에 거울을 설치했다.

 

(추가적으로 공유하고 싶은 좋아하는 이야기 중 하나, 바닐라 아이스크림 알레르기를 가진 자동차)

*/

 

 

무튼, 로딩 중일 시 지루함을 느끼지 않도록, 애니메이션이 포함된 로딩 뷰를 만드는 방향으로 개선하였습니다.

 

 

 

(사각, 라운드 엔드포인트와 라인 두께 별 눈 감은 이미지)

 

 

728x90