iOS/iOS

[iOS] 라우터 패턴과 Alamofire를 이용해서 API 통신을 해보자.

여성일 2024. 6. 26. 20:04
728x90

iOS 앱 개발에서 API 통신은 필수적인 요소이다. API를 통해 서버와 데이터를 주고 받으며, 이를 통해 다양한 기능을 구현할 수 있다. 그러나 여러 API 요청을 효과적으로 관리하고 유지보수하기 위해서는 체계적인 접근이 필요하다. 이때 유용한 방법 중 하나가 라우터 패턴을 사용하는 것이다.

 

라우터 패턴을 사용하면 각 API 요청을 구조화하여 관리할 수 있다. URL, HTTP Method, Parameter 등을 중앙에서 정의하고 관리할 수 있기 때문에 코드의 일관성과 가독성이 높아진다. 또한, 라우터를 통해 요청을 캡슐화함으로써 네트워크 요청 관련 코드를 재사용할 수 있고, 변경 사항을 쉽게 적용할 수 있다.

 

이 글에서는 Alamofire를 사용하여 라우터 패턴을 적용하는 방법에 대해 다뤄보겠다. Alamofire는 Swift에서 네트워크 요청을 간편하게 처리할 수 있는 라이브러리로, 라우터 패턴과 결합하면 더 효율적이고 유지보수하기 쉬운 네트워크 통신 코드를 작성할 수 있다. 

 

URLRequestConvertible

URLRequestConvertible는 Alamofire 라이브러리에서 사용하는 프로토콜로, URLRequest 객체를 생성하는 데 사용된다. 이 프로토콜을 채택하면 네트워크 요청을 정의하고, 이를 URLRequest 객체로 변환할 수 있다. 이는 API 요청을 구조화하고 관리하는 데 매우 유용하다.

 

URLRequestConvertible 프로토콜은 Alamofire가 네트워크 요청을 처리할 수 있도록, 요청을 URLRequest 객체로 변환하는 역할을 한다. 이 프로토콜은 asURLRequest() 메소드를 요구한다.

 

public protocol URLRequestConvertible {
    func asURLRequest() throws -> URLRequest
}

이 프로토콜을 채택한 객체는 asURLRequest() 메소드를 구현해야 하며, 이 메소드는 네트워크 요청을 표현하는 URLRequest 객체를 반환한다.

 

❓왜 URLRequestConvertible를 사용하는가?

1. 코드의 일관성 : 네트워크 요청을 정의하는 코드가 한 곳에 모이기 때문에 코드의 일관성을 유지할 수 있다.

2. 재사용성 : 공통된 요청 로직을 캡슐화하여 여러 곳에서 재사용할 수 있다.

3. 유지보수성 : 요청의 URL, HTTP Method, Header, Parameter 등을 쉽게 변경할 수 있다.

 

라우터 패턴

라우터 패턴은 네트워크 요청을 관리하는 데 사용되는 디자인 패턴으로, 각 API 요청의 URL, HTTP Method, Parameter, Header 등을 중앙에서 정의하고 관리할 수 있게 한다. 이를 통해 네트워크 요청 코드를 구조화하고 일관성 있게 유지할 수 있다. 라우터 패턴은 주로 RESTful API와 같은 네트워크 요청을 처리하는 데 유용하다.

 

라우터 패턴의 핵심 요소

1. 라우터 정의 : 각 API의 요청을 enum이나 struct로 정의하여, 요청의 URL, HTTPMethod, Parameter 등을 중앙에서 관리한다.

2. URLRequestConvertible : Alamofire와 같은 라이브러리에서는 URLRequestConvertible 프로토콜을 사용하여 각 API 요청을 URLRequest로 변환한다.

3. 중앙집중식 관리 : 모든 네트워크 요청을 한 곳에서 관리함으로써 코드의 일관성과 가독성을 높인다.

 

적용하기

간단한 예제를 위해 네트워크 파일 구조를 설계했다.

struct User: Codable {
    let id: Int
    let name: String
    let username: String
    let email: String
}

서버로부터의 응답을 처리할 모델을 정의한다. 

 

import Alamofire

enum UserRouter {
    case getUsers
    case getUser(id: Int)
    case createUser(name: String, job: String, username: String, email: String)
    case updateUser(id: Int, name: String, job: String, username: String, email: String)
    case deleteUser(id: Int)
}

