Skip to content

Commit

Permalink
Merge pull request #14 from f-lab-edu/feature/retry_request
Browse files Browse the repository at this point in the history
네트워크 통신 retry 기능 추가
  • Loading branch information
Peter1119 authored Feb 27, 2024
2 parents 671c5c2 + b199f9e commit 0ccc22c
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ struct TrainingRepository: TrainingRepositoryProtocol {
}

func trainingProgram() async throws -> PullUpProgram {
return try await trainingDataSource.trainingProgram().toDomain()
do {
return try await trainingDataSource.trainingProgram().toDomain()
} catch let error as NetworkError {
switch error {
case .invalidStatusCode, .notReachable, .timeOut:
throw TrainingError.notConnected
case .noData:
throw TrainingError.emptyData
case .unknown:
throw TrainingError.unknown
}
} catch {
throw TrainingError.unknown
}

}
}
22 changes: 11 additions & 11 deletions RaiseMeUp/RaiseMeUp/Sources/Domain/Entities/PullUpProgram.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@

import Foundation

struct PullUpProgram {
let program: [TrainingLevel]
public struct PullUpProgram {
public let program: [TrainingLevel]
}

struct TrainingLevel: Identifiable {
let id: String
let name: String
let description: String
let routine: [DailyRoutine]
public struct TrainingLevel: Identifiable {
public let id: String
public let name: String
public let description: String
public let routine: [DailyRoutine]
}

struct DailyRoutine: Identifiable {
let id: UUID = UUID()
let day: String
let routine: [Int]
public struct DailyRoutine: Identifiable {
public let id: UUID = UUID()
public let day: String
public let routine: [Int]
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@

import Foundation

public enum TrainingError: Error {
case emptyData
case notConnected
case unknown
}

protocol TrainingUseCase {
func getProgramList() async throws -> PullUpProgram
func getProgramList() async -> Result<PullUpProgram, TrainingError>
}
24 changes: 20 additions & 4 deletions RaiseMeUp/RaiseMeUp/Sources/Domain/UseCase/Program/Training.swift
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
//
// Tranining.swift
// Training.swift
// RaiseMeUp
//
// Created by 홍석현 on 11/28/23.
//

import Foundation

struct Training: TrainingUseCase {
public struct Training: TrainingUseCase {

private let repository: TrainingRepositoryProtocol

init(repository: TrainingRepositoryProtocol) {
self.repository = repository
}

func getProgramList() async throws -> PullUpProgram {
return try await repository.trainingProgram()
public func getProgramList() async -> Result<PullUpProgram, TrainingError> {
do {
let program = try await repository.trainingProgram()
guard !program.program.isEmpty else { return .failure(.emptyData) }

return .success(program)
} catch let error as NetworkError {
switch error {
case .invalidStatusCode, .notReachable, .timeOut:
return .failure(.notConnected)
case .noData:
return .failure(.emptyData)
case .unknown:
return .failure(.unknown)
}
} catch {
return .failure(.unknown)
}
}
}
21 changes: 13 additions & 8 deletions RaiseMeUp/RaiseMeUp/Sources/Features/Main/MainViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by 홍석현 on 11/28/23.
//

import Foundation
import UIKit
import OSLog
import Combine

Expand Down Expand Up @@ -37,13 +37,18 @@ final class MainViewModel {
}

func loadData() async -> [TrainingLevel] {
do {
let result = try await useCase.getProgramList()
OSLog.message(.info, "받아온 데이터 \(result)")
return result.program
} catch {
OSLog.message(.error, log: .network, error.localizedDescription)
return []
let result = await useCase.getProgramList()
switch result {
case .success(let data):
return data.program
case .failure(let error):
switch error {
case .emptyData:
OSLog.message(.error, "데이터가 없음")
case .notConnected, .unknown:
OSLog.message(.error, "인터넷 연결이 불안정합니다.\n다시 시도해주세요.")
}
return []
}
}

Expand Down
63 changes: 50 additions & 13 deletions RaiseMeUp/RaiseMeUp/Sources/Network/Provider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ public enum NetworkError: Error {
}

protocol ProviderProtocol {
func request<T: Decodable>(_ urlRequest: URLRequest) async throws -> T
func request<T: Decodable>(
_ urlRequest: URLRequest,
retryCount: Int
) async throws -> T
}

class Provider: ProviderProtocol {
Expand All @@ -26,19 +29,53 @@ class Provider: ProviderProtocol {
self.session = session
}

public func request<T: Decodable>(_ urlRequest: URLRequest) async throws -> T {
let (data, response) = try await session.data(for: urlRequest)

guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
throw NetworkError.invalidStatusCode
public func request<T: Decodable>(
_ urlRequest: URLRequest,
retryCount: Int = 3
) async throws -> T {
var attempts = 0
while attempts < retryCount {
do {
return try await performBasicRequest(urlRequest)
} catch let error as NetworkError {
switch error {
case .timeOut, .notReachable:
attempts += 1
try await Task.sleep(nanoseconds: 2_000_000_000 * UInt64(attempts))
default:
throw error
}
}
}

guard data.isEmpty == false else {
throw NetworkError.noData
throw NetworkError.timeOut
}

private func performBasicRequest<T: Decodable>(_ urlRequest: URLRequest) async throws -> T {
do {
let (data, response) = try await session.data(for: urlRequest)

guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
throw NetworkError.invalidStatusCode
}

guard !data.isEmpty else {
throw NetworkError.noData
}

return try JSONDecoder().decode(T.self, from: data)
} catch let error as URLError {
switch error.code {
case .notConnectedToInternet:
throw NetworkError.notReachable
case .timedOut:
throw NetworkError.timeOut
default:
throw NetworkError.unknown
}
} catch {
throw NetworkError.unknown
}

return try JSONDecoder().decode(T.self, from: data)
}
}
70 changes: 6 additions & 64 deletions RaiseMeUp/RaiseMeUpTests/MainViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,84 +10,26 @@ import Foundation

@testable import RaiseMeUp

let mockTrainingPlan = PullUpProgram(levels: [
TrainingLevel(id: "0", name: "비기너", description: "시작하는 사람들을 위한 운동 플랜", routine: [
DailyRoutine(day: "1일차", program: [3, 2, 2, 1]),
DailyRoutine(day: "2일차", program: [4, 3, 2, 1]),
DailyRoutine(day: "3일차", program: [4, 3, 3, 2]),
DailyRoutine(day: "4일차", program: [5, 3, 3, 2])
]),
TrainingLevel(id: "1", name: "중간자", description: "꽤나 하는 사람들을 위한 운동 플랜", routine: [
DailyRoutine(day: "1일차", program: [10, 8, 8, 5]),
DailyRoutine(day: "2일차", program: [10, 9, 8, 5]),
DailyRoutine(day: "3일차", program: [10, 9, 7, 6]),
DailyRoutine(day: "4일차", program: [10, 9, 8, 7])
]),
TrainingLevel(id: "2", name: "고도자", description: "고수를 위한 운동 플랜", routine: [
DailyRoutine(day: "1일차", program: [20, 15, 15, 10]),
DailyRoutine(day: "2일차", program: [20, 17, 17, 15]),
DailyRoutine(day: "3일차", program: [20, 18, 15, 12]),
DailyRoutine(day: "4일차", program: [20, 18, 18, 15])
])
])


class MockTrainingUseCase: TrainingUseCase {
var getProgramListCalled = false
var mockProgramListResult: PullUpProgram = mockTrainingPlan
var mockData: PullUpProgram

init(mockData: PullUpProgram) {
self.mockData = mockData
}

func getProgramList() async throws -> PullUpProgram {
getProgramListCalled = true
return mockProgramListResult
return mockData
}
}


class MockViewModelTests: XCTestCase {
var viewModel: MainViewModel!
var mockUseCase: MockTrainingUseCase!

override func setUp() {
super.setUp()

mockUseCase = MockTrainingUseCase()
viewModel = MainViewModel(useCase: mockUseCase)

viewModel.loadPrograms()
let expectation = XCTestExpectation(description: "Load programs")

DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
expectation.fulfill()
}
wait(for: [expectation], timeout: 5.0)
}

override func tearDown() {
viewModel = nil
mockUseCase = nil
super.tearDown()
}

func test_데이터를불러오는데성공을한다() async {
// Given
let expectedTrainingPlan = mockTrainingPlan

// When
let trainingPlan = await viewModel.loadData()

XCTAssertTrue(mockUseCase.getProgramListCalled, "프로그램 호출 메서드가 불렸다.")
XCTAssertEqual(trainingPlan.count, expectedTrainingPlan.program.count, "예상하는 데이터가 들어왔다.")
}

func test_섹션의갯수가유즈케이스에있는데이터의갯수와동일하다() {
// Given
let expectedSectionCount = mockTrainingPlan.program.count

// When
let sectionCount = viewModel.numberOfSection()

// Then
XCTAssertTrue(mockUseCase.getProgramListCalled, "프로그램 호출 메서드가 불렸다.")
XCTAssertEqual(expectedSectionCount, sectionCount, "리스트의 섹션 갯수가 동일하게 떨어진다.")
}
}

0 comments on commit 0ccc22c

Please sign in to comment.