SwiftUI 팀 프로젝트 혼자 리팩토링하기 #10 : Moya Provider 의존성 관리

·2024년 7월 21일
0

리팩토링 일지

목록 보기
11/13

요약



Moya Provider 의존성 관리

<리팩토링 전 DataSource>

protocol VoteDataSourceType {
    func getVotes(_ object: VoteRequestObject) -> AnyPublisher<[PostResponseObject], APIError>
    func getVoteDetail(_ postId: Int) -> AnyPublisher<VoteDetailResponseObject, APIError>
    func postVote(_ postId: Int, _ object: ChooseRequestObject) -> AnyPublisher<VoteCountsResponseObject, APIError>
    func registerVote(_ object: VoteCreateRequestObject) -> AnyPublisher<Void, APIError>
}

final class VoteDataSource: VoteDataSourceType {
    
    private let provider = MoyaProvider<VoteAPI>(session: Session(interceptor: AuthInterceptor()))

    func getVotes(_ object: VoteRequestObject) -> AnyPublisher<[PostResponseObject], APIError> {
        provider.requestPublisher(.getVotes(object))
            .tryMap {
                try JSONDecoder().decode(GeneralResponse<[PostResponseObject]>.self, from: $0.data)
            }
            .compactMap { $0.data }
            .mapError { APIError.error($0) }
            .eraseToAnyPublisher()
    }

    func getVoteDetail(_ postId: Int) -> AnyPublisher<VoteDetailResponseObject, APIError> {
        provider.requestPublisher(.getVoteDetail(postId))
            .tryMap {
                try JSONDecoder().decode(GeneralResponse<VoteDetailResponseObject>.self, from: $0.data)
            }
            .compactMap { $0.data }
            .mapError { APIError.error($0) }
            .eraseToAnyPublisher()
    }

    func postVote(_ postId: Int, _ object: ChooseRequestObject) -> AnyPublisher<VoteCountsResponseObject, APIError> {
        provider.requestPublisher(.postVote(postId: postId, requestObject: object))
            .tryMap {
                try JSONDecoder().decode(GeneralResponse<VoteCountsResponseObject>.self, from: $0.data)
            }
            .compactMap { $0.data }
            .mapError { APIError.error($0) }
            .eraseToAnyPublisher()
    }

    func registerVote(_ object: VoteCreateRequestObject) -> AnyPublisher<Void, APIError> {
        provider.requestPublisher(.registerVote(object))
            .tryMap {
                try JSONDecoder().decode(GeneralResponse<NoData>.self, from: $0.data)
            }
            .map { _ in }
            .mapError { APIError.error($0) }
            .eraseToAnyPublisher()
    }
}
  • DataSource 내에서 provider 객체를 직접 생성
  • 동일한 로직의 requestPublisher을 계속해서 사용하는 중복 코드

DIP를 위해 ProviderType 프로토콜을 만들고 의존성을 주입하기로 함.
그리고 공통된 requestPublisher 을 구현하기로!


<리팩토링 후>

Provider

protocol NetworkProviderType: AnyObject {

    func requestPublisher<U: Decodable>(
        _ target: TargetType,
        _ responseType: U.Type
    ) -> AnyPublisher<U, APIError>

    func requestVoidPublisher(_ target: TargetType) -> AnyPublisher<Void, APIError>

    func requestLoginPublisher(_ target: TargetType) -> AnyPublisher<AppleUserResponseObject, APIError>
}
  • U 타입으로 decode하는 requestPublisher
  • decode가 필요 없는, Void를 방출하는 requestVoidPublisher
  • apple login만 처리 로직이 달라서 따로 구현할 requestLoginPublihser

NetworkProviderType 프로토콜 규격을 만들고, 위와 같은 3개의 메서드를 선언해 주었음.

이를 구현하기 위해서는 메서드들 내에서 MoyaProvider에 접근하여 requestPublisher을 호출해야 함.

class NetworkProvider: NetworkProviderType {

    static let shared = NetworkProvider()

    private let provider: MoyaProvider<MultiTarget>

    private init() {
        let session = Session(interceptor: AuthInterceptor.shared)
        self.provider = MoyaProvider<MultiTarget>(session: session)
    }

    func requestPublisher<U>(_ target: TargetType, _ responseType: U.Type) -> AnyPublisher<U, APIError> where U : Decodable {
        provider.requestPublisher(MultiTarget(target))
            .tryMap { response in
                let decodedResponse = try JSONDecoder().decode(GeneralResponse<U>.self, from: response.data)
                guard let data = decodedResponse.data else {
                    throw APIError.decodingError
                }
                return data
            }
            .mapError { error in
                if let moyaError = error as? MoyaError {
                    return APIError.moyaError(moyaError)
                } else if let apiError = error as? APIError {
                    return apiError
                } else {
                    return APIError.error(error)
                }
            }
            .eraseToAnyPublisher()
    }

