본문 바로가기

iOS

Swift와 RestAPI - Rest? Api? 쉽게 좀 말해주세요...

코드작성에 어느정도 익숙해졌을 때 가장 경계해야 하는 마인드는 타성에 젖게되는 것이라고 생각한다. 처음에는 코드를 작성하기 위한 최소한의 이해만 하고 그렇게 고군분투하여 내가 원하는 것을 만들게 되면, 그 다음부터는 그 코드를 살짝 수정하여 재활용하거나 깃허브에서 서칭해서 내가 원하는 코드만 떼어오고, 그렇게 자연스럽게 작성하는 코드들의 정확한 작동을 잊어버리는 현상들이 일어나기 때문이다.

그래서 오늘은 아무리 코드를 밖에서 긁어오더라도 머리속에 자리잡혀 있어야 하는 RestAPI에 대한 최소한의 지식을 정리해보겠다.

그리고 다음으로는 Swift로 RestAPI 통신을 구현하기 위해 알아야할 지식들을 알아볼 것이다.

 

*본 글은 컴퓨터과학적인 지식을 이해가 쉽도록 풀어서 쓰는 과정에서 다소 비약적인 비유가 포함되어 있을 수 있습니다.

 

1. 서버와 클라이언트

 

나는 어문계 인문학사(통번역) 출신이라 그런지 코딩을 할때 사용하게 되는 영단어들을 왜 그렇게 지었는지에 대한 유추를 항상한다.

Server(서버)는 무엇일까? serve는 일한다는 뜻, 봉사한다는 듯, 섬긴다는 뜻, 제공한다는 뜻을 가지고 있다. 그 뒤에 -(e)r이 붙어있으니 이러한 행동을 하는 사람 혹은 것을 의미할 것이다. 이러한 맥락으로 필자는 service와 가장 비슷한 한국말은 '일'이라고 생각한다.(물론 번역 일을 하였을땐 서비스라고 번역했었다)

 

그래서 server를 컴퓨터적인 어떤 것이라고 생각하지 말고, 그냥 식당 종업원처럼 일하는 사람, 고객이 원하는 무언가를 전해주고, 봉사해주는 사람이라고 생각해보자.

그렇다면 Client는 위의 비유에서 유추할 수 있듯 말그대로 고객이다.

 

위의 비유로 서버와 클라이언트 사이의 통신을 비유하자면 다음과 같다.

고객이 옷가게에 가서 마음에 드는 신발을 사려고 직원에게 요청한다. 그 직원은 창고에 가서 고객이 요청한 신발을 준다.

 

이것을 컴퓨터적으로는 이렇게 이해하면 될 것이다.

클라이언트(웹이나, 모바일 혹은 데스크톱 앱)이 서버에게 데이터를 요청하고. 서버는 DB(데이터 창고)에 가서 요청한 데이터를 가져온다.

 

이를 통해 기억할 수 있는 것은 서버는 서비스(일), DB에 있는 데이터를 관리하고 제공하는 역할을 가지고 있다는 것, 클라이언트는 그것을 요청하고 받는 역할인 점을 알 수 있다.

 

2. RestAPI?

 

처음 개발강의를 보면 api라는 것을 접하게 될텐데, 그렇게 공부하면 api의 모든 것을 알게 된 것과 같은 기분이 든다. 그러고 취업공고나 cs관련 블로그 포스팅을 읽으면 RestAPI, RestfulAPI 라는 것이 있단다... 그럼 Rest가 아닌 것들(Restful하지 않은 것들)도 있겠구나 싶어서 어지럽기 시작하는데, 도대체 형용사 혹은 명사와도 같은 Rest의 의미는 도대체 무엇인지(휴식이란 뜻은 절대 아닐거고..) 싶어서 머리속에서 정리가 안될 것이다. 필자도 그 기분 잘 안다. 천천히 정리해보자. 

 

API?

우선은 API 부터 알아보자. API는 찾아보면 Application Programming Interface의 약자라고 한다. Interface라는 말의 inter은 between의 의미로 '사이, 중간'을 뜻하고, face는 '접하고, 면하는 것'을 뜻해서 interface는 예상과 달리 '의사소통, 연락'이라는 의미도 가지고 있다. 따라서, 완벽하진 않지만 의미가 성립되도록 직역해보자면, API는 응용프로그램(들)의 통신(의사소통)법을 뜻한다.

 

Rest?

그 다음으로 REST는 Representative State Transfer의 약어로, 아키텍쳐인데 이 REST 아키텍쳐 스타일을 따르는 api를 RestAPI RESTful API라고 표현한다. (Rest 약어를 번역해도 밑에 작성될 규칙들과 요소와 크게 유관한 의미가 나오질 않아 넘어가도록 하겠다)

 