extension UserRouter: URLRequestConvertible {
    var baseURL: URL {
        guard let url = URL(string: "https://jsonplaceholder.typicode.com") else {
            fatalError("baseURL Error")
        }
        return url
    }
    
    var path: String {
        switch self {
         case .getUsers:
             return "/users"
         case .getUser(let id):
             return "/users/\(id)"
         case .createUser:
             return "/users"
        case .updateUser(let id, _, _, _, _):
             return "/users/\(id)"
         case .deleteUser(let id):
             return "/users/\(id)"
         }
    }
    
    var method: HTTPMethod {
        switch self {
         case .getUsers, .getUser:
             return .get
         case .createUser:
             return .post
         case .updateUser:
             return .put
         case .deleteUser:
             return .delete
         }
    }
    
    var parameters: Parameters? {
        switch self {
        case .getUsers, .getUser, .deleteUser:
            return nil
        case .createUser(let name, let job, let username, let email):
            return ["name": name, "job": job, "username": username, "email": email]
        case .updateUser(_, let name, let job, let username, let email):
            return ["name": name, "job": job, "username": username, "email": email]
        }
    }
    
    func asURLRequest() throws -> URLRequest {
        let url = try baseURL.asURL()
        var urlRequest = URLRequest(url: url.appendingPathComponent(path))
        urlRequest.method = method
        urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
        if let parameters = parameters {
            urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters)
        }
        return urlRequest
    }
}

라우터를 정의하는 첫 번째 단계는 URLRequestConvertible 프로토콜을 준수하는 열거형을 만드는 것이다. 이 enum은 각 API 요청을 정의한다.

 

import Alamofire

class UserService {
    
    static let shared = UserService()
    
    private init() { }
    
    func getUsers(completion: @escaping (Result<[User], Error>) -> Void) {
        AF.request(UserRouter.getUsers).responseDecodable(of: [User].self) { response in
            switch response.result {
            case .success(let users):
                completion(.success(users))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
    
    func getUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
        AF.request(UserRouter.getUser(id: id)).responseDecodable(of: User.self) { response in
            switch response.result {
            case .success(let user):
                completion(.success(user))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
    
    func createUser(name: String, job: String, username: String, email: String, completion: @escaping (Result<User, Error>) -> Void) {
        AF.request(UserRouter.createUser(name: name, job: job, username: username, email: email)).responseDecodable(of: User.self) { response in
                switch response.result {
                case .success(let user):
                    completion(.success(user))
                case .failure(let error):
                    completion(.failure(error))
                }
            }
        }
    
    func updateUser(id: Int, name: String, job: String, username: String, email:String, completion: @escaping (Result<User, Error>) -> Void) {
        AF.request(UserRouter.updateUser(id: id, name: name, job: job, username: username, email: email)).responseDecodable(of: User.self) { response in
                switch response.result {
                case .success(let user):
                    completion(.success(user))
                case .failure(let error):
                    completion(.failure(error))
                }
            }
        }
    
    func deleteUser(id: Int, completion: @escaping (Result<Void, Error>) -> Void) {
            AF.request(UserRouter.deleteUser(id: id)).response { response in
                switch response.result {
                case .success:
                    completion(.success(()))
                case .failure(let error):
                    completion(.failure(error))
                }
            }
        }
}

이제 라우터를 사용하여 API 요청을 수행하는 서비스를 구현한다.

 

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        UserService.shared.getUsers { result in
            switch result {
            case .success(let users):
                print(users)
            case .failure(let error):
                print(error)
            }
        }
        
        UserService.shared.getUser(id: 1) { result in
            switch result {
            case .success(let user):
                print(user)
            case .failure(let error):
                print(error)
            }
        }
        
        UserService.shared.createUser(name: "Seongil", job: "Football Player", username: "Yeo", email: "yeo@seong.il") { result in
            switch result {
            case .success(let user):
                print(user)
            case .failure(let error):
                print(error)
            }
        }
        
        UserService.shared.updateUser(id: 1, name: "Seongil", job: "iOS App Developer", username: "Yeo", email: "yeo@seong.il") { result in
            switch result {
            case .success(let user):
                print(user)
            case .failure(let error):
                print(error)
            }
        }
        
        UserService.shared.deleteUser(id: 1) { result in
            switch result {
            case .success(let user):
                print(user)
            case .failure(let error):
                print(error)
            }
        }
    }
}

이제 UserService를 사용하여 API 요청을 수행할 수 있다. 

 

정상적으로 서버와 통신하는 모습