ํ‹ฐ์Šคํ† ๋ฆฌ ๋ทฐ

iOS & swift

Clean Architecture Network Module

ggasoon2 2024. 12. 22. 21:40

 

 

 

 

Clean Architecture ๊ธฐ๋ฐ˜ํ•˜์—ฌ

๋„คํŠธ์›Œํฌ๋ฅผ ์ถ”์ƒํ™”, ๋ชจ๋“ˆํ™”ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ •๋ฆฌํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค

 

 

 

 

 

Clean Architecture์—์„œ ๋„คํŠธ์›Œํฌ ๊ณ„์ธต์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ ˆ์ด์–ด๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

 

- Domain Layer: ์•ฑ์˜ ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๊ทœ์น™์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค

- Data Layer: ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์†Œ์Šค(๋„คํŠธ์›Œํฌ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋“ฑ)์™€์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค

- Presentation Layer: UI์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋ฉฐ, ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค

 

 

์ด์ค‘์—์„œ network ๋กœ์ง์€ Data Layer์— ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

 

 

 

 

๋จผ์ € 

๋„คํŠธ์›Œํฌ ์„ธ์…˜์˜ ๋™์ž‘ ๊ตฌํ˜„ํ•˜๋Š” URLSessionWrapper๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

import Foundation

public protocol SessionProtocol {
    func performRequest(_ url: URLRequest) async throws -> (Data, URLResponse)
}

public class URLSessionWrapper: SessionProtocol {
    private let session: URLSession

    public init(session: URLSession = .shared) {
        self.session = session
    }

    public func performRequest(_ url: URLRequest) async throws -> (Data, URLResponse) {
        return try await session.data(for: url)
    }
}

 

 

SessionProtocol๋ฅผ ์ฑ„ํƒํ•˜๋Š” URLSessionWrapper๋ฅผ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ

์ถ”ํ›„์— SessionProtocol๋กœ ๋งŒ๋“  MockSession์œผ๋กœ

URLSessionWrapper๋Œ€์‹  ์‚ฌ์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค

 

 

 

 

 

 

๊ทธ ๋‹ค์Œ ์‹ค์ œ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” NetworkManger ์ž…๋‹ˆ๋‹ค.

import Foundation

public enum NetworkError: Error {
    case invalidURL
    case requestFailed(String)
    case dataNil
    case invalidResponse
    case decodingFailed(String)
    case serverError(Int)
}

public protocol NetworkManagerProtocol {
    func executeRequest<T: Decodable>(url: String,
                                      method: String,
                                      parameters: [String: Any]?) async -> Result<T, NetworkError>
}

public class NetworkManager: NetworkManagerProtocol {
    private let session: SessionProtocol

    public init(session: SessionProtocol) {
        self.session = session
    }

    public func executeRequest<T: Decodable>(url: String,
                                             method: String,
                                             parameters: [String: Any]?) async -> Result<T, NetworkError> {
        guard let url = URL(string: url) else {
            return .failure(.invalidURL)
        }

        var request = URLRequest(url: url)
        request.httpMethod = method
        if let parameters = parameters {
            request.httpBody = try? JSONSerialization.data(withJSONObject: parameters, options: [])
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        }

        do {
            let (data, response) = try await session.performRequest(request)
            guard let httpResponse = response as? HTTPURLResponse else {
                return .failure(.invalidResponse)
            }

            if (200..<300).contains(httpResponse.statusCode) {
                let decodedData = try JSONDecoder().decode(T.self, from: data)
                return .success(decodedData)
            } else {
                return .failure(.serverError(httpResponse.statusCode))
            }
        } catch {
            return .failure(.requestFailed(error.localizedDescription))
        }
    }
}

 

 

SessionProtocol์„ ์˜์กด์„ฑ์œผ๋กœ ์ฃผ์ž… ๋ฐ›์•„์„œ

์‹ค์ œ HTTP ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” NetworkManager ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค

NetworkManagerProtocol์„ ํ†ตํ•ด ์ถ”์ƒํ™”๋˜์–ด ์žˆ์–ด Mock ๊ฐ์ฒด๋กœ ๋Œ€์ฒด๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค

 

๋˜ Generic์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์–‘ํ•œ return ๊ฐ’์— ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค

 

 

 

 

 

 

๊ทธ๋ฆฌ๊ณ 

์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๋กœ์ง UserNetwork๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

import Foundation

public final class UserNetwork {
    private let manager: NetworkManagerProtocol

    public init(manager: NetworkManagerProtocol) {
        self.manager = manager
    }

    public func fetchUsers(query: String, page: Int) async -> Result<[User], NetworkError> {
        let url = "https://api.github.com/search/users?q=\(query)&page=\(page)"
        return await manager.executeRequest(url: url, method: "GET", parameters: nil)
    }
}

public struct User: Decodable {
    let id: Int
    let login: String
}

 

 

 

 

 

์‹ค์ œ ํ˜ธ์ถœ์—์„œ๋Š”

