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()
}
}
DIP를 위해 ProviderType
프로토콜을 만들고 의존성을 주입하기로 함.
그리고 공통된 requestPublisher
을 구현하기로!
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>
}
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
을 그대로 사용.
프로토콜을 채택함으로써 준수해야 하는 메서드들에 알맞은 공통된 로직을 구현해 줬다.
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)
}
typealias
로 VoteAPI
을 간단히 참조할 수 있도록 했음.이렇게 보니까 구현한 건 적지만, 정말 오랜 시간을 고민했던 것 같다.
만약 내가 추후에 수정을 하게 된다면, 어떻게 구현해야 코스트가 가장 적게 들까를 생각하면서 구현하려고 했다. 😄