다음은 API가 Rest하다고 간주되기 위한 기준들이다

  • 클라이언트, 서버, 리소스, HTTP로 관리되는 요청들로 이루어지는 클라이언트-서버 아키텍쳐
    -  1번에 잘 설명되어있는 클라이언트와 서버가 서로 통신할때 그 요청이 HTTP로 관리가 되어야한다.

  • Stateless(상태가 없는 / (CS) 상태 비저장) 클라이언트-서버 통신. 요청 간에 클라이언트 정보가 저장되지 않으며 각 요청은 개별적이고 연결되어있지가 않아야한다.
    - Stateless의 반대말은 Stateful인데 이는 사용자의 상태를 저장하고 그 이전의 상태를 유지(기억)하는데, Stateless는 이러하지 않아야한다는 것.

  • 클라이언트와 서버 간의 상호 작용을 streamlines(流線적으로 하다, 즉 합리적이고 능률적으로)하는 캐셔블 데이터
    - 캐시(Cache)는 데이터를 임시로 저장하는 저장소인데, 이를 통해  네트워크 대역폭을 절약하고 다음 접근할때 빨라진다. 이 저장소에 저장될 수 있는 데이터가 캐셔블(cacheable) 데이터이다. 

  • ****표준 형태(standard form)로 정보가 전송되도록 통신하는 요소들 간에 통일된 인터페이스를 제공해야한다.
    - 요청된 리소스를 식별할 수 있고 클라이언트에 전송된 표현과 분리되어야한다.

    - 클라이언트가 리소스를 조작할 수 있는 충분한 정보를 포함하고 있기 때문에 리소스는 클라이언트가 수신하는 표현을 통해 조작할 수 있어야 한다.(CRUD)

    - 클라이언트에 반환된 자체 압축 메시지에는 클라이언트가 처리해야 하는 방법을 설명할 수 있는 충분한 정보가 있어야 한다.

    - 하이퍼텍스트/하이퍼스트 미디어를 사용할 수 있다. 즉, 리소스에 접근한 후 클라이언트는 하이퍼링크를 사용하여 현재 수행할 수 있는 다른 모든 작업을 찾을 수 있어야 한다.

  • 각 유형의 서버를 구성하는 계층형 시스템은 클라이언트가 볼 수 없는 계층 구조로 요청된 정보를 검색하는 작업을 포함해야한다.
    - 이건 조금 더 이해가 필요할 것 같다...! ㅜㅜ

위에 내용은 한 번 읽어보면 좋은데, 너무 어렵고 이해 안가도 ****부분이 핵심이기 때문에 코드를 작성하기 위해서는 밑의 내용을 숙지하길 바란다.

 

CRUD?
CRUD는 Creat Read Update Delete로 클라이언트에서 요청하는 작업들을 의미하는데, RestAPI는 일반적으로 다음과 같은 HTTP 메서드를 사용하여 데이터를 요청하고 처리한다. (용어 다른거 저만 불편한가요??ㅡㅅㅡ)

POST: 새로운 리소스를 생성하거나 데이터를 제출 (Create)

GET: 리소스의 조회를 요청 (Read)

PUT: 기존 리소스를 수정 (Update)

DELETE: 리소스를 삭제 (Delete)

 

Curl?
이러한 HTTP 메소드를 사용하여 서버로 요청을 보내고 응답을 받을때 사용되는 명령 줄 도구(URL)이다. 이 URL을 통해 데이터를 가져오거나 보내고, 헤더 정보를 수정하거나 설정할 수 있는 다양한 옵션을 제공한다.

 

etc.
위 내용을 종합해서 생각해본다면 Restful하지 않은 api도 있을 거고, 다른 아키텍쳐를 사용하는 api도 있을 것을 유추할 수 있다. 그 중 대표적으로는 공부하다 보면 접할 수 있는 Socket API이다. 소켓(Socket)은 서버와 클라이언트 사이의 양방향 통신을 가능하게 해주는 인터페이스이다. 그렇다면 우리는 이제 위의 내용들을 숙지하였으니까 Socket API를 보면 이렇게 생각할 수 있어야 한다.
"Socket API는 양방향통신을 가능하게 하는 인터페이스를 사용하는 두 응용프로그램 사이의 통신(법)이겠구나~"

 

3. RestAPI with Swift

 

스위프트로 RestAPI를 이용해 클라이언트-서버 통신을 구현하는 법으로는 URLSession을 이용하거나 Alamofire과 같은 HTTP 네트워킹 라이브러리를 이용할 수 있다.

 

다양한 비동기적인 방법이 있으므로, 각각의 장단점을 선택하여 사용하길 바란다.
다음의 예시 코드들을 참고하면서 어떤 식으로 다른 지만 느껴보길 바란다.
코드에 대한 자세한 설명같은 경우엔 다른 블로그에도 많고, 각각의 글을 써야할 정도이니 따로 설명은 하지 않겠다.

 

(*) Effect(TCA)

