Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#84] Concurrency를 사용하여 앱에서 사용하는 네트워크 공통 로직 구현 #85

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from

Conversation

seungchan2
Copy link
Contributor

@seungchan2 seungchan2 commented Jan 8, 2025

🔍 What is this PR?

  • RxMoya 코드를 Concurrency로 리팩토링 진행하였습니다.
  • 여러 부분을 개선아닌 개선을 진행하면서 자세히 봐야하는 부분 먼저 말씀드리겠습니다 ㅎㅎ.. 좋은 방법있으면 더 말씀해주세요 :)

📝 Changes

1. URLSession -> 프로토콜 기반 설계

  • RxSwift 기반 네트워크 코드를 URLSession으로 바꾸면서 모든 코드가 한 곳에 모여 역할 분리가 되지 않는다는 문제점을 고민했습니다.
  • 아래는 URLSession의 일반적인 코드입니다.
func fetchData() {
       let url = URL(string: "테스트")!
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
       1. 에러 처리
       guard error == nil else { 
           return 
       }
       
       // 2. 응답 처리
       guard let httpResponse = response as? HTTPURLResponse,
             (200...299).contains(httpResponse.statusCode) else {
           상태 코드 처리
           return
       }
       
       3. 데이터 파싱
       do {
           let result = try JSONDecoder().decode(Response.self, from: data!)
           성공 처리
       } catch {
           디코딩 에러 처리
       }
   }
   task.resume()
   }
func request<T: Decodable>(
       _ endpoint: Endpoint,
       type: T.Type
   ) async throws -> T {
       let request = try requestBuilder.buildRequest(from: endpoint)
       let (data, response) = try await session.data(for: request)
       return try processResponse(data: data, response: response, type: type)
   }
  • 하나의 메서드에서 너무 많은 역할을 수행하고, 테스트가 어렵다는 문제점이 있었습니다.
  • 그래서 네트워크 통신, 응답 처리, 요청 생성 부분을 추상화했습니다. NetworkSession ResponseDecoding ModernURLRequestBuildable
  • 해당 구조의 장점은 일단 독립적인 테스트가 가능하고, 각 역할이 명확하게 분리되는 것 같습니다. (마이그레이션 하면서 와닿을 것 같습니다..!)

2. 여러 시나리오를 고려한 네트워크 메소드 설계

  • ModernNetworkService.swift 파일을 보면 Networking 프로토콜 안에 5개의 메소드가 있습니다.

  • 여러가지 네트워크 시나리오에 따라서 다음과 같이 나눴습니다.

    1. 기본 네트워크 요청 수행
    2. 취소 가능한 네트워크 요청 수행
    3. 진행률을 포함한 네트워크 요청 수행
    4. 재시도 네트워크 요청 수행
    5. 타임아웃이 필요한 네트워크 요청 수행
  • 기본 네트워크는 request 메소드를 사용하면 되고, 취소 가능한 네트워크 요청은 Concurrency가 GCD 코드보다 취소에 장점이 있어 사용자가 네트워크 통신 중, 이탈하는 경우 등 다양한 엣지 케이스를 고려하여 일단 ㅎㅎ.. 5가지를 만들어 뒀습니다!

  • 내부 코드는 온라인 / 오프라인에서 설명드릴게요!

3. 에러 상태 정립 필요

  • 2년만에 다시 하려고 하니까 헷갈려요 ..
  • 일단 기존 코드 참고해서 에러 코드를 세분화했습니다.
  • 기존에는 열거형이 Int 타입으로, 에러 처리를 따로 해줬어야 했는데 지금은 Error를 채택해서 한번에 처리해주는 방법으로 수정해뒀습니다.
  • 좋은 의견 있으면 말씀주십시오.

4. actor를 사용한 네트워크 구현

  • 짧은 시간 내 여러 번의 네트워크 호출, 한 화면에 여러 API 사용, 취소나 타임아웃 처리 시 발생할 수 있는 Data race를 피하기 위해 actor로 구현했습니다.
  • actor 관련 WWDC 첨부했습니다요!
    https://developer.apple.com/videos/play/wwdc2021/10133/