URLSession์„ ๊ตฌํ˜„ํ•˜๋Š” URLSessionWrapper() ์„ ์–ธํ•˜๊ณ 

๊ทธ๋ฆฌ๊ณ  session์„ ์‹ค์ œ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” NetworkManager์˜ session๊ฐ’์œผ๋กœ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค

 

 

 

๋งˆ์ง€๋ง‰์œผ๋กœ ํ˜ธ์ถœ url๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” UserNetwork ํด๋ž˜์Šค๋ฅผ ์ดˆ๊ธฐํ™”ํ•ด์ค๋‹ˆ๋‹ค

let session = URLSessionWrapper() // URLSession์„ ์‚ฌ์šฉํ•˜๋Š” ์„ธ์…˜ ๊ตฌํ˜„์ฒด
let networkManager = NetworkManager(session: session) // NetworkManager๋ฅผ ๊ตฌ์„ฑ
let userNetwork = UserNetwork(manager: networkManager) // UserNetwork ์ดˆ๊ธฐํ™”

 

 

 

 

 

๊ทธ๋ฆฌ๊ณ  userNetwork ์˜ fetchUsers ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ api๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

Task {
    let query = "john"
    let page = 1

    let result = await userNetwork.fetchUsers(query: query, page: page)
    switch result {
    case .success(let users):
        print("๊ฒ€์ƒ‰๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก: \(users)")
    case .failure(let error):
        print("์—๋Ÿฌ ๋ฐœ์ƒ: \(error)")
    }
}

 

 

 

 

 

๊ทธ ๋‹ค์Œ์€ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

 

 

 

 

 

Session Protocol์„ ์ฑ„ํƒํ•˜๋Š” MockSession์„ ์ƒ์„ฑํ•ด์ฃผ๊ณ 

import Foundation

public class MockSession: SessionProtocol {
    private let mockData: Data
    private let mockResponse: URLResponse

    public init(mockData: Data, mockResponse: URLResponse) {
        self.mockData = mockData
        self.mockResponse = mockResponse
    }

    public func performRequest(_ url: URLRequest) async throws -> (Data, URLResponse) {
        return (mockData, mockResponse)
    }
}

 

mockData, mockResponse๋ฅผ returnํ•˜๋Š” performRequest๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

 

 

 

 

 

๊ทธ๋ฆฌ๊ณ  ํ…Œ์ŠคํŠธ์—์„œ MockSession์„ ์‚ฌ์šฉํ•ด ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•ฉ๋‹ˆ๋‹ค.

import XCTest

final class UserNetworkTests: XCTestCase {
    func testFetchUsersSuccess() async {
        // Mock ๋ฐ์ดํ„ฐ ์„ค์ •
        let mockUserData = """
        [
            {"id": 1, "login": "user1"},
            {"id": 2, "login": "user2"}
        ]
        """.data(using: .utf8)!
        let mockResponse = HTTPURLResponse(url: URL(string: "https://api.github.com")!,
                                           statusCode: 200,
                                           httpVersion: nil,
                                           headerFields: nil)!

        // MockSession ์ƒ์„ฑ
        let mockSession = MockSession(mockData: mockUserData, mockResponse: mockResponse)
        let networkManager = NetworkManager(session: mockSession)
        let userNetwork = UserNetwork(manager: networkManager)

        // ํ…Œ์ŠคํŠธ ์‹คํ–‰
        let result = await userNetwork.fetchUsers(query: "test", page: 1)
        switch result {
        case .success(let users):
            XCTAssertEqual(users.count, 2)
            XCTAssertEqual(users.first?.login, "user1")
        case .failure(let error):
            XCTFail("Test failed with error: \(error)")
        }
    }

    func testFetchUsersFailure() async {
        // Mock ๋ฐ์ดํ„ฐ ์„ค์ •
        let mockResponse = HTTPURLResponse(url: URL(string: "https://api.github.com")!,
                                           statusCode: 500,
                                           httpVersion: nil,
                                           headerFields: nil)!

        // MockSession ์ƒ์„ฑ
        let mockSession = MockSession(mockData: Data(), mockResponse: mockResponse)
        let networkManager = NetworkManager(session: mockSession)
        let userNetwork = UserNetwork(manager: networkManager)

        // ํ…Œ์ŠคํŠธ ์‹คํ–‰
        let result = await userNetwork.fetchUsers(query: "test", page: 1)
        switch result {
        case .success:
            XCTFail("Test should have failed")
        case .failure(let error):
            XCTAssertEqual(error, .serverError(500))
        }
    }
}

 

 

 

Clean Architecture ๊ธฐ๋ฐ˜ํ•˜์—ฌ ๋„คํŠธ์›Œํฌ ์ถ”์ƒํ™” ๊ทธ๋ฆฌ๊ณ  ํ…Œ์ŠคํŠธ ๊นŒ์ง€ ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค:)

๋Œ“๊ธ€