TCA 아키텍쳐에서 사용하는 Effect를 통해 비동기 작업을 처리하고 반환하는 코드이다. 다음 코드가 SeoulSalam에 사용된 실제 코드이며, 이 코드를 다음 네 가지 방법으로 같은 방식으로 수행되도록 작성해보았다.

 

import ComposableArchitecture

//API 통신
struct CalendarClient {
    //calendarByCity/2017/4?city=Seoul&country=South%20Korea
    //서울 한정으로 할꺼니까 도시는 고정해놓고 년 월을 받고 day로 원하는 날짜로 인덱스 접근.
    var fetchCalendarItem: (_ yearAndMonth: String, _ day: Int) -> Effect<PrayerTime, Failure>
    
    struct Failure: Error, Equatable {}
}

extension CalendarClient {
    static let live = Self(
        fetchCalendarItem: { yearAndMonth, day in
            Effect.task{
                let (data, _) = try await URLSession.shared
                    .data(from: URL(string:
                                        "http://api.aladhan.com/v1/calendarByCity/\(yearAndMonth)?city=Seoul&country=South%20Korea&method=2")!)
                let result = try JSONDecoder().decode(CalendarResponse.self, from: data)
                
                // 현재 3일이면 인덱스는 2이기 때문에 -1 해준다
                return result.data[day-1].timings
            }
            .mapError { _ in Failure() }
            .eraseToEffect()
        }
    )
}

 

에러코드와 같은 경우엔 임의적으로 수정해서 사용하길 바란다.

 

(1) Completion Closure

 

func fetchCalendarItem(yearAndMonth: String, day: Int, completion: @escaping (Result<PrayerTime, Error>) -> Void) {
    guard let url = URL(string: "http://api.aladhan.com/v1/calendarByCity/\(yearAndMonth)?city=Seoul&country=South%20Korea&method=2") else {
        completion(.failure(NetworkError.invalidURL)))
        return
    }
    
    //컴플리션 핸들러를 이용한 비동기처리
    let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
        if let error = error {
            completion(.failure(error))
            return
        }
        
        guard let data = data else {
            completion(.failure(NetworkError.missingData))
            return
        }
        
        do {
            let result = try JSONDecoder().decode(CalendarResponse.self, from: data)
            let prayerTime = result.data[day - 1].timings
            completion(.success(prayerTime))
        } catch {
            completion(.failure(error))
        }
    }
    
    task.resume()
}

 

(2)  RxSwift

Observable을 직접 생성하는 형태로 작성해 보았습니다. 혹시 틀린 부분 있다면 댓글로 알려주세요.

 

enum NetworkError: Error {
    case invalidURL
    case missingData
}

func fetchCalendarItem(yearAndMonth: String, day: Int) -> Observable<PrayerTime> {
    guard let url = URL(string: "http://api.aladhan.com/v1/calendarByCity/\(yearAndMonth)?city=Seoul&country=South%20Korea&method=2") else {
        return Observable.error(NetworkError.invalidURL)
    }
    
    return Observable.create { observer in
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            if let error = error {
                observer.onError(error)
                return
            }
            
            guard let data = data else {
                observer.onError(NetworkError.missingData)
                return
            }
            
            do {
                let result = try JSONDecoder().decode(CalendarResponse.self, from: data)
                let prayerTime = result.data[day - 1].timings
                observer.onNext(prayerTime)
                observer.onCompleted()
            } catch {
                observer.onError(error)
            }
        }
        
        task.resume()
        
        return Disposables.create {
            task.cancel()
        }
    }
}

 

(3)  Combine

 

import Combine

func fetchCalendarItem(yearAndMonth: String, day: Int) -> AnyPublisher<PrayerTime, Error> {
    guard let url = URL(string: "http://api.aladhan.com/v1/calendarByCity/\(yearAndMonth)?city=Seoul&country=South%20Korea&method=2") else {
        return Fail(NetworkError.invalidURL).eraseToAnyPublisher()
    }
    
    //tryMap을 이용하여 오류발생시 mapError에서 에러 반환
    
    return URLSession.shared.dataTaskPublisher(for: url)
        .tryMap { data, _ in
            return try JSONDecoder().decode(CalendarResponse.self, from: data)
        }
        .map { result in
            return result.data[day - 1].timings
        }
        .mapError { error in
            return error
        }
        .eraseToAnyPublisher()
}

 

(4)  async await

 

func fetchCalendarItem(yearAndMonth: String, day: Int) async throws -> PrayerTime {
    guard let url = URL(string: "http://api.aladhan.com/v1/calendarByCity/\(yearAndMonth)?city=Seoul&country=South%20Korea&method=2") else {
        throw NetworkError.invalidURL
    }
    
    let (data, _) = try await URLSession.shared.data(from: url)
    
    let result = try JSONDecoder().decode(CalendarResponse.self, from: data)
    let prayerTime = result.data[day - 1].timings
    
    return prayerTime
}



참고자료:
https://aws.amazon.com/what-is/restful-api/

https://www.redhat.com/en/topics/api/what-is-a-rest-api