5. 사용법

  • 기존 Moya 사용할 때와 비슷하게 사용하시면 됩니다!
  • 다만 문제점이 있다면 return값에 Observable 방출을 해야해서,, onNext를 통해서 방출하거나 아예 Rx를 걷어내는 것도 .. 좋은 방법일수도 ... 다음 이슈에서 기존 API 수정해둘겝쇼
   let task = Task {
            do {
                let response = try await self.networkClient.request(
               EndPoint.fetch,
               type: Response.self
   )
}
스크린샷 2025-01-08 오후 3 27 40

📮 관련 이슈

@seungchan2 seungchan2 added the Network 서버통신 작업 label Jan 8, 2025
@seungchan2 seungchan2 self-assigned this Jan 8, 2025
@seungchan2 seungchan2 requested a review from jane1choi January 8, 2025 08:59
Copy link
Member

@jane1choi jane1choi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트에 대한 언급을 pr에서 언급해줘서 테스트 관련해서 코멘트 달아보자면...

protocol URLSessionProtocol {
    func data(for request: URLRequest) async throws -> (Data, URLResponse)
    func bytes(for request: URLRequest) async throws -> (URLSession.AsyncBytes, URLResponse)
}

extension URLSession: URLSessionProtocol { }
class MockURLProtocol: URLProtocol {
    typealias RequestHandler: ((URLRequest) throws -> (Data?, URLResponse?, Error?))?
    static var requestHandler: RequestHandler?

    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    override func startLoading() {
        guard let handler = MockURLProtocol.requestHandler else {
            fatalError("Request handler is not set.")
        }

        do {
            let (data, response, error) = try handler(request)
            if let response = response {
                client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            }
            if let data = data {
                client?.urlProtocol(self, didLoad: data)
            }
            if let error = error {
                client?.urlProtocol(self, didFailWithError: error)
            } else {
                client?.urlProtocolDidFinishLoading(self)
            }
        } catch {
            client?.urlProtocol(self, didFailWithError: error)
        }
    }

    override func stopLoading() {
        // 요청 취소 시 ...
    }
}

final class MockURLSession {
    private let urlSession: URLSession = {
        let configuration = URLSessionConfiguration.ephemeral
        configuration.protocolClasses = [MockURLProtocol.self]
        return URLSession(configuration: configuration)
    }()

    private let isFailRequest: Bool
    private let successMockData: Data

    private let successStatusCode = 200
    private let failureStatusCode = 401

    init(isFailRequest: Bool = false, successMockData: Data) {
        self.isFailRequest = isFailRequest
        self.successMockData = successMockData
    }

    func data(for url: URL) async throws -> (Data, URLResponse) {
        let successResponse = HTTPURLResponse(
            url: url,
            statusCode: successStatusCode,
            httpVersion: "2",
            headerFields: nil
        )
        let failureResponse = HTTPURLResponse(
            url: url,
            statusCode: failureStatusCode,
            httpVersion: "2",
            headerFields: nil
        )

        let response = isFailRequest ? failureResponse : successResponse
        let data = isFailRequest ? nil : successMockData

        MockURLProtocol.requestHandler = { _ in
            return (data, response, nil)
        }

        return try await urlSession.data(for: URLRequest(url: url))
    }
}

이런식으로 네트워크 요청 성공, 실패 케이스 테스트를 위한 Mock 객체를 만들어두고

let mockData = """
{ "message": "Success" }
""".data(using: .utf8)!

let mockSession = MockURLSession(isFailRequest: false, successMockData: mockData)

let url = URL(string: "https://example.com")!

Task {
    do {
        let (data, response) = try await mockSession.data(for: url)
        if let response = response as? HTTPURLResponse {
            print("Status Code: \(response.statusCode)")
        }
        print("Data: \(String(data: data, encoding: .utf8) ?? "")")
    } catch {
        print("Error: \(error)")
    }
}

위와 같이 테스트 할 수 있도록 해도 좋을 것 같습니당! (특히나 지금 서버가 닫혀 있기 때문에...ㅎ)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Network 서버통신 작업
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Refactor] 네트워크 코드 수정
2 participants