    func requestVoidPublisher(_ target: TargetType) -> AnyPublisher<Void, APIError> {
        provider.requestPublisher(MultiTarget(target))
            .map { _ in }
            .mapError { APIError.error($0) }
            .eraseToAnyPublisher()
    }

    func requestLoginPublisher(_ target: TargetType) -> AnyPublisher<AppleUserResponseObject, APIError> {
        provider.requestPublisher(MultiTarget(target))
            .tryMap { response in
                let decodedResponse = try JSONDecoder().decode(GeneralResponse<AppleUserResponseObject>.self, from: response.data)

                if let divisionCode = decodedResponse.divisionCode,
                   let tokens = decodedResponse.data?.jwtToken,
                   divisionCode == "E009" {
                    let tokens: TokenObject = tokens

                    throw APIError.notCompletedSignUp(token: tokens)
                }

                return decodedResponse
            }
            .compactMap { $0.data }
            .mapError { error in
                if let moyaError = error as? MoyaError {
                    return APIError.moyaError(moyaError)
                } else if let apiError = error as? APIError {
                    return apiError
                } else {
                    return APIError.error(error)
                }
            }
            .eraseToAnyPublisher()
    }
}

따라서 NetworkProviderType을 채택한 클래스를 만들고 provider을 생성해 주었음.


  • 공유 자원이 없고, 전역적으로 사용하기 위해서 싱글톤으로 선언했고, 토큰 인터셉터를 주입하여 초기화시켰다.

  • 이때 MultiTarget이라는 것을 사용했는데, 이는 다양한 TargetType을 provider가 수행할 수 있게 함.

    MultiTarget:
    A TargetType used to enable MoyaProvider to process multiple TargetTypes.

    protocol 내에서 protocol의 제네릭인 associatedType 을 활용하는 것도 후보군에 있었는데, MultiTarget을 쓸 수 있도록 구현되어 있었어서 MultiTarget 을 그대로 사용.

  • 프로토콜을 채택함으로써 준수해야 하는 메서드들에 알맞은 공통된 로직을 구현해 줬다.


DataSource

protocol VoteDataSourceType {
    func getVotes(_ object: VoteRequestObject) -> AnyPublisher<[PostResponseObject], APIError>
    func getVoteDetail(_ postId: Int) -> AnyPublisher<VoteDetailResponseObject, APIError>
    func postVote(_ postId: Int, _ object: ChooseRequestObject) -> AnyPublisher<VoteCountsResponseObject, APIError>
    func registerVote(_ object: VoteCreateRequestObject) -> AnyPublisher<Void, APIError>
}

final class VoteDataSource: VoteDataSourceType {

    typealias Target = VoteAPI

    private let provider: NetworkProviderType

    init(provider: NetworkProviderType) {
        self.provider = provider
    }

    func getVotes(_ object: VoteRequestObject) -> AnyPublisher<[PostResponseObject], APIError> {
        provider.requestPublisher(Target.getVotes(object), [PostResponseObject].self)
    }

    func getVoteDetail(_ postId: Int) -> AnyPublisher<VoteDetailResponseObject, APIError> {
        provider.requestPublisher(Target.getVoteDetail(postId), VoteDetailResponseObject.self)
    }

    func postVote(_ postId: Int, _ object: ChooseRequestObject) -> AnyPublisher<VoteCountsResponseObject, APIError> {
        provider.requestPublisher(Target.postVote(postId: postId, requestObject: object), VoteCountsResponseObject.self)
    }

    func registerVote(_ object: VoteCreateRequestObject) -> AnyPublisher<Void, APIError> {
        provider.requestVoidPublisher(Target.registerVote(object))
    }
}
  • NetworkProviderType 으로 주입하여 DIP를 수행
  • MultiTarget 을 사용하면, 아래와 같이 사용할 Target을 명시해 줘야 접근이 가능하다.
    func getVotes(_ object: VoteRequestObject) -> AnyPublisher<[PostResponseObject], APIError> {
    provider.requestPublisher(VoteAPI.getVotes(object), [PostResponseObject].self)
}
  • 따라서 나는 typealiasVoteAPI을 간단히 참조할 수 있도록 했음.
  • 그리고 직접 구현한 provider에 접근하여 request 함수들을 불러오도록 했다.

이렇게 보니까 구현한 건 적지만, 정말 오랜 시간을 고민했던 것 같다.
만약 내가 추후에 수정을 하게 된다면, 어떻게 구현해야 코스트가 가장 적게 들까를 생각하면서 구현하려고 했다. 😄

0개의 댓글

관련 채용 정보