Alamofire Document를 번역한 글 입니다.

(오타 오역이 많을 수 있습니다)

소개

Alamofire은 HTTP 네트워크 요청에 대한 우아하고 구성 가능한 인터페이스를 제공합니다. 자체 HTTP 네트워킹 기능을 구현하지 않습니다. 대신 Foundation 프레임워크에서 제공하는 Apple의 URL Loading System을 기반으로 구축됩니다.이 시스템의 핵심은 URLSession 및 URLSessionTask 하위 클래스 라는 것 입니다. Alamofire은 이러한 API와 기타 여러 API를 사용하기 쉬운 인터페이스로 래핑하고 HTTP 네트워킹을 사용하는 최신 애플리케이션 개발에 필요한 다양한 기능을 제공합니다. 그러나 Alamofire의 핵심 동작 중 많은 부분이 어디에서 나오는지 아는 것이 중요하므로 URL Loading System에 대해 잘 알고 있는 것이 중요합니다. 궁극적으로 Alamofire의 네트워킹 기능은 해당 시스템의 기능에 따라 제한되며 동작과 모범사례는 항상 기억하고 준수해야합니다.

또한 Alamofire(및 일반적인 URL Loading System)의 네트워킹은 비동기적으로 수행됩니다. 비동기 프로그램은 이 개념에 익숙하지 않은 프로그래머에게는 좌절할 수 있지만 이런방식으로 하는데는 아주 좋은 이유가 있습니다.

 

Aside: The AF Namespace and Reference

Alamofire 설명서의 이전버전에는 Alamofire.request()와 같은 예시를 사용하였습니다. 이 API는 Alamofire 접두사가 필요한것처럼 보이지만 실제로 Alamofire접두사 없이도 잘 작동하였습니다. 요청방법 및 기타기능들은 import Alamofire 가 있는 모든 파일에서 전역적으로 사용 가능했었습니다. Alamofire 5 시작되면서 이 기능은 제거되고 대신에 AF 전역은 Session.defualt에 대한 참조입니다.(AF. 접두사를 사용하여 Alamofire의 기능을 전역으로 사용한다.) 이를 통해 Alamofire은 사용할 때마다 전역 네임스페이스를 오염시키지않고  전역적으로 Session API를 복제할 필요 없이 동일한 편의 기능을 제공할 수 있습니다. 마찬가지로 Alamofire에 의해 확장된 유형은 af 속성 확장을 사용하여 Alamofire가 추가하는 기능을 다른 확장들과 분리 합니다.

 

Making Requests

Alamofire HTTP Request 만드는 다양한 편의 방법을 제공합니다. 가장 간단하게는 URL 변환할  있는 String 제공하기만 하면 됩니다.

AF.request("https://httpbin.org/get").response { response in
    debugPrint(response)
}

- 모든 예제는 import Alamofire 소스 파일 어딘가에 있어야합니다.

 

이것은 실제로 Alamofire's Session type에서 요청을 만드는 두 가지 최상위 API중 한 형태 입니다. 전체 정의는 다음과 같습니다.

open func request<Parameters: Encodable>(_ convertible: URLConvertible,
                                         method: HTTPMethod = .get,
                                         parameters: Parameters? = nil,
                                         encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default,
                                         headers: HTTPHeaders? = nil,
                                         interceptor: RequestInterceptor? = nil) -> DataRequest

이 방법은 method  headers 같은 개별 구성 요소의 요청을 구성하는 것을 허용하는 동시에 요청별 RequestInterceptor및 Encodable 매개변수를 허용하는 DataRequest을 생성합니다.

- 추가적으로 Parameters dictionaries 와 ParameterEncoding types 을 사용하여 요청을 구성하는 방법이 있습니다. 이 API는 더이상 권장되지 않으며 Alamofire에서 사용되지 않고 제거될 예정입니다.

 

이 API의 두 번째 버전은 훨씬 간단합니다.

open func request(_ urlRequest: URLRequestConvertible, 
                  interceptor: RequestInterceptor? = nil) -> DataRequest

이 방법은 Alamofire의 URLRequestConvertible 프르토콜을 따르는 모든 유형에 대해 DataRequest를 생성합니다. 이전 버전의 모든 다른 매개변수는 해당 값에 캡슐화되어 매우 강력한 추상화를 생성할 수 있습니다. 

 

HTTP Method

이 HTTPMethod 유형은 RFC 7231 에 정의된 HTTP Method 를 나열합니다.

public struct HTTPMethod: RawRepresentable, Equatable, Hashable {
    public static let connect = HTTPMethod(rawValue: "CONNECT")
    public static let delete = HTTPMethod(rawValue: "DELETE")
    public static let get = HTTPMethod(rawValue: "GET")
    public static let head = HTTPMethod(rawValue: "HEAD")
    public static let options = HTTPMethod(rawValue: "OPTIONS")
    public static let patch = HTTPMethod(rawValue: "PATCH")
    public static let post = HTTPMethod(rawValue: "POST")
    public static let put = HTTPMethod(rawValue: "PUT")
    public static let query = HTTPMethod(rawValue: "QUERY")
    public static let trace = HTTPMethod(rawValue: "TRACE")

    public let rawValue: String

    public init(rawValue: String) {
        self.rawValue = rawValue
    }
}

이러한 값들은 AF.request API의 method인자로 전달할 수 있습니다.

AF.request("https://httpbin.org/get")
AF.request("https://httpbin.org/post", method: .post)
AF.request("https://httpbin.org/put", method: .put)
AF.request("https://httpbin.org/delete", method: .delete)

다양한 HTTP Method 는 서버에 따라 다른 의미를 가질 수 있고 다른 매개변수 인코딩이 필요할 수 있다는 점을 기억하는 것이 중요합니다. 예를 들어, GET 요청에서 본문데이터를 전달하는 것은 Alamofire와 URLSession에서 지원되지 않으며 오류를 반환합니다.

Alamofire은 또한 String을 HTTPMethod 값으로 반환하는 httpMethod 속성을 연결하기 위해 URLRequest에 대한 확장 기능을 제공합니다.

extension URLRequest {
    /// Returns the `httpMethod` as Alamofire's `HTTPMethod` type.
    public var method: HTTPMethod? {
        get { httpMethod.flatMap(HTTPMethod.init) }
        set { httpMethod = newValue?.rawValue }
    }
}

 

Alamofire의 HTTPMethod 유형이 지원하지 않는 HTTP Method 를 사용해야 하는 경우 유형을 확장하여 사용자 정의 값을 추가할 수 있습니다.

extension HTTPMethod {
    static let custom = HTTPMethod(rawValue: "CUSTOM")
}

AF.request("https://httpbin.org/headers", method: .custom)

 

 

Setting Other URLRequest Properties

Alamofire의 요청 생성 방법은 사용자 정의를 위한 가장 일반적인 매개변수를 제공하지만 때로는 이것만으로는 충분하지 않을 수 있습니다. 전달된 값에서 생성된 URLRequest 들은 요청을 생성할 때 RequestModifier 의 클로저를 사용하여 수정할 수 있습니다. 예를 들어, URLRequest timeoutInterval을 5초로 설정하려면 클로저에서 요청을 수정합니다.

AF.request("https://httpbin.org/get", requestModifier: { $0.timeoutInterval = 5 }).response(...)

RequestModifier는 후행 클로저 구문에도 적용됩니다.

AF.request("https://httpbin.org/get") { urlRequest in
    urlRequest.timeoutInterval = 5
    urlRequest.allowsConstrainedNetworkAccess = false
}
.response(...)

RequestModifier는 오직 URL및 기타 개별 요소들을 사용하여 생성된 요청에만 적용되고, URLRequsetConvertible로 직접 생성된 값에는 적용되지 않습니다. 이러한 값은 모든 매개변수 자체를 설정할 수 있어야 하기 때문에 적용이 되지 않습니다. 또한 생성중에 대부분의 요청을 수정해야 할 필요가 있다면 URLRequestConvertible을 채택하는 것을 추천합니다.

 

 

Request Parameters and Parameter Encoders

Alamofire는 request의 매개변수로 모든 Encodable유형을 전달하는것을 지원합니다. 이러한 매개변수는 ParameterEncoder 프로토콜을 준수하는 유형을 통해 전달되고 URLRequest에 추가된 다음 네트워크를 통해 전송됩니다. Alamofire는 두가지 ParameterEncoder 채택 유형을 포함: JSONParameterEncoder, URLEncodedFormParameterEncoder. 이러한 유형들은 최신 서비스에서 사용하는 가장 일반적인 인코딩을 포함합니다.

struct Login: Encodable {
    let email: String
    let password: String
}

let login = Login(email: "test@test.test", password: "testPassword")

AF.request("https://httpbin.org/post",
           method: .post,
           parameters: login,
           encoder: JSONParameterEncoder.default).response { response in
    debugPrint(response)
}

 

URLEncodedFormParameterEncoder

URLEncodedFormParameterEncoder는 값을 URL 인코딩 문자열로 인코딩하여 기존 URL 쿼리 문자열로 설정,추가 또는 요청의 HTTP 본문으로 설정합니다. 인코딩된 문자열이 설정되는 위치를 제어하려면 인코딩의 destination을 설정합니다. URLEncodedFormParameterEncoder.Destination 열거형은 세가지 경우가 있습니다.

  • .methodDependent: 인코딩된 쿼리 문자열 결과를 .get, .head .delete 요청에 대한 기존 쿼리 문자열에 적용하고 다른 HTTP 메서드를 사용하는 요청에 대해서는 이를 HTTP 본문으로 요청하고 설정합니다.
  • .queryString: 요청 URL의 쿼리에 인코딩된 문자열을 설정하거나 추가합니다.
  • .httpBody: 인코딩된 문자열을 URLRequest 의 HTTP Body로 설정합니다.

HTTP body가 포함된 인코딩된 요청의 Content-Type HTTP Header는 application/x-www-form-urlencoded로 설정 됩니다. Content-Type이 아직 설정되지 않은 경우 charset=utf-8입니다.

내부적으로 URLEncodedFormParameterEncoder는 URLEncodedFormEncoder를 사용하여 Encodable유형에서 URL 인코딩된 형식 문자열로 실제 인코딩을 수행합니다. 이 Encoder는 다양한 유형에 대한 인코딩을 사용자 정의하여 사용할 수 있습니다: ArrayEncoding -> Array || BoolEncoding -> Bool || DataEncoding -> Data || DateEncoding -> Date

KeyEncoding -> coding keys || SpaceEncoding -> spaces

 

GET Request With URL-Encoded Parameters

let parameters = ["foo": "bar"]

// All three of these calls are equivalent
AF.request("https://httpbin.org/get", parameters: parameters) // encoding defaults to `URLEncoding.default`
AF.request("https://httpbin.org/get", parameters: parameters, encoder: URLEncodedFormParameterEncoder.default)
AF.request("https://httpbin.org/get", parameters: parameters, encoder: URLEncodedFormParameterEncoder(destination: .methodDependent))

// https://httpbin.org/get?foo=bar

 

POST Request With URL-Encoded Parameters

let parameters: [String: [String]] = [
    "foo": ["bar"],
    "baz": ["a", "b"],
    "qux": ["x", "y", "z"]
]

// All three of these calls are equivalent
AF.request("https://httpbin.org/post", method: .post, parameters: parameters)
AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: URLEncodedFormParameterEncoder.default)
AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: URLEncodedFormParameterEncoder(destination: .httpBody))

// HTTP body: "qux[]=x&qux[]=y&qux[]=z&baz[]=a&baz[]=b&foo[]=bar"

 

Configuring the Sorting of Encoded Values

Swift 4.2부터 Swift의 Dictionary유형에서 사용하는 해싱 알고리즘은 런타임에 앱 시작 간에 다른 무작위 내부 순서를 생성합니다. 이로 인해 인코딩된 매개변수가 순서가 변경되어 캐싱 및 기타 동작에 영향을 미칠 수 있습니다. 기본적으로 URLEncodedFormEncoder는 인코딩된 키-값 쌍을 정렬합니다. 이는 모든 Encodable유형에 대해 일정한 출력을 생성하지만 유형에 의해 구현된 실제 인코딩 순서와 일치하지 않을 수 있습니다. AlphabetizeKeyValuePairs를 false로 설정하여 구현 순서로 돌아갈 수 있지만 사전 순서도 무작위로 지정됩니다.

 

사용자 지정 URLEncodedFormparameterEncoder를 생성할 수 있고 전달된 URLEncodedFormEncoder의 초기화과정에서 원하는 alphabetizeKeyValuePairs을 지정할 수 있습니다.

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(alphabetizeKeyValuePairs: false))

 

Configuring the Encoding of Array Parameters

컬렉션 유형을 인코딩하는 방법에 대해 알려진 사양이 없기 때문에 기본적으로 Alamofire는 배열 값의 키에 []를 추가하고(foo[]=1&foo[]=2), 중첩된 사전 값의 경우 대괄호로 묶인 키를 추가하는 규칙을 따릅니다. (foo[bar]=baz)

URLEncodedFormEncoder.ArrayEncoding 열거형은 Array 매개변수를 인코딩하기 위해 다음과 같은 방법을 제공합니다.

  • .brackets: 모든 값에 대해 빈 대괄호 세트가 키에 추가됩니다. (기본값)
  • .noBrackets: 괄호는 추가되지않고 키는 그대로 인코딩됩니다.

기본적으로 Alamofire는 .Brackets로 인코딩합니다. foo = [1, 2]가 foo[]=1&foo[]=2로 인코딩됩니다.

.noBracktes로 한다면 foo =[1, 2] 가 foo=1&foo=2로 인코딩됩니다.

 

또한 사용자 지정 URLEncodedFormParameterEncoder를 생성하고 전달된 URLEncodedFormEncoder의 생성과정에서 원하는 ArrayEncoding을 지정할 수 있습니다. 

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(arrayEncoding: .noBrackets))

 

Configuring the Encoding of Bool Parameters

URLEncodedFormEncoder.BoolEncoding 열거형은 Bool매개변수를 인코딩하기 위해 다음과 같은 방법을 제공합니다.

  • .numeric: true를 1, false를 0으로 인코딩합니다. (기본값)
  • .literal: true와 false를 문자열 리터럴로 인코딩합니다.

기본적으로 Alamofire는 .numberic 인코딩을 사용합니다.

 

또한 사용자 지정 URLEncodedFormParameterEncoder를 생성하고 전달된 URLEncodedFormEncoder의 생성과정에서 원하는 BoolEncoding을 지정할 수 있습니다. 

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(boolEncoding: .numeric))

 

Configuring the Encoding of Data Parameters

DataEncoding은 Data 매개변수를 인코딩하기 위해 다음과 같은 방법들을 제공합니다.

  • .deferredToData: Data의 기본 Encodable 지원을 사용합니다.
  • .base64: Data를 Base 64로 인코딩된 문자열로 인코딩합니다. (기본값)
  • .custom((Data) -> throws -> String): Data를 주어진 클로저를 인코딩합니다.

또한 사용자 지정 URLEncodedFormParameterEncoder를 생성하고 전달된 URLEncodedFormEncoder의 생성과정에서 원하는 DataEncoding을 지정할 수 있습니다. 

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(dataEncoding: .base64))

 

Configuring the Encoding of Date Parameters

Date를 String으로 인코딩하는 방법은 너무 많기 때문에 DateEncoding에는 Date 매개변수를 인코딩 하기위한 다음 방법들을 제공합니다.

  • .deferredToDate: Date의 기본적인 Encodable기능들을 사용합니다. (기본값)
  • .secondsSince1970: Date를 1970년 1월 1일 UTC 자정 이후의 초로 인코딩합니다.
  • .millisecondsSince1970: Date를 1970년 1월 1일 UTC 자정 이후의 밀리초로 인코딩합니다.
  • .iso8601: ISO8601 및 RFC3339 표준에 따라 인코딩합니다.
  • .formatted(DateFormatter): 주어진 DataFormatter를 사용하여 인코딩합니다
  • .custom((Date) throws -> String) : 주어진 클로저를 사용하여 인코딩합니다.

또한 사용자 지정 URLEncodedFormParameterEncoder를 생성하고 전달된 URLEncodedFormEncoder의 생성과정에서 원하는 DateEncoding을 지정할 수 있습니다. 

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(dateEncoding: .iso8601))

 

Configuring the Encoding of Coding Keys

매개변수 키 스타일이 다양하기 때문에 KeyEncoding은 lowerCamelCase의 키에서 키 인코딩을 사용자 정의하는 다음 방법을 제공합니다.

  • .useDefaultKeys: 각 유형에서 지정한 키를 사용합니다. (기본값)
  • .convertToSnakeCase: 키를 스네이크 케이스로 변환합니다. oneTwoThree -> one_two_three
  • .convertToKebabCase: 키를 케밥케이스로 변환합니다. oneTwoThree -> one-two-three
  • .capitalized: 첫글자만 대문자로 변환합니다.(UpperCamelCase) oneTwoThree -> OneTwoThree 
  • .uppercased: 모든 글자를 대문자로 변환합니다. oneTwoThree -> ONETWOTHREE
  • .lowercased: 모든 글자를 소문자로 변환합니다: oneTwoThree -> onetwothree
  • .custom((String) -> String): 주어진 클로저를 사용하여 인코딩합니다.

또한 사용자 지정 URLEncodedFormParameterEncoder를 생성하고 전달된 URLEncodedFormEncoder의 생성과정에서 원하는KeyEncoding을 지정할 수 있습니다. 

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(keyEncoding: .convertToSnakeCase))

 

 

Configuring the Encoding of Object Key Paths

Nest 객체 키 경로는 일반적으로 괄호를 사용하여 인코딩됩니다. Alamofire의 KeyPathEncoding은 해당 동작을 사용자 정의하기위해 룰을 제공합니다.

  • .brackets - 키 경로의 각 하위 키를 괄호로 묶습니다. parent[child][grandchild]
  • .dots - 키 경로의 각 하위 키를 점으로 구분합니다. parent.child.grandchild

또한 사용자 정의 인코딩 클로저로 인스턴스를 생성하여 고유한 인코딩을 생성할 수 있습니다. 예를 들어, KeyPathEncoding {"-\($0)"}은 각 하위 키 경로를 하이픈으로 구분합니다.(ex: parent-child-grandchild)

사용자 지정 URLEncodedFormParameterEncoder를 생성하고 전달된 URLEncodedFormEncoder의 생성과정에서 원하는KeyPathEncoding을 지정할 수 있습니다. 

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(keyPathEncoding: .brackets))

 

 

Configuring the Encoding of Spaces

이전 형식의 인코더는 공백을 인코딩하는 데 +가 사용되었으며 일부 서버는 최신 퍼센트 인코딩 대신 이 인코딩을 여전히 사용하므로 Alamofire에는 공백을 인코딩하기 위해 다음 방법이 포함되어 있습니다.

  • .percentEscaped: 표준 퍼센트 이스케이프를 적용하여 공백문자를 인코딩합니다. " "이"%20"으로 인코딩됩니다. (기본값)
  • .plusReplaced: 공백 문자를 +로 대체되어 인코딩합니다. " " -? "+"

사용자 지정 URLEncodedFormParameterEncoder를 생성하고 전달된 URLEncodedFormEncoder의 생성과정에서 원하는SpaceEncoding을 지정할 수 있습니다.

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(spaceEncoding: .plusReplaced))

 

Configuring the Encoding of Optionals

Optional값을 데이터의 일부 값으로 인코딩하는 것에 대한 표준은 없습니다. 그럼에도 불구하고 Alamofire는 옵션 인코딩을 위해 다음과 같은 방법으로 NilEncoding을 제공합니다.

  • .dropKey: 출력에서 nil값을 완전히 삭제하여 인코딩합니다. 이는 다른 Swift 인코더와 일치합니다. (ex: otherValue=2)
  • .dropValue: 출력에서 nil값을 삭제하여 인코딩합니다. (ex: nilvalue=&otherValue=2)
  • .null: nil값을 문자열 null로 인코딩합니다.(ex: nilvalue=null&otherValue=2)

또한 nil 대체 값을 제공하는 인코딩 클로저를 지정하여 사용자 정의 인코딩을 생성 할 수 있습니다.

extension URLEncodedFormEncoder.NilEncoding {
  static let customEncoding = NilEncoding { "customNilValue" }
}

사용자 지정 URLEncodedFormParameterEncoder를 생성하고 전달된 URLEncodedFormEncoder의 생성과정에서 원하는NilEncoding을 지정할 수 있습니다.

let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(nilEncoding: .dropKey))

 

 

JSONParameterEncoder

JSONParameterEncoder은 Swift의 JSONEncoder를 사용하여 Encodable 값을 인코딩하고 결과를 URLRequest의 httpBody로 설정합니다. 인코딩된 요청의 Content-Type HTTP 헤더 필드는 아직 설정되지 않은 경우 application/json으로 설정됩니다.

 

POST Request with JSON-Encoded Parameters

let parameters: [String: [String]] = [
    "foo": ["bar"],
    "baz": ["a", "b"],
    "qux": ["x", "y", "z"]
]

AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default)
AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: JSONParameterEncoder.prettyPrinted)
AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: JSONParameterEncoder.sortedKeys)

// HTTP body: {"baz":["a","b"],"foo":["bar"],"qux":["x","y","z"]}

 

Configuring a Custom JSONEncoder

사용자의 필요에 맞게 구성된 JSONEncoder 인스턴스를 전달하여 JSONParameterEncoder의 동작을 사용자 정의할 수 있습니다.

let encoder = JSONEncoder()
encoder.dateEncoding = .iso8601
encoder.keyEncodingStrategy = .convertToSnakeCase
let parameterEncoder = JSONParameterEncoder(encoder: encoder)

 

Manual Parameter Encoding of a URLRequest

ParameterEncoder API는 URLRequest에서 매개변수를 직접 인코딩하여 Alamofire 외부에서도 사용할 수 있습니다.

let url = URL(string: "https://httpbin.org/get")!
var urlRequest = URLRequest(url: url)

let parameters = ["foo": "bar"]
let encodedURLRequest = try URLEncodedFormParameterEncoder.default.encode(parameters, 
                                                                          into: urlRequest)

 

 

 

HTTP Headers

Alamofire에는 자체 HTTPHeaders 유형이 포함되어 있으며, HTTP 헤더 이름/값 쌍의 순서를 유지하고 대소문자를 구분하지 않는 표현입니다. HTTPHeader 유형은 단일 이름/값 쌍을 캡슐화하고 일반적인 헤더에 대한 다양한 정적 값을 제공합니다.

Request에 사용자 정의 HTTPHeaders 를 추가하는 것은 Request 메서드중 하나에 값을 전달하는 것 만큼 간단합니다.

let headers: HTTPHeaders = [
    "Authorization": "Basic VXNlcm5hbWU6UGFzc3dvcmQ=",
    "Accept": "application/json"
]

AF.request("https://httpbin.org/headers", headers: headers).responseDecodable(of: DecodableType.self) { response in
    debugPrint(response)
}

HTTPHeaders는 HTTPHeader값의 배열에서도 구성될 수 있습니다.

let headers: HTTPHeaders = [
    .authorization(username: "Username", password: "Password"),
    .accept("application/json")
]

AF.request("https://httpbin.org/headers", headers: headers).responseDecodable(of: DecodableType.self) { response in
    debugPrint(response)
}

- 변경되지 않는 HTTP Headers에 경우에 URLSessionConfiguration에서 설정하여 기본 URLSession에 의해 생성된 모든 URLSessionTask에 자동으로 적용 되도록 설정하는 것이 좋습니다.

 

기본 Alamofire Session은 모든 Request에 대한 기본 Header 세트를 제공합니다. 여기에는 다음이 포함됩니다.

  • Accept-Encoding,  RFC 7230 §4.2.3.에 따라 기본값은 br;q=1.0, gzip;q=0.8, deflate;q=0.6
  • Accept-Language,  RFC 7231 §5.3.5.에 따라 en;q=1.0과 같은 형식으로 시스템에서 기본적으로 사용되는 최대 6개의 언어로 기본설정됩니다.
  • User-Agent, 현재 앱에 대한 버전 관리 정보가 포함되었습니다.(예: iOS Example/1.0 (cohttp://m.alamofire.iOS-Example; build:1; iOS 13.0.0) Alamofire/5.0.0, per RFC 7231 §5.5.3.)

이 헤더를 사용자 지정 하는 경우엔 사용자 지정 URLSessionConfiguration을 생성하고 headers 속성을 업데이트하고 해당 구성을 새로운 Session 인스턴스에 적용해야 합니다. Alamofire의 기본 헤더를 유지하면서 구성을 사용자 정의하려면 URLSesisonConfiguration.af.defualt를 사용하세요.

 

Response Validation

기본적으로 Alamofire는 응답의 내용과 관계없이 완료된 모든 요청을 성공으로 처리합니다. 응답 핸들러이전에 validate()를 호출하면 응답에 허용할 수 없는 상태 코드나 MIME 유형이 있는 경우 오류가 생성됩니다.

 

Automatic Validation

validate() API는 자동으로 상태코드가 200..<300 범위에 있는지 검증하며, requset header가 제공된 경우 응답의 Content-Type 헤더가 request의 Accept 헤더와 일치하는지 검증합니다.

AF.request("https://httpbin.org/get").validate().responseData { response in
    debugPrint(response)
}

 

Manual Validation

AF.request("https://httpbin.org/get")
    .validate(statusCode: 200..<300)
    .validate(contentType: ["application/json"])
    .responseData { response in
        switch response.result {
        case .success:
            print("Validation Successful")
        case let .failure(error):
            print(error)
        }
    }

 

 

 

Response Handling

Alamofire의 DataRequest와 DownloadRequest 둘다 다음 응답 유형을 갖습니다

-> DataResponse<Success, Failure: Error>, DownloadResponse<Success, Failure: Error>

이 둘은 모두 직렬화된 유형과 오류 유형 두가지 제네릭으로 구성됩니다. 기본적으로 모든 응답 값들은 AFError 에러유형을 생성합니다.(DataResponse<Success, AFError>) Alamofire는 공개 API에서 항상 AFError 오류 유형이 있는 더 간단한 AFDateResponse<Success> 및 AFDownloadResponse<Success를 사용합니다. DataRequest의 서브클래스인 UploadRequest도 같은 DataResponse 유형으로 사용합니다.

 

Alamofire에서 생성된 DataRequest 또는 UploadRequest의 DataResponse를 처리하는것은  responseDecodable과 같은 응답 핸들러를 DataRequest에 연결하는것을 포함합니다.

AF.request("https://httpbin.org/get").responseDecodable(of: DecodableType.self) { response in
    debugPrint(response)
}

 

위의 예에서 responseDecodable 핸들러는 DataRequest가 완료되면 실행되도록 DataRequest에 추가됩니다.

핸들러에 전달된 클로저는 URLRequest, HTTPURLResponse, Data 및 Error로부터 DecodableResopnseSerializer가 생성된 DataResponse<DecodableType, AFError>값을 수신합니다.

서버로부터 응답을 기다리기 위해 실행을 차단하는 대신, 이 클로저는 응답을 받으면 응답을 처리하기 위한 콜백으로 추가됩니다. 요청의 결과는 오직 응답 클로저 범위내에서만 사용이 가능합니다. 서버로 부터 받은 데이터나 응답에 따른 모든 실행은 응답 클로저 안에서 실행되어야합니다.

Alamofire 네트워킹은 비동기적으로 실행됩니다. 비동기적 프로그래밍은 개념에 익숙하지 않은 프로그래머에게 좌절감을 줄 수 있지만 이런 방식으로 수행하는 데는 아주 좋은 이유가 있습니다.

Alamofire는 기본적으로 5개의 다양한 데이터 응답 핸들러가 포함되어 있습니다.

// Response Handler - Unserialized Response
func response(queue: DispatchQueue = .main, 
              completionHandler: @escaping (AFDataResponse<Data?>) -> Void) -> Self

// Response Serializer Handler - Serialize using the passed Serializer
func response<Serializer: DataResponseSerializerProtocol>(queue: DispatchQueue = .main,
                                                          responseSerializer: Serializer,
                                                          completionHandler: @escaping (AFDataResponse<Serializer.SerializedObject>) -> Void) -> Self

// Response Data Handler - Serialized into Data
func responseData(queue: DispatchQueue = .main,
                  dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
                  emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
                  emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods,
                  completionHandler: @escaping (AFDataResponse<Data>) -> Void) -> Self

// Response String Handler - Serialized into String
func responseString(queue: DispatchQueue = .main,
                    dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
                    encoding: String.Encoding? = nil,
                    emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
                    emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods,
                    completionHandler: @escaping (AFDataResponse<String>) -> Void) -> Self

// Response Decodable Handler - Serialized into Decodable Type
func responseDecodable<T: Decodable>(of type: T.Type = T.self,
                                     queue: DispatchQueue = .main,
                                     dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor,
                                     decoder: DataDecoder = JSONDecoder(),
                                     emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
                                     emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods,
                                     completionHandler: @escaping (AFDataResponse<T>) -> Void) -> Self

어떤 응답 처리기도 서버로부터 받은 HTTPURLResponse에 대해 어떠한 검증도 수행하지 않습니다.

- 예를 들어, 400..<500 및 500..<600 범위의 응답 상태 코드는 자동으로 Error를 트리거하지 않습니다. Alamofire는 이를 달성하기 위해 Response Validation method chaining 사용합니다.

 

Response Handler

response 핸들러는 응답 데이터를 평가하지 않습니다. 단지 URLSessionDelegate에서 직접 모든 정보를 전달합니다. 이는 cURL 을 사용하여 Request를 실행하는 것과 동일한 Alamofire입니다.

AF.request("https://httpbin.org/get").response { response in
    debugPrint("Response: \(response)")
}

- Response 및 Result types를 활용할 수 있는 다른 응답 직렬 변환기를 활용하는 것이 좋습니다.

 

Response Data Handler

responseData 핸들러는 DataResponseSerializer를 사용하여 서버에서 반환된 Data를 추출하고 검증합니다. 오류가 발생하지 않고 Data가 추출됬다면 response Result는 .success로 되고 value는 서버에서 반환된 Data가 될것입니다.

AF.request("https://httpbin.org/get").responseData { response in
    debugPrint("Response: \(response)")
}

 

Response String Handler

responseString handler는 StringResponseSerializer를 사용하여 서버로 부터 받은 Data를 지정된 인코딩을 사용하여 String으로 변환합니다. 오류가 발생하지 않고 서버 데이터가 성공적으로 String으로 변환되면 응답 결과는 .success가 되고 값은 String 유형이 됩니다.

AF.request("https://httpbin.org/get").responseString { response in
    debugPrint("Response: \(response)")
}

지정된 인코딩이 없다면 Alamofire는 서버의 HTTPURLResponse에 지정된 텍스트 인코딩을 사용합니다. 만약 서버에서 지정한 텍스트 인코딩이 없다면 기본값인 .isoLatin1으로 사용됩니다.

 

Response Decodable Handler

responseDecodable handler는 DecodableResponseSerializer을 사용하여 지정된 DataDecoder(데이터에서 디코딩할 수 있는 디코더에 대한 프로토콜 추상화)를 사용하여 서버로부터 받은 Data를 Decodable 유형으로 변환합니다. 에러가 발생하지 않고 서버로부터 받은 데이터가 성공적으로 Decodable 유형으로 디코딩 됬다면 응답결과는 .success가 되고, 값은 전달받은 유형이 됩니다.

struct DecodableType: Decodable { let url: String }

AF.request("https://httpbin.org/get").responseDecodable(of: DecodableType.self) { response in
    debugPrint("Response: \(response)")
}

 

Chained Response Handlers

Response handler는 또한 연결될 수 있습니다.

Alamofire.request("https://httpbin.org/get")
    .responseString { response in
        print("Response String: \(response.value)")
    }
    .responseDecodable(of: DecodableType.self) { response in
        print("Response DecodableType: \(response.value)")
    }

- 동일한 요청에 여러 응답 핸들러를 사용하려면 서버 데이터를 각 응답 핸들러에 대해 한번씩 여러번 직렬화해야 한다는 점에 유의하는 것이 중요합니다. 동일한 요청에 여러 응답 핸들러를 사용하는 것은 일반적으로 특히 프로덕션 환경에서 모범 사례로 피해야 합니다. 디버깅이나 더 나은 옵션이 없는 상황에서만 사용해야 합니다.

 

Response Handler Queue

응답핸들러에 전달된 클로저는 기본적으로 .main 큐에서 실행되지만 클로저를 실행하기 위해 특정 DispatchQueue로 전달될 수 있습니다. 실제 직렬화 작업(데이터를 다른 유형으로 변환)은 항상 요청을 발행하는 세션의 rootQueue 또는 serializationQueue(제공된 경우)의 백그라운드에서 실행됩니다.

let utilityQueue = DispatchQueue.global(qos: .utility)

AF.request("https://httpbin.org/get").responseDecodable(of: DecodableType.self, queue: utilityQueue) { response in
    print("This closure is executed on utilityQueue.")
    debugPrint(response)
}

 

 

Response Caching

Response caching은 URLCache에 의해 시스템 프레임워크 수준에서 처리됩니다. 복합적인 in-memory 및 on-disk 캐시를 제공하며 in-memory, on-dist 둘다 크기를 조작할 수 있습니다.

- 기본적으로 Alamofire는 URLCache.shared 인스턴스를 활용합니다. 사용된 URLCache 인스턴스를 사용자 정의하려면 Session Configuration을 참조하세요.

 

Authentication

Authentication은 URLCredential와 URLAuthenticationChallenge에 의해 시스템 프레임워크 수준에서 처리됩니다.

- 이러한 Authentication API는 인증 또는 동등한 헤더가 필요한 API와 함께 일반적으로 사용되지 않고 승인을 요청하는 서버용 입니다.

 

지원되는 인증체계

- HTTP Basic

- HTTP Digest

- Kerberos

- NTLM

 

HTTP Basic Authentication

요청의 인증방법은 URLAuthenticationChallenge로 인증 요청을 받을 때 자동으로 URLCredential을 제공합니다.

let user = "user"
let password = "password"

AF.request("https://httpbin.org/basic-auth/\(user)/\(password)")
    .authenticate(username: user, password: password)
    .responseDecodable(of: DecodableType.self) { response in
        debugPrint(response)
    }

 

Authentication with URLCredential

let user = "user"
let password = "password"

let credential = URLCredential(user: user, password: password, persistence: .forSession)

AF.request("https://httpbin.org/basic-auth/\(user)/\(password)")
    .authenticate(with: credential)
    .responseDecodable(of: DecodableType.self) { response in
        debugPrint(response)
    }

 

- 인증을 위해 URLCredential사용할 때 서버에서 문제가 발생하면 기본 URLSession은 실제로 두개의 요청을 생성하게 된다는 점에 유의해야합니다. 첫번째 요청에는 서버에서 챌린지를 "촉발할 수 있는" 자격 증명이 포함되지 않습니다. 그런 다음 Alamofire가 챌린지를 수신하고 자격 증명이 추가되며 기본 URLSession이 요청을 다시 시도합니다.

 

Manual Authentication

메세지를 표시하지 않고 항상 인증 또는 유사한 헤더가 필요한 API와 통신하는 경우 수동으로 추가할 수 있습니다.

let user = "user"
let password = "password"

let headers: HTTPHeaders = [.authorization(username: user, password: password)]

AF.request("https://httpbin.org/basic-auth/user/password", headers: headers)
    .responseDecodable(of: DecodableType.self) { response in
        debugPrint(response)
    }

그러나 모든 요청에 반드시 포함되어야 하는 헤더는 RequestAdapter를 사용하거나 사용자 지정 URLSessionConfiguration의 부분으로 처리되는게 더 나은 경우가 많습니다.

 

Downloading Data to a File

Alamofire는 메모리로 데이터를 가져오는 것 외에도 디스크로 다운로드 할 수 있게 Session.download, DownloadRequest 그리고 DownloadResponse<Success, Failure: Error> API를 제공합니다. 메모리로 다운로드하는 것은 대부분의 JSON API응답과 같은 작은 페이로드에 적합하지만, 이미지 및 비디오와 같은 더 큰 데이터를 가져오는 것은 애플리케이션의 메모리 문제를 피하기 위해 디스크로 다운로드해야 합니다.

AF.download("https://httpbin.org/image/png").responseURL { response in
    // Read file from provided file URL.
}

DataRequest와 동일한 응답 핸들러를 갖는 것 외에도 DownloadRequest에는 responseURL도 포함됩니다. 다른 응답 핸들러와는 다르게 이 핸들러는 단지 다운로드된 데이터의 위치가 포함된 URL만 반환하고 디스크에서 Data를 읽진 않습니다.

responseDecodable과 같은 다른 핸들러는 디스크에서 응답 Data를 읽는것 까지 포함됩니다. 이것은 아마도 많은양의 데이터를 메모리로 읽어들이는 것이 포함될 수 있으므로 다운로드에 이러한 핸들러를 사용할 때 이를 염두해 두는 것이 중요합니다.

 

Download File Destination

모든 다운로드된 데이터는 처음에는 시스템 임시 디렉토리에 저장됩니다. 언젠가는 시스템에 의해 결국 삭제될 것이므로 더 오래 보관해야 하는 파일이라면 다른곳으로 옮기는 것이 중요합니다.

파일을 임시 디렉토리에서 최종 대상으로 이동하기 위해 대상 클로저를 사용할 수 있습니다. 임시 파일이 실제로 목적지URL로 이동되기 전에 클로저에 지정된 옵션이 실행됩니다. 현재 지원되는 두가지 옵션은 다음과 같습니다.

  • .createIntermediateDirectories: 지정된 경우 대상 URL에 대한 중간 디렉토리를 생성합니다.
  • .removePreviousFile: 지정된 경우 대상 URL에서 이전 파일을 제거합니다.
let destination: DownloadRequest.Destination = { _, _ in
    let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    let fileURL = documentsURL.appendingPathComponent("image.png")

    return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}

AF.download("https://httpbin.org/image/png", to: destination).response { response in
    debugPrint(response)

    if response.error == nil, let imagePath = response.fileURL?.path {
        let image = UIImage(contentsOfFile: imagePath)
    }
}

제안된 다운로드 대상 API를 사용할 수도 있습니다.

let destination = DownloadRequest.suggestedDownloadDestination(for: .documentDirectory)

AF.download("https://httpbin.org/image/png", to: destination)

 

Download Progress

다운로드 진행 상황을 사용자에게 보고하는 것이 도움이 될 수 있는 경우가 많습니다. 모든 DownloadRequest는 downloadProgress API를 사용하여 다운로드 진행 상황을 보고할 수 있습니다.

AF.download("https://httpbin.org/image/png")
    .downloadProgress { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseData { response in
        if let data = response.value {
            let image = UIImage(data: data)
        }
    }

- URLSession 및 Alamofire의 진행 상황 보고 API는 서버가 진행 상황을 계산하는 데 사용할 수 있는 Content-Length 헤더를 올바르게 반환하는 경우에만 작동합니다. 해당 헤더가 없으면 다운로드가 완료될 때 까지 진행률이 0.0으로 유지되며, 다운로드가 완료되면 진행률이 1.0으로 점프됩니다.

downloadProgress API는 다운로드 진행 클로저가 호출되어야 하는 DispatchQueue를 정의하는 대기열 매개변수를 취할 수도 있습니다.

let progressQueue = DispatchQueue(label: "com.alamofire.progressQueue", qos: .utility)

AF.download("https://httpbin.org/image/png")
    .downloadProgress(queue: progressQueue) { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseData { response in
        if let data = response.value {
            let image = UIImage(data: data)
        }
    }

 

Canceling and Resuming a Download

모든 Request 클래스에 있는 cancel() API외에도 DownloadRequests는 나중에 다운로드를 재개하는 데 사용할 수 있는 ResumeData를 생성할 수 있습니다. 이 API는 두가지 형식이 있습니다. cancel(producingResumeData: Bool)은 ResumeData 생성 여부를 제어할 수 있지만 DownloadResponse에서만 사용할 수 있도록 합니다.

그리고 cancel(byProducingResumeData:(_ resumeData: Data?) -> Void)는 동일한 작업을 수행하지만 완료 핸들러에서 ResumeData를 사용할 수 있게 만듭니다.

DownloadRequest가 취소되거나 중단되면 기본 URLSessionDownloadTask가 ResumeData를 생성할 수 있습니다. 이런 일이 발생하면 ResumeData를 다시 사용하여 중단된 DownloadRequest를 다시 시작할 수 있습니다.

- 중요: 모든 Apple 플랫폼(iOS 10 - 10.2, macOS 10.12 - 10.12.2, tvOS 10 - 10.1, watchOS 3 - 3.1.1)의 일부 버전에서는 ResumeData가 백그라운드 URLSessionConfigurations에서 손상됩니다. ResumeData 생성 로직에는 데이터가 잘못 기록되어 항상 다운로드를 재개하지 못하는 기본 버그가 있습니다. 버그해결은 다음을 참조하세요 https://stackoverflow.com/questions/39346231/resume-nsurlsession-on-ios10/39347461#39347461

 

Resume NSUrlSession on iOS10

iOS 10 is going to be released soon so it worth to test applications for compatibility with it. During such test we've discovered that our app can't resume background downloads on iOS10. Code that ...

stackoverflow.com

var resumeData: Data!

let download = AF.download("https://httpbin.org/image/png").responseData { response in
    if let data = response.value {
        let image = UIImage(data: data)
    }
}

// download.cancel(producingResumeData: true) // Makes resumeData available in response only.
download.cancel { data in
    resumeData = data
}

AF.download(resumingWith: resumeData).responseData { response in
    if let data = response.value {
        let image = UIImage(data: data)
    }
}

 

Updating Data to a Server

JSON 또는 URL 인코딩 매개변수를 사용하여 상대적으로 적은 양의 데이터를 서버에 보낼 때는 일반적으로 request() API로 충분합니다. 메모리 내 데이터, 파일 URL 또는 InputStream에서 훨씬 더 많은 양의 데이터를 보내야하는 경우 upload() API를 사용하는 것이 좋습니다.

 

Uploading Data

let data = Data("data".utf8)

AF.upload(data, to: "https://httpbin.org/post").responseDecodable(of: DecodableType.self) { response in
    debugPrint(response)
}

 

Uploading a File

let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

AF.upload(fileURL, to: "https://httpbin.org/post").responseDecodable(of: DecodableType.self) { response in
    debugPrint(response)
}

 

Uploading Multipart Form Data

AF.upload(multipartFormData: { multipartFormData in
    multipartFormData.append(Data("one".utf8), withName: "one")
    multipartFormData.append(Data("two".utf8), withName: "two")
}, to: "https://httpbin.org/post")
    .responseDecodable(of: DecodableType.self) { response in
        debugPrint(response)
    }

 

Uploading Progress

사용자가 업로드가 완료되기를 기다리는 동안 사용자에게 업로드 진행 상황을 표시하는 것이 편리한 경우가 있습니다. 모든 UploadRequest는 uploadProgress 및 downloadProgress API를 사용하여 업로드의 업로드 진행률과 응답 데이터 다운로드의 다운로드 진행률을 모두 보고할 수 있습니다.

let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

AF.upload(fileURL, to: "https://httpbin.org/post")
    .uploadProgress { progress in
        print("Upload Progress: \(progress.fractionCompleted)")
    }
    .downloadProgress { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseDecodable(of: DecodableType.self) { response in
        debugPrint(response)
    }

 

 

Streaming Data from a Server

시간이 오래걸리는 대규모 다운로드 또는 오래동안 서버 연결은 데이터가 도착했을 때 저장하는 것보다 스트리밍을 통해 제공되는게 더 좋을 방법일 수 있습니다. Alamofire는 이러한 사용을 처리하기 위해 DataStreamRequest유형과 관련 API를 제공합니다. 다른 요청과 동일한 API를 많이 제공하지만 몇 가지 주요 차이점이 있습니다. 특히 DataStreamRequest는 데이터를 메모리에 축적하거나 디스크에 저장하지 않습니다. 대신 데이터가 도착할 때 추가된 responseStream 클로저가 반복적으로 호출됩니다. 연결이 완료되거나 오류가 수신되면 동일한 클로저가 다시 호출됩니다.

모든 핸들러 클로저는 처리중인 이벤트와 요청을 취소하는 데 사용할 수 있는 CancellationToken을 모두 포함하는 스트림 값을 캡처합니다.

public struct Stream<Success, Failure: Error> {
    /// Latest `Event` from the stream.
    public let event: Event<Success, Failure>
    /// Token used to cancel the stream.
    public let token: CancellationToken
    /// Cancel the ongoing stream by canceling the underlying `DataStreamRequest`.
    public func cancel() {
        token.cancel()
    }
}

Event는 두가지 가능한 스트림 상태를 나타내는 열거형입니다.

public enum Event<Success, Failure: Error> {
    /// Output produced every time the instance receives additional `Data`. The associated value contains the
    /// `Result` of processing the incoming `Data`.
    case stream(Result<Success, Failure>)
    /// Output produced when the instance has completed, whether due to stream end, cancellation, or an error.
    /// Associated `Completion` value contains the final state.
    case complete(Completion)
}

완료되면 Completion값에는 스트림이 종료되었을 때 DataStreamRequest의 상태가 포함됩니다.

public struct Completion {
    /// Last `URLRequest` issued by the instance.
    public let request: URLRequest?
    /// Last `HTTPURLResponse` received by the instance.
    public let response: HTTPURLResponse?
    /// Last `URLSessionTaskMetrics` produced for the instance.
    public let metrics: URLSessionTaskMetrics?
    /// `AFError` produced for the instance, if any.
    public let error: AFError?
}

 

Streaming Data

서버에서 데이터 스트리밍은 다른 Alamofire 요청처럼 수행될 수 있지만 핸들러 클로저가 추가됩니다.

func responseStream(on queue: DispatchQueue = .main, stream: @escaping Handler<Data, Never>) -> Self

제공된 queue는 Handler 클로저가 호출되는 위치입니다.

AF.streamRequest(...).responseStream { stream in
    switch stream.event {
    case let .stream(result):
        switch result {
        case let .success(data):
            print(data)
        }
    case let .complete(completion):
        print(completion)
    }
}

- 데이터 수신은 절대 실패할 수 없으므로 위 예제처럼 결과의 .failure 사례를 처리하는 것은 불필요합니다.

 

Streaming 'String's

데이터 스트리밍과 마찬가지로 문자열도 핸들러를 추가하여 스트리밍할 수 있습니다.

func responseStreamString(on queue: DispatchQueue = .main,
                          stream: @escaping StreamHandler<String, Never>) -> Self

문자열 값은 UTF8로 디코딩되며 디코딩이 실패할 수 없습니다.

AF.streamRequest(...).responseStreamString { stream in
    switch stream.event {
    case let .stream(result):
        switch result {
        case let .success(string):
            print(string)
        }
    case let .complete(completion):
        print(completion)
    }
}

 

Streaming Decodable Values

들어오는 스트림 데이터값은 responseStreamDecodable을 사용하여 Decodable 값으로 변환될 수 있습니다.

func responseStreamDecodable<T: Decodable>(of type: T.Type = T.self,
                                           on queue: DispatchQueue = .main,
                                           using decoder: DataDecoder = JSONDecoder(),
                                           preprocessor: DataPreprocessor = PassthroughPreprocessor(),
                                           stream: @escaping Handler<T, AFError>) -> Self

디코딩 실패로 인해 스트림이 종료되지는 않지만 대신 출력 결과에 AFError가 생성됩니다.

AF.streamRequest(...).responseStreamDecodable(of: SomeType.self) { stream in
    switch stream.event {
    case let .stream(result):
        switch result {
        case let .success(value):
            print(value)
        case let .failure(error):
            print(error)
        }
    case let .complete(completion):
        print(completion)
    }
}

 

Producing an InputStream

StreamHandler 클로저를 사용하여 들어오는 데이터를 처리하는 것 외에도 DataStreamRequest는 바이트가 도착할 때 읽는 데 사용할 수 있는 InputStream값을 생성할 수 있습니다.

func asInputStream(bufferSize: Int = 1024) -> InputStream

이러한 방식으로 생성된 InputStream은 읽기가 시작되기 전에 open()을 호출해야 하며, 그렇지 않으면 스트림을 자동으로 여는 API에 전달되어야 합니다. 이 메서드에서 반환된 후에는 InputStream 값을 활성 상태로 유지하고 읽기가 완료된 후 close()를 호출하는 것은 호출자의 책임입니다.

let inputStream = AF.streamRequest(...)
    .responseStream { output in
        ...
    }
    .asInputStream()

 

Cancellation

DataStreamRequest는 네 가지 방법으로 취소할 수 있습니다.

첫째, 다른 모든 Alamofire 요청과 마찬가지로 DataStreamRequest는 cancel()을 호출하여 기본 작업을 취소하고 스트림을 완료할 수 있습니다.

let request = AF.streamRequest(...).responseStream(...)
...
request.cancel()

 

둘째, DataStreamSerializer에 오류가 발생하면 DataStreamRequest가 자동으로 취소될 수 있습니다. 이 동작은 기본적으로 비활성화되어 있으며 요청을 생성할 때 automaticCancelOnStreamError 매개변수를 전달하여 활성화할 수 있습니다.

AF.streamRequest(..., automaticallyCancelOnStreamError: true).responseStream(...)

 

셋째, Handler 클로저에서 오류가 발생하면 DataStreamRequests가 취소됩니다. 이 오류는 요청에 저장되며 완료값에서 사용할 수 있습니다.

AF.streamRequest(...).responseStream { stream in
    // Process stream.
    throw SomeError() // Cancels request.
}

 

마지막으로 Stream값의 cancel() 메서드를 사용하여 DataStreamRequests를 취소할 수 있습니다.

AF.streamRequest(...).responseStream { stream in 
    // Decide to cancel request.
    stream.cancel()
}

 

 

Statistical Metrics

URLSessionTaskMetrics

Alamofire는 모든 요청에 대해 URLSessionTaskMetrics를 수집합니다. URLSessionTaskMetrics는 기본 네트워크 연결과 요청 및 응답 타이밍에 대한 환상적인 통계정보를 캡슐화합니다.

AF.request("https://httpbin.org/get").responseDecodable(of: DecodableType.self) { response in
    print(response.metrics)
}

- FB7624529로 인해 watchOS의 URLSessionTaskMetrics 수집이 현재 비활성화되어 있습니다.

 

cURL Command Output

플랫폼 문제를 디버깅하는 것은 실망스러울 수 있습니다. 다행히 Alamofire의 요청 유형은 쉬운 디버깅을 위해 동등한 cURL 명령을 생성할 수 있습니다. Alamofire 요청 생성의 비동기 특성으로 인해 이 API에는 동기 버전과 비동기 버전이 모두 있습니다. cURL 명령을 최대한 빨리 얻으려면 cURLDescription을 요청에 연결하면 됩니다.

AF.request("https://httpbin.org/get")
    .cURLDescription { description in
        print(description)
    }
    .responseDecodable(of: DecodableType.self) { response in
        debugPrint(response.metrics)
    }

 

 

 

 

 

 

 

 

 

배경


공공데이터 포털에 있는 오픈API를 활용하여 프로젝트를 만들던중 관광지검색을 위해 지역코드를 Key값으로 넣어줘야하는 상황이였습니다. 지역코드는 HTTP통신을 통해 불러오는게 아니라 따로 엑셀파일을 제공해줘서 이를 사용해보고자합니다!

 이런식으로 제공해줬는데.. 데이터가 200개가 넘어가는 많은 데이터량입니다.

이를 JSON형식으로 바꾸자니 엄청난 노가다가 예상되어 그냥 복사붙여넣기 후 변수로 지정한다음 데이터 가공하여 배열로 담아주었습니다.

 var text = """
    11    서울특별시    11110    종로구
    11    서울특별시    11140    중구
    11    서울특별시    11170    용산구
    11    서울특별시    11200    성동구
    ....
    ...
    ....
"""

// 텍스트가 저장된 변수를 배열에 담기
func uploadData() {
        var array = text.components(separatedBy: "\n")
        for element in array {
            let arr = element.components(separatedBy: " ")
            for j in 12..<arr.count {
                data.append(LocationDataModel(areaName: arr[4], signguName: arr[j], areaCode: Int(arr[0])!, signguCode: Int(arr[8])!))
        }
    }
}

 

이 방법을 썼더니.. 한 소스코드파일의 길이가 엄청 길어져서 불편한 상황이 생겼습니다.

 

이건 너무 비효율적이다 싶어서 JSON형식으로 변환하여 사용하기로 했습니다.

 

 

XLS -> JSON 형식변환

저것을 일일히 형식으로 바꿔주기엔 너무 많은 시간이 걸리므로 해당 형식을 JSON형식으로 변환해주는 사이트를 발견했습니다.

https://shancarter.github.io/mr-data-converter/

 

Mr. Data Converter

 

shancarter.github.io

XLS에 저장된 변수명을 포함하여 복사 붙여넣기 해주면JSON 형식으로 변환해줍니다!

 

 

이대로 사용해도 좋지만 가독성 좋게 다시 변환해보려고 합니다!

http://json.parser.online.fr/beta/

 

Json Parser Online

 

json.parser.online.fr

위 사이트에서는 JSON형식으로 가독성 있게 변환해줍니다

 

이제 변환된 JSON형식을 사용해보겠습니다.

 

Xcode에서 json파일 만들기

다른 IDE에서 json파일로 만들고 프로젝트에 넣어도 되지만 Xcode에서도 직접 만들 수 있습니다.

 

1. 새로운파일을 선택해서 string을 검색 -> Strings File

 

2. 파일명과 확장자를 json으로 변경하고 Create -> Use .json을 선택해줍니다.

 

 

3. 완성된 JSON파일에 이전에 변환했던 JSON형식 데이터를 붙여넣어줍니다.

 

 

여기까지 JSON파일이 완성되었습니다.

주의할점은 JSON파일이 만들어졌지만 위 주석처리된 문장들을 모두 지워주셔야 디코딩 형식 오류가 나지 않습니다.

 

이제 JSON데이터를 불러와 디코딩하는 과정까지 보겠습니다

 

1. JSON형식과 맞는 데이터모델을 구성

struct LocationModel: Decodable {
    let data: [LocationDataModel]
}
struct LocationDataModel: Decodable {
    let areaName: String
    let sigunguName: String
    let areaCode: Int
    let sigunguCode: Int
    
    enum CodingKeys: String, CodingKey {
        case areaName = "areaNm"
        case sigunguName = "sigunguNm"
        case areaCode = "areaCd"
        case sigunguCode = "signguCd"
    }
}

 

2. JSON파일을 불러오는 코드를 작성해줍니다.

// 데이터 불러오기
func load() -> Data? {
    // 파일이름
    let fileName = "AreaCode"
    // 파일 확장자
    let fileType = "json"
    // 파일 위치
    guard let fileLocation = Bundle.main.url(forResource: fileName, withExtension: fileType) else { return nil }
    
    // 해당 파일을 Data로 변환
    do {
        let data = try Data(contentsOf: fileLocation)
        return data
    } catch {
        return nil
    }
}

 

3. 불러온 Data를 디코딩해줍니다.

// 디코딩
func decode(data: Data?) -> [LocationDataModel] {
    // 데이터를 잘 받았는지 확인
    guard let data = data else { return [] }
    
    do {
        // 지정한 데이터모델로 디코딩
        let decoded = try JSONDecoder().decode(LocationModel.self, from: data)
        return decoded.data
    } catch {
        return []
    }
}

 

 

4. 결과

 

정상적으로 디코딩되어 배열에 저장되었습니다!

 

디코딩 형식 오류로 시간을 잡아먹었는데 변수명, null처리, json 위 주석문장 제거 필히 확인하는게 좋습니다.

 

프로젝트의 기능 중에 셀 안에 체크버튼을 누르면 Lottie 체크 애니메이션이 재생이 되고, 체크표시가 되어야 하는데 의도와는 다르게 작동해서 이에 대해 정리해보고자 합니다.

 

Lottie-Animaion


https://github.com/airbnb/lottie-ios

 

GitHub - airbnb/lottie-ios: An iOS library to natively render After Effects vector animations

An iOS library to natively render After Effects vector animations - airbnb/lottie-ios

github.com

 

Lottie는 JSON형식의 애니메이션 코드를 실시간으로 렌더링하여 출력하는 라이브러리입니다.

장점으로는 벡터기반 애니메이션 형식으로 확대해도 이미지가 깨지지 않는점입니다.

또한 JSON형식으로 저장되기 때문에 파일 크기가 작아 로딩시간이 짧습니다.

이 라이브러리를 사용해서 애니메이션을 출력하고자 했습니다.

 

 

기획


  • 테이블 뷰 셀안에 LottieAnimationView와 버튼이 있습니다.
  • LottieAnimationView는 체크 애니메이션을 재생합니다.
  • 버튼을 누르면 해당 셀의 가운데위치에서 체크애니메이션을 재생합니다.
  • 애니메이션이 종료되면 체크표시가 남아있도록 합니다.
  • 여러 개의 셀이 존재할 수 있고, 각자 독립적인 셀입니다.

처음 기획했던 의도는 위와 같습니다.

쉽게 말하면 To-do와 같이 해야 할 일을 불러와 완료되면 애니메이션 출력 후 완료표시를 남겨두는 것입니다.

 

하지만 기획했던 의도와 다르게 작동하는 버그가 발생하였습니다.

 

 

버그발생


애니메이션이 재생되지않음 / 다른셀에 있는 애니메이션 뷰가 반응함

위와 같이 버그가 발생했습니다. 버그내용은 다음과 같습니다.

  • 체크버튼을 눌러도 애니메이션이 재생을 건너뛰고 완료표시만 생김
  • 체크버튼을 누르면 다른 셀이 반응함

왜 이런 버그가 발생했는지 원인을 찾아보기로 했습니다.

 

 

원인분석


전체적으로 테이블뷰가 존재하고, 커스텀 셀을 구현하여 데이터모델에 맞춰 재사용되고 있습니다.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // TableViewCell을 만들어서 해당 셀을 사용 (커스텀 셀)
    guard let cell = tableView.dequeueReusableCell(
        withIdentifier: TodayTableViewCell.identifier,
        for: indexPath
    ) as? TodayTableViewCell else {
        return UITableViewCell()
    }
    // 데이터모델의 데이터를 셀의 UI Property에 주입
    let study = viewModel.todo[indexPath.row]
    cell.configure(with: study)
    cell.setUIColor()
    
    // 버튼 이벤트를 위해 델리게이트 위임
    cell.delegate = self

    return cell
}
// 체크버튼 클릭 시 이벤트
@objc func checkButtonTapped() {
	// 애니메이션 재생
    checkView.play()
    // 해당 셀의 데이터 done을 true로 변경하는 메서드
    delegate?.checkButtonTapped(name: name)
}

// TableViewCell.Swift
// Cell 데이터 바인딩 메서드
func configure(with model: FirebaseDataModel) {
    self.name = model.name
    label.text = model.name
    guard let done = model.done else { return }
    // done값이 True면 체크표시로 남긴다.
	if done {
    	checkView.currentProgress = 20
    }
}

 

테이블 뷰의 셀을 최신화하기 위해 늘 사용해 왔던 메서드 tableView.reloadData()가 있습니다.

reloadData() 메서드가 실행되면 테이블뷰는 데이터를 가장 최신값을 기준으로 다시 불러오는 과정을 실행하게 됩니다.

이때 tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) 메서드 역시 재실행하는데요

만들어둔 커스텀셀의 데이터 바인딩을 위해 구현한 configure메서드도 실행됩니다.

configure메서드가 실행되므로 셀의 데이터가 변경되어 사용자가 시점에서는 변경된 데이터로 즉시 볼 수 있게 됩니다.

 

configure메서드 내부를 살펴보면 done값이 true면 체크표시가 남도록 설계했었습니다.

이 결과 체크버튼을 누르면 즉시 데이터가 변경되어 reloadData() 메서드가 실행되어 애니메이션 재생을 건너뛰고 바로 체크표시로 남게 되었습니다.

 

두 번째로 발생한 버그는 셀의 재사용 특성에서 볼 수 있었습니다.

커스텀셀을 dequeueReuseableCell로 지정하여 셀이 생성되거나 스크롤 시 재사용큐에 저장된 셀을 사용하도록 구현되었습니다.

데이터를 추가하여 새로운 셀이 추가되니 다음과 같은 현상을 볼 수 있었습니다.

currentProgress 0: 재생전 / currentProgress 1: 재생후 체크표시

2가 적힌 셀을 재생시키고 체크표시를 해두었지만 새로운 데이터를 추가하여 셀을 추가하니

이미 재생된 셀이 나오거나 재생되지 않은 셀이 나오는 현상을 발견하였습니다.

 

재사용하는 셀의 초기 UI상태를 직접 설정하는 prepareForReuse() 메서드를 사용하지 않으니 UI가 뒤죽박죽 되었습니다.

 

 

 

해결한 방안


버그 1) 체크버튼을 눌러도 애니메이션이 재생을 건너뛰고 완료표시만 생김

-> 데이터바인딩이 되어 있어서 데이터값이 변경되면 tableView.reloadData() 메서드가 즉시 실행되는 구조였습니다.

-> 이를 데이터값이 변경은 되어도 tableView.reloadData()메서드가 실행되지 않도록 수정하여 애니메이션이 정상적으로 실행되도록 수정하였습니다.

 

 

 

버그 2) 체크버튼을 누르면 다른 셀이 반응함

-> 커스텀셀을 구현했고, dequeueReusableCell을 사용했습니다. 이에 따라 테이블뷰가 스크롤되거나 새로운 셀이 추가되면 재사용큐에 저장된 셀을 사용하거나 이전에 만들어진 셀을 바탕으로 셀이 생성됩니다.

-> 그 결과 UI가 뒤죽박죽 된 상태로 되었고 이를 방지하기 위해 셀이 추가되거나 재사용될 때 초기 UI 상태를 지정하는 prepareForReuse()를 사용하였습니

 

 

 

결과


정상적으로 작동하게 되었습니다.

 

 

 

당연하게 테이블 뷰를 구성할 때 커스텀 셀을 만들었고 dequeueReusableCell을 이용하여 셀을 구성하였었습니다.

이번 버그를 고치기 위해 셀을 재사용한다는 점과 데이터와 UI가 바인딩된 상태에서 애니메이션구현까지 된다면 기획했던 과정이 잘 진행이 되는지 검토해야 한다는 점을 느꼈습니다. 그리고 tableView.reloadData()는 즉시 테이블 뷰를 새로고침해서 애니메이션이 부자연스럽게 작동한다는 것을 알았습니다.

 

개인 프로젝트를 진행하며 쉽고 편리하게 데이터를 저장할 수 있어서 많이 써왔었는데요

문득 궁금해졌습니다

사용하면서 이게 어디에 저장되는 것이고, 어떤 형식으로 저장되었는지

혹시 내가 코딩하면서 키가 잘못쓰여서 사용되지 않는 데이터가 있는지 많은 생각이 들었는데요

궁금증을 해결하고자 검색하고 앱 디렉토리를 탐색하며 얻은 정보를 바탕으로 작성하였습니다

 

UserDefaults


UserDefaults는 런타임동안 Key-Value 값 쌍을 영구적으로 저장하는 기본 데이터베이스에 대한 인터페이스입니다.

공식문서에 따르면 기본값을 설정하면 동기적으로 변경되고, 저장소에서는 비동기적으로 변경된다고 합니다.

Key만 정해준다면 다양한 타입의 데이터를 저장할 수 있습니다.

영구적인 저장이기 때문에 앱을 종료해도 데이터가 남아있습니다.

 

 

 

저장장소


UserDefaults를 이용하여 저장한 데이터는 xml형식의 Property List기반인 plist 파일로 저장되어 있습니다.

저장위치는 Sandbox내부의 Data Container에 저장됩니다.

갑자기 튀어나온 Sandbox는 나중에 자세하게 글로 남기겠습니다.

쉽게 앱이 외부의 데이터와 접근 및 허용을 하지 않게 가둔 틀이라고 생각하시면 됩니다.

 

저는 두 눈으로 plist로 저장된 데이터를 직접 보고 싶어서 디렉토리를 찾아보았습니다.

 

1. 장치에서 실행한 앱의 데이터를 보기

Xcode에서 아이폰에서 직접 앱을 돌려보기 위해 저장했던 장치입니다.

중간에 INSTALLED APPS에 설치된 앱의 목록이 뜨는데요.

Download Container버튼을 통해 다운받아주시면 됩니다!

다운받은 파일을 패키지 내용 보기를 누르시면 접근하실 수 있습니다.

AppData\Library\Preferences\앱번들ID.plist

다크모드 기능 여부에 대한 key-value값이 저장되어 있네요

 

2. 시뮬레이터에서 실행한 앱의 데이터를 보기

시뮬레이터에서 실행한 앱의 데이터를 보기 위해서는 직접 디렉터리를 찾아가도 좋지만 명령어를 통해 더 빠르게 접근할 수 있습니다.

저는 시뮬레이터로 많은 앱을 돌려보니 정말 많은 앱 폴더가 생겼는데요.. 이 명령어로 쉽게 찾을 수 있었습니다.

 

※ 명령어 사용 전 시뮬레이터를 켜주세요

※ app.identifier 대신에 앱 번들 Identifier을 넣어주세요

$ xcrun simctl get_app_container booted app.identifier data

위 명령어를 쓰시면 해당 앱의 위치가 출력됩니다

$ cd(디렉터리 폴더)
$ open .

open . 을 쓰시면 파인더가 바로 열리고 Library\Preferences\앱번들ID.plist를 확인하실 수 있습니다.

 

 

데이터 저장


데이터를 저장할 땐 set메서드를 사용하면 됩니다.

Array의 append와 같이 기존의 값에서 추가하는 방식이 아닌 덮어 쓰이는 방식으로 만약 같은 키값에서 밸류의 값이 추가가 된다면 

set메서드를 사용하여 덮어쓰면 됩니다!

// 문자열
UserDefaults.standard.setValue("String", forKey: "String")
// Bool
UserDefaults.standard.setValue(true, forKey: "Bool")
// 배열
UserDefaults.standard.setValue([1, 2, 3, 4, 5], forKey: "IntegerArray")

 

구조체

// 구조체는 인코딩하여 저장하고 디코딩하여 가져오므로 Codable을 채택한다.
struct Person: Codable {
    let name: String
    let age: Int
    let job: String
}

let person = Person(name: "cheolsu", age: 20, job: "student")    
let encoder = JSONEncoder()
do {
    let encoded = try encoder.encode(person)
    UserDefaults.standard.setValue(encoded, forKey: "struct")
} catch {
    print("Error")
}

 

데이터 저장된 모습

 

데이터 불러오기


데이터를 불러오는 방법은 해당 타입에 맞게 캐스팅해 주거나, 해당타입의 메서드로 불러오면 됩니다.

// 문자열
UserDefaults.standard.string(forKey: "String")
// Bool
UserDefaults.standard.bool(forKey: "Bool")
// 배열
UserDefaults.standard.array(forKey: "IntegerArray")


// 구조체
let decoder = JSONDecoder()
guard let data = UserDefaults.standard.object(forKey: "struct") as? Data else { return }
do {
    let decoded = try decoder.decode(Person.self, from: data)
    print(decoded)
} catch {
    print("Error")
}

 

 

 

 

데이터 제거하기


// key로 저장된 데이터 제거
UserDefaults.standard.removeObject(forKey: "key")

 

 

 

 

 

저장된 데이터 확인하기


직접 디렉터리를 접근하여 찾는 방법도 있지만 모든 저장된 UserDefaults 데이터를 불러오는 메서드가 있습니다.

// UserDefaults.standard.dictionaryRepresentation()

for (key, value) in UserDefaults.standard.dictionaryRepresentation() {
    print("key: \(key), value: \(value)")
}

/* 출력
 key: struct, value: {length = 43, bytes = 0x7b226a6f 62223a22 73747564 656e7422 ... 61676522 3a32307d }
 key: String, value: String
 key: IntegerArray, value: (
     1,
     2,
     3,
     4,
     5
 )
 key: Bool, value: 1
 */

(위 메서드 사용 하면 UserDefaults에 저장된 시스템 기본설정값도 출력됩니다.)

 

 

 

열거형을 사용하여 키 분류하기


UserDefaults는 plist파일로 저장되기 때문에 보안이 강력하다고 하기는 어렵고, 데이터를 쉽게 저장하고 불러올 수 있지만 저장공간을 많이 차지하기 때문에 대용량 데이터를 저장하기에는 적합하지 않다고 생각합니다.

그래서 비교적 가벼운 데이터를 저장하는데요 (예로 자동로그인 여부, 다크모드 설정여부 등등)

그래도 많은 Key-Value타입의 데이터를 저장하면서 느꼈던 경험이 있습니다.

바로 키를 잘못입력해서 생성되는 불필요한 데이터가 생성되는 점과 호출할 때 키값이 뭐였더라.. 하는 점입니다.

코드를 짜고 여러 군데에서 호출하여 데이터를 수정하다 보면 쉽게 발생할 수 있을 거라 생각합니다.

한두 개야 뭐 앱 성능에 영향 주겠어? 싶지만 좋은 습관은 아닌 거 같아서 열거형으로 키값을 분류하는 방법을 적어놓고 사용하고자 합니다!

// 키 열거형
enum UserDefaultsKey: String {
    case string
    case bool
    case array
    case object
}

// 데이터 입력
// 문자열
UserDefaults.standard.setValue("String", forKey: UserDefaultsKey.string.rawValue)
// Bool
UserDefaults.standard.setValue(true, forKey: UserDefaultsKey.bool.rawValue)
// 배열
UserDefaults.standard.setValue([1, 2, 3, 4, 5], forKey: UserDefaultsKey.array.rawValue)

// 구조체
let person = Person(name: "cheolsu", age: 20, job: "student")
let encoder = JSONEncoder()

do {
    let encoded = try encoder.encode(person)
    UserDefaults.standard.set(encoded, forKey: UserDefaultsKey.object.rawValue)
} catch {
    print("Error")
}

// 데이터 가져오기
// 문자열
UserDefaults.standard.string(forKey: UserDefaultsKey.string.rawValue)
// Bool
UserDefaults.standard.bool(forKey: UserDefaultsKey.bool.rawValue)
// 배열
UserDefaults.standard.array(forKey: UserDefaultsKey.array.rawValue)

// 구조체
let decoder = JSONDecoder()
guard let data = UserDefaults.standard.object(forKey: UserDefaultsKey.object.rawValue) as? Data else { return }
do {
    let decoded = try decoder.decode(Person.self, from: data)
    print(decoded)
} catch {
    print("Error")
}

 

 

문제

수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오.

예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {1020, 10, 30, 20, 50} 이고, 길이는 4이다.

입력

첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000,000)이 주어진다.

둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (-1,000,000,000 ≤ Ai ≤ 1,000,000,000)

출력

첫째 줄에 수열 A의 가장 긴 증가하는 부분 수열의 길이를 출력한다.

둘째 줄에는 정답이 될 수 있는 가장 긴 증가하는 부분 수열을 출력한다.

내가 푼 풀이

이제까지 가장 긴 증가하는 부분수열(LIS)를 구하는 방법은 두가지가 있었다.

첫번째는 DP방법으로 해당 인덱스의 원소까지의 부분수열 길이를 최신화 하는 방법이였다.

이는 시간복잡도가 $O(N^2)$가 걸리므로 주어진 문제의 입력은 최대 100만개의 원소이므로 시간초과가 일어났다.

 

두번째 방법으론 이분탐색이 있다.

이분탐색을 통해 이문제를 해결했지만 몇가지 주의해야할 점이 있었다.

이전에 이분탐색으로 구하면 최장증가 부분수열의 "길이"만 구할 수 있었다.

이분탐색을 통해 배열에서 원소가 위치하는 자리를 구하고 해당위치에 넣은결과 길이는 정답이지만, 부분수열이 옳지 않게 구하게 되었다.

 

예로 수열 [10, 20, 10, 30, 15, 50]이 주어졌을 때, 이분탐색을 이용하여 구한다면

[10, 15, 30, 50]배열이 구해지고 이는 최장증가 부분수열이 아니다.

이유는 15는 50이전의 원소로 30보다 나중에 나오는 원소이므로 옳지 않다.

 

부분수열은 구할 수 없지만 길이는 알고있다.

이 점을 이용하여 이분탐색을 통해 부분수열을 구할 때, 해당 원소의 인덱스를 저장한다.

괄호안 숫자는 해당원소가 주어진 수열의위치를 의미한다.

 

첫번째 원소 10

첫번째 원소는 비교대상이 없으므로 첫번째에 넣는다.

result = [10(1)]

idx = [1]

 

두번째 원소 20

두번쨰 원소는 이분탐색을 이용하여 10뒤에 오는 숫자임을 알 수 있다.

result = [10(1), 20(2)]

idx = [1, 2]

 

세번째 원소 10

세번째 원소는 이분탐색 결과 첫번째 오는 숫자와 같음을 알 수 있다.

result = [10(3), 20(2)]

idx = [1, 2, 1]

 

네번째 원소 30

네번째 원소는 20뒤에오는 숫자임을 알 수 있다.

result = [10(3), 20(2), 30(4)]

idx = [1, 2, 1, 3]

 

다섯번째 원소 15

다섯번째 원소는 이분탐색 결과 10과 20사이에 오는 숫자임을 알 수 있다.

이분탐색 결과 부분수열은 아래와 같다.

result = [10(3), 15(5), 30(4)]

idx = [1, 2, 1, 3, 2]

-> 이분탐색으로 만들어진 부분수열은 이미 정답과는 다른 부분수열이 만들어졌다. 해당원소의 위치를 배열안에 저장한다.

 

여섯번째 원소 50

여섯번째 원소는 이분탐색결과 마지막에 오는 숫자임을 알 수 있다.

result = [10(3), 15(5), 30(4), 50(6)]

idx = [1, 2, 1, 3, 2, 4]

 

-> 결과

result = [10(3), 15(5), 30(4), 50(6)]

idx = [1, 2, 1, 3, 2, 4]

 

이전의 이분탐색을 이용하면 result 배열을 얻고 길이는 정답이지만 부분 수열은 옳지않은 수열이 된다.

이분탐색을 통해 부분수열의 값이 변경될때 해당 원소가 위치하는 인덱스값을 저장함으로써 길이를 차감하며 부분수열을 구할 수 있게된다.

길이가 4이므로 역순으로 4 3 2 1의 원소를 뽑으면 [50, 30, 20 ,10]이고 이배열의 역순은 정답이 된다.

 

이분탐색을 이용하는 방법은 알았지만, 부분수열을 뽑아내는 과정은 쉽게 떠올리지 못했다.

 

코드로 구현하면 다음과 같다.

import Foundation

// 입력받기
let N = Int(readLine()!)!
var A = readLine()!.split(separator: " ").map{Int(String($0))!}
A.insert(0, at: 0)
let INF = 1000000001
// 이분탐색 결과배열과 인덱스 배열
var result = Array(repeating: INF, count: N+1)
var idxArr = [Int]()
result[1] = A[1]

// 이분탐색
func binarySearch(num: Int) {
    var left = 1
    var right = result.count-1
    
    while left <= right {
        let mid = (right + left) / 2
        
        if result[mid] < num {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }
    // 해당위치에 넣고, 인덱스배열에 저장
    result[left] = num
    idxArr.append(left)
}

for i in 1...N {
    binarySearch(num: A[i])
}

var answer = [Int]()
var count = 0

// 부분수열의 길이 구하기
for i in 1...N {
    if result[i] != INF {
        count += 1
    } else {
        break
    }
}
// 부분수열 구하기
for i in (0..<idxArr.count).reversed() {
    if idxArr[i] == count {
        count -= 1
        answer.append(A[i+1])
    }
}
// 출력
print(answer.count)
print(answer.map{String($0)}.reversed().joined(separator: " "))

 

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-17404 RGB거리 2 Swift  (1) 2024.07.16
BOJ-2631 줄세우기 Swift  (0) 2024.06.17
BOJ-1450 냅색문제 Swift  (0) 2024.06.06
BOJ-1644 소수의 연속합 Swift  (0) 2024.06.06
BOJ-11657 타임머신 Swift  (1) 2024.06.04

이 글은 아래 영어로 된 글을 번역하였습니다.

제대로 읽지 않으면 이해하기 어려울 것 같아서 따로 번역하여 올립니다.(잘못 번역된 부분이 있을 수 있습니다.)

https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3

 

Clean Architecture and MVVM on iOS

When we develop software it is important to not only use design patterns, but also architectural patterns. There are many different…

tech.olx.com

 

우리가 소프트웨어를 개발할 때 디자인패턴만 사용하는 것뿐만 아니라 소프트웨어 아키텍처 패턴을 사용하는 것도 중요합니다.

소프트웨어 개발에 있어 다양한 많은 아키텍쳐 패턴들이 있습니다.

모바일 소프트웨어 개발에는 가장 범용적으로 MVVM, Clean Architecture과 Redux 패턴을 사용합니다.

 

글에서는 iOS App MVVM패턴과 Clean Architecture 적용시킨 프로젝트 예시를 보여줍니다.

Clean Architecture 표를 보면, 애플리케이션에 서로 다른 계층이 존재하는 걸 볼 수 있습니다.

가장 중요한 규칙은 내부계층이 외부계층의 의존성을 가지면 안된다는 것입니다.

외부에서 내부로 가리키는 화살표는 의존성 규칙입니다.

오직 외부계층이 내부계층의 의존성을 갖는다는것입니다.

 

계층들을 그룹화하면 세가지로 나타낼  있습니다. : Presentation, Domain, Data

Domain Layer(Business logic)은 가장 안쪽의 계층입니다.(다른 계층과 의존성을 갖지 않는, 완벽히 분리됨)

이 계층은 Entities, Use Cases, Repository Interfaces를 포함합니다.

이 계층은 잠재적으로 다른 프로젝트 안에서 재사용될 수 있습니다.

이러한 분리를 통해서 의존성이 필요하지 않으므로, 테스트 대상 내에서 호스트 앱을 사용하지 않을 수 있습니다.

이로 인해 Domain Use Cases 테스트가 단 몇 초 만에 완료됩니다.

Domain 계층은 다른 계층의 어떤 것이든 포함시키면 안 됩니다.(ex Presentation - UIKit, SwiftUI or Data Layer)

좋은 아키텍처가 Use Cases에 집중되는 이유는 아키텍트가 환경, 도구 그리고 프레임워크에 전념하지 않고 해당 Use Cases를 지원하는 구조를 안전하게 설명할 수 있기 때문입니다.

이것을 Screaming Architecture이라고 합니다.

 

Presentation Layer은 UI를 포함합니다(UIViewControllers, or SwiftUI Views)

Views는 하나 혹은 여러 개의 Use Cases를 실행하는 ViewModel에 의해 조정됩니다.

Presentation Layer 오직 Domain Layer에만 의존합니다.

 

Data Layer은 저장소 구현과 하나 이상의 Data Source를 포함합니다.

저장소들은 다른 Data Source로부터 데이터 조정하는 역할을 갖고 있습니다.

Data Source는 원격 혹은 로컬일 수 있습니다.

Data Layer은 오직 Domain Layer에만 의존합니다.

계층에서, 네트워크 JSON 데이터 매핑을 Domain Model 추가할 있습니다.

 

아래의 구성요소들은 계층사이의 의존성과 데이터흐름을 나타냅니다.

또한 리포지토리 인터페이스를 사용하는 종속성 반전 지점을 볼 수 있습니다.

계층의 설명은 기사 초반에 말했던 예제프로젝트를 기반으로 합니다.

Data Flow(데이터 흐름)

  1. View는 ViewModel로부터 메서드를 호출합니다.
  2. ViewModel은 Use case를 실행합니다.
  3. Use Case는 사용자와 리포지토리의 데이터를 결합합니다.
  4. 각 리포지토리는 원격데이터(네트워크), 영구적 DB저장소, 메모리에 있는 데이터를 반환합니다.
  5. 정보들은 항목의 아이템을 표시하는 View로 다시 이동합니다.

Dependency Direction(의존성 방향)

  • Presentation -> Domain Layer <- Data Repositories Layer
  • Presentation Layer (MVVM) = ViewModels(Presenters) + Views(UI) (뷰와 뷰모델)
  • Domain Layer = Entities + Use Cases + Repositories Interfaces (엔티티, 유스케이스, 리포지토리 인터페이스)
  • Data Repositories Layer = Repositories Implementations + API(Network) + Persistence DB (리포지토리 구현, API, 영구적 DB)

 

Example Project: "Movies App"

예제 프로젝트 주소: https://github.com/kudoleh/iOS-Clean-Architecture-MVVM

 

GitHub - kudoleh/iOS-Clean-Architecture-MVVM: Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoor

Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoordinator, DTO, Response Caching and one of the views in SwiftUI - GitHub - kudoleh/iOS-Clean-Architecture-MVVM: Tem...

github.com

 

Domain Layer

예제프로젝트 안에서 Domain Layer을 찾아볼 수 있습니다.

여기에는 영화들을 검색하고, 최근 성공한 쿼리들을 저장하는 SearchMoviesUseCase와 Entities를 포함합니다.

또한 종속성 반전에 필요한 Data Repositories Interfaces를 포함하고 있습니다.

protocol SearchMoviesUseCase {
    func execute(requestValue: SearchMoviesUseCaseRequestValue,
                 completion: @escaping (Result<MoviesPage, Error>) -> Void) -> Cancellable?
}

final class DefaultSearchMoviesUseCase: SearchMoviesUseCase {

    private let moviesRepository: MoviesRepository
    private let moviesQueriesRepository: MoviesQueriesRepository
    
    init(moviesRepository: MoviesRepository, moviesQueriesRepository: MoviesQueriesRepository) {
        self.moviesRepository = moviesRepository
        self.moviesQueriesRepository = moviesQueriesRepository
    }
    
    func execute(requestValue: SearchMoviesUseCaseRequestValue,
                 completion: @escaping (Result<MoviesPage, Error>) -> Void) -> Cancellable? {
        return moviesRepository.fetchMoviesList(query: requestValue.query, page: requestValue.page) { result in
            
            if case .success = result {
                self.moviesQueriesRepository.saveRecentQuery(query: requestValue.query) { _ in }
            }

            completion(result)
        }
    }
}

// Repository Interfaces
protocol MoviesRepository {
    func fetchMoviesList(query: MovieQuery, page: Int, completion: @escaping (Result<MoviesPage, Error>) -> Void) -> Cancellable?
}

protocol MoviesQueriesRepository {
    func fetchRecentsQueries(maxCount: Int, completion: @escaping (Result<[MovieQuery], Error>) -> Void)
    func saveRecentQuery(query: MovieQuery, completion: @escaping (Result<MovieQuery, Error>) -> Void)
}

Use Cases를 만드는 다른 방법은 start() 함수와 함께 UseCase 프로토콜을 사용하는 것이고, 모든 UseCase는 해당 프로토콜을 준수하는 것입니다.

예제프로젝트의 Ues Case 중 하나는 이 접근 방식을 따릅니다 : FetchRecentMovieQueriesUseCase

Use Cases는 Interactors라고도 불립니다.

Use Case 다른 Use Case들과 의존될 있습니다.

 

 

Presentation Layer

Presentation Layer은 MoviesListView로부터 관찰되는 항목이 포함된 MovieListViewModel을 포함합니다.

MoviesListViewModel은 UIKit을 임폴트 하지 않습니다.

뷰모델이 UIKit, SwiftUI와 같은 모든 UI 프레임워크를 채택하지 않으면서 쉽게 재사용하고 리팩터링 할 수 있습니다.

예로, 앞으로는 뷰모델은 변하지 않으므로 UIKit에서 SwiftUI로의 Views 리팩토링은 더욱더 쉬워질 것입니다.

// Note: We cannot have any UI frameworks(like UIKit or SwiftUI) imports here. 

protocol MoviesListViewModelInput {
    func didSearch(query: String)
    func didSelect(at indexPath: IndexPath)
}

protocol MoviesListViewModelOutput {
    var items: Observable<[MoviesListItemViewModel]> { get }
    var error: Observable<String> { get }
}

protocol MoviesListViewModel: MoviesListViewModelInput, MoviesListViewModelOutput { }

struct MoviesListViewModelActions {
    // Note: if you would need to edit movie inside Details screen and update this 
    // MoviesList screen with Updated movie then you would need this closure:
    //  showMovieDetails: (Movie, @escaping (_ updated: Movie) -> Void) -> Void
    let showMovieDetails: (Movie) -> Void
}

final class DefaultMoviesListViewModel: MoviesListViewModel {
    
    private let searchMoviesUseCase: SearchMoviesUseCase
    private let actions: MoviesListViewModelActions?
    
    private var movies: [Movie] = []
    
    // MARK: - OUTPUT
    let items: Observable<[MoviesListItemViewModel]> = Observable([])
    let error: Observable<String> = Observable("")
    
    init(searchMoviesUseCase: SearchMoviesUseCase,
         actions: MoviesListViewModelActions) {
        self.searchMoviesUseCase = searchMoviesUseCase
        self.actions = actions
    }
    
    private func load(movieQuery: MovieQuery) {
        
        searchMoviesUseCase.execute(movieQuery: movieQuery) { result in
            switch result {
            case .success(let moviesPage):
                // Note: We must map here from Domain Entities into Item View Models. Separation of Domain and View
                self.items.value += moviesPage.movies.map(MoviesListItemViewModel.init)
                self.movies += moviesPage.movies
            case .failure:
                self.error.value = NSLocalizedString("Failed loading movies", comment: "")
            }
        }
    }
}

// MARK: - INPUT. View event methods
extension MoviesListViewModel {
    
    func didSearch(query: String) {
        load(movieQuery: MovieQuery(query: query))
    }
    
    func didSelect(at indexPath: IndexPath) {
        actions?.showMovieDetails(movies[indexPath.row])
    }
}

// Note: This item view model is to display data and does not contain any domain model to prevent views accessing it
struct MoviesListItemViewModel: Equatable {
    let title: String
}

extension MoviesListItemViewModel {
    init(movie: Movie) {
        self.title = movie.title ?? ""
    }
}

 

MoviesListViewModelInput과 MoviesListViewModelOutput의 인터페이스를 사용하여, 임의의 ViewModel을 쉽게 만들어 MoviesListViewContoller를 테스트할 수 있게 만듭니다.

또한 MoviesSearchFlowCoordinator에게 언제 다른 뷰를 나타낼지 전달할 MoviesListViewModelActions클로저가 있습니다.

액션 클로저가 Coordinator에 호출될 때, 영화의 자세한 내용의 뷰를 나타냅니다.

우리는 나중에 필요하다면 쉽게 많은 액션들을 추가하기 위해 구조체를 사용하여 액션을 그룹화합니다.

 

Presentation Layer은 또한 MoviesListViewModel의 데이터가 바인딩된 MoviesListViewController를 포함합니다.

 

UI는 비즈니스 로직이나 애플리케이션 로직(Business Models and UseCases)에 접근할 수 없고, 오직 뷰모델만 가능합니다.

이것이 바로 관심사의 분리입니다.

우리는 비즈니스 모델을 UI로 직접적으로 전달할 수 없습니다.

이것은 우리가 비즈니스모델을 뷰모델 내부의 뷰모델을 매핑하고 뷰에 전달하는 이유입니다.

 

우리는 또한 영화 검색을 시작하기 위해 뷰에서 뷰모델로 검색 이벤트 호출을 추가합니다.

import UIKit

final class MoviesListViewController: UIViewController, StoryboardInstantiable, UISearchBarDelegate {
    
    private var viewModel: MoviesListViewModel!
    
    final class func create(with viewModel: MoviesListViewModel) -> MoviesListViewController {
        let vc = MoviesListViewController.instantiateViewController()
        vc.viewModel = viewModel
        return vc
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        bind(to: viewModel)
    }
    
    private func bind(to viewModel: MoviesListViewModel) {
        viewModel.items.observe(on: self) { [weak self] items in
            self?.moviesTableViewController?.items = items
        }
        viewModel.error.observe(on: self) { [weak self] error in
            self?.showError(error)
        }
    }
    
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        guard let searchText = searchBar.text, !searchText.isEmpty else { return }
        viewModel.didSearch(query: searchText)
    }
}

항목들을 관찰하고, 항목이 바뀌면 뷰를 다시 불러옵니다.

여기엔 아래 MVVM섹션에서 설명할 간단한 Observable 사용합니다.

 

또한 MoviesSearchFlowCoordinator내부의 MoviesListViewModel 작업에 showMovieDetails함수를 할당하여 영화의 세부장면을 표시합니다.

 

protocol MoviesSearchFlowCoordinatorDependencies  {
    func makeMoviesListViewController() -> UIViewController
    func makeMoviesDetailsViewController(movie: Movie) -> UIViewController
}

final class MoviesSearchFlowCoordinator {
    
    private weak var navigationController: UINavigationController?
    private let dependencies: MoviesSearchFlowCoordinatorDependencies

    init(navigationController: UINavigationController,
         dependencies: MoviesSearchFlowCoordinatorDependencies) {
        self.navigationController = navigationController
        self.dependencies = dependencies
    }
    
    func start() {
        // Note: here we keep strong reference with actions closures, this way this flow do not need to be strong referenced
        let actions = MoviesListViewModelActions(showMovieDetails: showMovieDetails)
        let vc = dependencies.makeMoviesListViewController(actions: actions)
        
        navigationController?.pushViewController(vc, animated: false)
    }
    
    private func showMovieDetails(movie: Movie) {
        let vc = dependencies.makeMoviesDetailsViewController(movie: movie)
        navigationController?.pushViewController(vc, animated: true)
    }
}

ViewController의 크기와 책임을 줄이기 위해 프레젠테이션 로직에 Flow Coordinator을 사용합니다.

필요한 동안 Flow가 할당 해제 되지 않기 위해 Flow(액션 클로저, 자체기능)에 대한 강한 참조를 하고 있습니다.

 

이러한 접근방식은 다른 뷰들을 수정 없이 같은 뷰모델을 사용하기 쉽게 해 줍니다.

iOS 13.0 이상 버전을 확인하고 UIKit 대신에 SwiftUI를 만들고 같은 뷰모델로 데이터 바인딩을 합니다. 그렇지 않다면 UIKit을 생성합니다.

예제프로젝트에서 MoviesQueriesSuggestionList에 대한 SwiftUI예제를 추가해 두었습니다.

적어도 Xcode 11 베타버전이 필요합니다.

// MARK: - Movies Queries Suggestions List
func makeMoviesQueriesSuggestionsListViewController(didSelect: @escaping MoviesQueryListViewModelDidSelectAction) -> UIViewController {
   if #available(iOS 13.0, *) { // SwiftUI
       let view = MoviesQueryListView(viewModelWrapper: makeMoviesQueryListViewModelWrapper(didSelect: didSelect))
       return UIHostingController(rootView: view)
   } else { // UIKit
       return MoviesQueriesTableViewController.create(with: makeMoviesQueryListViewModel(didSelect: didSelect))
   }
}

 

 

Data Layer

Data Layer은 DefaultMoviesRepository를 포함합니다.

이것은 Domain Layer(의존성이 분리됨) 내부에 정의된 인터페이스를 준수합니다.

또한 JSON 데이터와 CoreData Entities Domain Model 매핑하는 기능을 추가합니다.

final class DefaultMoviesRepository {
    
    private let dataTransferService: DataTransfer
    
    init(dataTransferService: DataTransfer) {
        self.dataTransferService = dataTransferService
    }
}

extension DefaultMoviesRepository: MoviesRepository {
    
    public func fetchMoviesList(query: MovieQuery, page: Int, completion: @escaping (Result<MoviesPage, Error>) -> Void) -> Cancellable? {
        
        let endpoint = APIEndpoints.getMovies(with: MoviesRequestDTO(query: query.query,
                                                                     page: page))
        return dataTransferService.request(with: endpoint) { (response: Result<MoviesResponseDTO, Error>) in
            switch response {
            case .success(let moviesResponseDTO):
                completion(.success(moviesResponseDTO.toDomain()))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

// MARK: - Data Transfer Object (DTO)
// It is used as intermediate object to encode/decode JSON response into domain, inside DataTransferService
struct MoviesRequestDTO: Encodable {
    let query: String
    let page: Int
}

struct MoviesResponseDTO: Decodable {
    private enum CodingKeys: String, CodingKey {
        case page
        case totalPages = "total_pages"
        case movies = "results"
    }
    let page: Int
    let totalPages: Int
    let movies: [MovieDTO]
}
...
// MARK: - Mappings to Domain

extension MoviesResponseDTO {
    func toDomain() -> MoviesPage {
        return .init(page: page,
                     totalPages: totalPages,
                     movies: movies.map { $0.toDomain() })
    }
}

데이터 전송객체 DTO는 JSON응답을 Domain으로 매핑하는 중간 객체로 사용됩니다.

또한 응답의 마지막을 캐시한다면 영구 데이터전송객체를 영구객체에 매핑하여 영구 저장소에 저장됩니다.

(마지막 응답값을 데이터전송객체를 매핑하여 저장소에 저장한다는 얘기인듯합니다..)

(인터넷 접속과 관계없이 최근에 불러온 데이터를 보여주기 위해서 저장해 두는 것?)

 

일반적인 데이터저장소는 API 데이터서비스와 영구저장소와 함께 주입될 수 있습니다.

데이터 저장소는 두 개의 의존성을 갖고 데이터를 반환합니다.

규칙은 먼저 캐시 된 데이터를 출력하기위해 영구저장소를 호출하는것입니다.(NSManagerObject는 DTO 객체를 통해 매핑되고, 캐시된 데이터클로저에서 검색됨)

그다음 최신 업데이트된 데이터를 리턴할 API Data Service를 호출합니다.

그 다음 영구 저장소는 최신으로 업데이트됩니다.(DTO가 영구객체에 매핑되고 저장됩니다.)

그 다음 DTO는 Domain으로 매핑되고 업데이트된 데이터/ 완료 클로저에서 검색됩니다.

이 방식은 유저들이 즉각적으로 데이터를 볼 수 있습니다.

네트워크 연결이 없음에도 사용자들은 영구저장소로부터 가장 최신의 데이터를 볼 수 있습니다.

 

저장소와 API는 완전히 다른 구현으로 대체될 수 있습니다(예를 들어 CoreData에서 Realm으로)

앱의 모든 나머지 계층들은 이러한 변화에 영항을 받지 않지만 DB 세부사항이기 떄문입니다.

 

Infrastructure Layer(Network)

이것은 네트워크 프레임워크를 둘러싼 래퍼입니다. Alamofire일 수 있습니다.(아니면 다른 네트워크 프레임워크)

이것은 네트워크 파라미터들로 구성할 수 있습니다.(예를 들어 기본 URL)

또한 엔드포인트 정의를 지원하고 데이터 매핑 메서드를 포함합니다(Decodable 사용한)

struct APIEndpoints {
    
    static func getMovies(with moviesRequestDTO: MoviesRequestDTO) -> Endpoint<MoviesResponseDTO> {

        return Endpoint(path: "search/movie/",
                        method: .get,
                        queryParametersEncodable: moviesRequestDTO)
    }
}


let config = ApiDataNetworkConfig(baseURL: URL(string: appConfigurations.apiBaseURL)!,
                                  queryParameters: ["api_key": appConfigurations.apiKey])
let apiDataNetwork = DefaultNetworkService(session: URLSession.shared,
                                           config: config)

let endpoint = APIEndpoints.getMovies(with: MoviesRequestDTO(query: query.query,
                                                             page: page))
dataTransferService.request(with: endpoint) { (response: Result<MoviesResponseDTO, Error>) in
    let moviesPage = try? response.get()
}

 

 

MVVM

Model-View-ViewModel 패턴은 UI와 Domain사이의 관심사를 완전한 분리됨을 제공합니다.

클린 아키텍처와 함께 사용하면 Presentation과 UI 계층의 관심사 분리에 도움을 줍니다.

 

다른 뷰 구현은 같은 뷰모델이 사용될 수 있습니다.

예를 들어, CarsAroundListView와 CarsAroundMapView는 둘 다 CarsAroundViewModel을 사용할 수 있습니다.

또한 하나의 UIKit 뷰와 다른 하나의 SwiftUI도 같은 뷰모델로 지정할 수 있습니다.

뷰모델 내부에 UIKit, WatchKit, SwiftUI를 임폴트하지 않는 것을 기억하는 것이 중요합니다.

이러한 방식은 필요하다면 다른 플랫폼에서 쉽게 재사용될 있다는점입니다.

View와 ViewModel 간의 데이터바인딩은 예시로 클로저, 델리게이트 또는 관찰자(RxSwift 같은)로 이루어집니다.

Combine과 SwiftUI 또한 사용될 수 있지만 최소한의 iOS 13 버전이상을 사용해야 합니다.

View는 ViewModel과 직접적인 관계가 있으며, View내부에서 이벤트가 발생할 때마다 ViewModel에게 전달합니다

ViewModel에서는 View에 대한 직접참조는 없습니다.(데이터바인딩만 참조)

이 예시에서, 우리는 타사 의존성을 피하기 위해 클로저의 와 didSet의 간단한 조합을 사용합니다.

public final class Observable<Value> {
    
    private var closure: ((Value) -> ())?

    public var value: Value {
        didSet { closure?(value) }
    }

    public init(_ value: Value) {
        self.value = value
    }

    public func observe(_ closure: @escaping (Value) -> Void) {
        self.closure = closure
        closure(value)
    }
}

이것은 여러 관찰자와 관찰자 제거가 포함된 전체 구현을 볼 수 있는 매우 간단한 버전의 Observable입니다.

 

ViewController의 데이터바인딩 예시:

final class ExampleViewController: UIViewController {
    
    private var viewModel: MoviesListViewModel!
    
    private func bind(to viewModel: ViewModel) {
        self.viewModel = viewModel
        viewModel.items.observe(on: self) { [weak self] items in
            self?.tableViewController?.items = items
            // Important: You cannot use viewModel inside this closure, it will cause retain cycle memory leak (viewModel.items.value not allowed)
            // self?.tableViewController?.items = viewModel.items.value // This would be retain cycle. You can access viewModel only with self?.viewModel
        }
        // Or in one line
        viewModel.items.observe(on: self) { [weak self] in self?.tableViewController?.items = $0 }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bind(to: viewModel)
        viewModel.viewDidLoad()
    }
}


protocol ViewModelInput {
    func viewDidLoad()
}

protocol ViewModelOutput {
    var items: Observable<[ItemViewModel]> { get }
}

protocol ViewModel: ViewModelInput, ViewModelOutput {}

클로저 관찰에서 뷰모델 접근은 허용하지 않습니다. 이것은 메모리 릭 현상을 발생하기 때문입니다.

오직 self와 함께 뷰모델을 접근할 수 있습니다: self?.viewModel

 

final class MoviesListItemCell: UITableViewCell {

    private var viewModel: MoviesListItemViewModel! { didSet { unbind(from: oldValue) } }
  
    func fill(with viewModel: MoviesListItemViewModel) { 
        self.viewModel = viewModel
        bind(to: viewModel)
    }
    
    private func bind(to viewModel: MoviesListItemViewModel) {
        viewModel.posterImage.observe(on: self) { [weak self] in self?.imageView.image = $0.flatMap(UIImage.init) }
    }
    
    private func unbind(from item: MoviesListItemViewModel?) {
        item?.posterImage.remove(observer: self)
    }
}

뷰가 재사용가능하다면 바인딩을 해제해야 합니다.(테이블  셀)

 

MVVMs Communication (MVVM의 통신)

Delegation

하나의 MVVM 뷰모델은 델리게이트 패턴을 사용하여 또다른 하나의 MVVM 뷰모델과 통신합니다.

예를 들어, ItemsListViewModel과 ItemEditViewModel이 있습니다.

그다음 ItemEditViewModelDidEditItem메서드를 갖는 ItemEditViewModelDelegate 프로토콜을 만듭니다.

그리고 이것을 다음 프로토콜을 채택하도록 만듭니다. Extension ListItemsviewModel: ItemEditViewModelDelegate

// Step 1: Define delegate and add it to first ViewModel as weak property
protocol MoviesQueryListViewModelDelegate: class {
    func moviesQueriesListDidSelect(movieQuery: MovieQuery)
}
...
final class DefaultMoviesQueryListViewModel: MoviesListViewModel {
    private weak var delegate: MoviesQueryListViewModelDelegate?
    
    func didSelect(item: MoviesQueryListViewItemModel) { 
        // Note: We have to map here from View Item Model to Domain Enity
        delegate?.moviesQueriesListDidSelect(movieQuery: MovieQuery(query: item.query))
    }
}

// Step 2:  Make second ViewModel to conform to this delegate
extension MoviesListViewModel: MoviesQueryListViewModelDelegate {
    func moviesQueriesListDidSelect(movieQuery: MovieQuery) {
        update(movieQuery: movieQuery)
    }
}

경우 델리게이트를 Responders 지정할 있습니다: ItemEditViewModelResponder

 

Closures

또 다른 통신방법은 FlowCoordinator로부터 주입되거나 할당받은 클로저를 사용하는 것입니다.

이 예제프로젝트에서는 MoviesListViewModel이 액션 클로저 showMovieQueriesSuggestions를 사용하여 MoviesQueriesSuggestionsView를 표시하는 방법을 볼 수 있습니다.

또한 매개변수 (_ didSelect: MovieQuery) -> Void를 전달하여 해당 View에서 재호출 할 수 있습니다.

통신은 MoviesSearchFlowCoordinator내부와 연결되어 있습니다.

// MoviesQueryList.swift

// Step 1: Define action closure to communicate to another ViewModel, e.g. here we notify MovieList when query is selected
typealias MoviesQueryListViewModelDidSelectAction = (MovieQuery) -> Void

// Step 2: Call action closure when needed
class MoviesQueryListViewModel {
    init(didSelect: MoviesQueryListViewModelDidSelectAction? = nil) {
        self.didSelect = didSelect
    }
    func didSelect(item: MoviesQueryListItemViewModel) {
        didSelect?(MovieQuery(query: item.query))
    }
}

// MoviesQueryList.swift

// Step 3: When presenting MoviesQueryListView we need to pass this action closure as paramter (_ didSelect: MovieQuery) -> Void
struct MoviesListViewModelActions {
    let showMovieQueriesSuggestions: (@escaping (_ didSelect: MovieQuery) -> Void) -> Void
}

class MoviesListViewModel { 
    var actions: MoviesListViewModelActions?

    func showQueriesSuggestions() {
        actions?.showMovieQueriesSuggestions { self.update(movieQuery: $0) } 
        //or simpler actions?.showMovieQueriesSuggestions(update)
    }
}

// FlowCoordinator.swift

// Step 4: Inside FlowCoordinator we connect communication of two viewModels, by injecting actions closures as self function
class MoviesSearchFlowCoordinator {
    func start() {
        let actions = MoviesListViewModelActions(showMovieQueriesSuggestions: self.showMovieQueriesSuggestions)
        let vc = dependencies.makeMoviesListViewController(actions: actions)  
        present(vc)
    }

    private func showMovieQueriesSuggestions(didSelect: @escaping (MovieQuery) -> Void) {
        let vc = dependencies.makeMoviesQueriesSuggestionsListViewController(didSelect: didSelect)
        present(vc)
    }
}

 

 

Layer Separation into frameworks (Modules)

이제 예제 앱의 각 계층(도메인, 프레젠테이션, UI, 데이터, 인프라네트워크)들은 별도의 프레임워크와 쉽게 분리할 수 있습니다.

그다음 CocoaPod 사용해 이러한 프레임워크를 당신의 기본 앱에 포함시킬 있습니다.

 

Dependency Injection Container (의존성 주입 컨테이너?)

의존성 주입은 한 객체가 다른 객체의 종속성을 제공하는 기술입니다.

앱 내 DIContainer 모든 주입의 중심단위입니다.

 

종속성 팩토리 프로토콜 사용

많은 보기 중 하나는 종속성 생성을 DIContainer에 위임하는 종속성 프로토콜을 선언하는 것입니다.

이를 수행하기 위해 MoviesSearchFlowCoordinatorDependencies프로토콜을 정의하고 MoviesSceneDIContainer가 이 프로토콜을 준수하도록 생성합니다. 그다음 이 주입이 필요한 MoviesSearchFlowCoordinator에 주입하여 MoviesListViewController를 생성하고 표시해야 합니다.

여기에 단계가 있습니다.

// Define Dependencies protocol for class or struct that needs it
protocol MoviesSearchFlowCoordinatorDependencies  {
    func makeMoviesListViewController() -> MoviesListViewController
}

class MoviesSearchFlowCoordinator {
    
    private let dependencies: MoviesSearchFlowCoordinatorDependencies

    init(dependencies: MoviesSearchFlowCoordinatorDependencies) {
        self.dependencies = dependencies
    }
...
}

// Make the DIContainer to conform to this protocol
extension MoviesSceneDIContainer: MoviesSearchFlowCoordinatorDependencies {}

// And inject MoviesSceneDIContainer `self` into class that needs it
final class MoviesSceneDIContainer {
    ...
    // MARK: - Flow Coordinators
    func makeMoviesSearchFlowCoordinator(navigationController: UINavigationController) -> MoviesSearchFlowCoordinator {
        return MoviesSearchFlowCoordinator(navigationController: navigationController,
                                           dependencies: self)
    }
}

클로저 사용

또 다른 옵션은 클로저를 사용하는 것입니다.

이를 수행하기 위해 주입이 필요로 하는 클래스 내부에 클로저 정의가 필요합니다. 그리고 클로저를 주입합니다.

// Define makeMoviesListViewController closure that returns MoviesListViewController
class MoviesSearchFlowCoordinator {
   
    private var makeMoviesListViewController: () -> MoviesListViewController

    init(navigationController: UINavigationController,
         makeMoviesListViewController: @escaping () -> MoviesListViewController) {
        ...
        self.makeMoviesListViewController = makeMoviesListViewController
    }
    ...
}

// And inject MoviesSceneDIContainer's `self`.makeMoviesListViewController function into class that needs it
final class MoviesSceneDIContainer {
    ...
    // MARK: - Flow Coordinators
    func makeMoviesSearchFlowCoordinator(navigationController: UINavigationController) -> MoviesSearchFlowCoordinator {
        return MoviesSearchFlowCoordinator(navigationController: navigationController,
                                           makeMoviesListViewController: self.makeMoviesListViewController)
    }
    
    // MARK: - Movies List
    func makeMoviesListViewController() -> MoviesListViewController {
        ...
    }
}

 

 

 

결론

모바일 개발에서 대부분 사용된 아키텍처 패턴들은 클린 아키텍처, MVVM, Redux입니다.

MVVM과 클린 아키텍처는 별도로 사용할 수 있습니다. 그러나 MVVM은 오직 Presentation Layer내부에서의 관심사 분리만 제공하지만, 클린아키텍처는 코드를 쉽게 테스트하고 재사용하고 이해하도록 모듈식 계층을 분할합니다.

유스케이스가 저장소를 호출하는 것 외에 다른 작업이 없더라도 유스케이스 생성을 건너뛰지 않는 것이 중요합니다.

이 방식은, 당신의 아키텍처가 새로운 개발자가 당신의 유스케이스들을 봤을 때 자체적으로 설명될 것입니다.

 

이것은 출발점으로 유용할지라도, 모든 면에서 적합하진 않습니다.

당신의 프로젝트에서 요구사항을 충족하는 아키텍처를 고르세요.

 

클린 아키텍처는 TDD에서 매우 잘 작동합니다(Test Driven Development)(테스트 중심 개발)

이 아키텍쳐는 프로젝트를 테스트하기 용이하게 만들어주고 계층들을 쉽게 교체할 수 있습니다.(UI나 데이터)

도메인 기반 설계는 클린 아키텍쳐와 맞습니다.


URLSession


URLSession은 네트워크 작업을 처리하기 위해 구현된 클래스입니다.

저는 프로젝트에서 에러 핸들링 과정과 함께 URLSession을 아래와 같이 사용했습니다.

// URLSession NetworkError
enum NetworkError: Error {
    case invalidUrl
    case transportError
    case serverError(code: Int)
    case missingData
    case decodingError(error: Error)
}

// URLSessionConfiguration
private let session = URLSession(configuration: .default)
// 반환 타입 정의
typealias NetworkResult = (Result<WeatherDataModel, NetworkError>) -> ()

// URLSession GET Data
func dataFetch(nx: Int, ny: Int, convenience: Bool, completion: @escaping NetworkResult) {
    
    // URL 확인
        let urls = getUrl(convenience: convenience, nx: nx, ny: ny)
        for url in urls {
            guard let url = url.url else {
            completion(.failure(.invalidUrl))
            return
        }
        var request: URLRequest = URLRequest(url: url)
        request.httpMethod = "GET"
        let dataTask = session.dataTask(with: request) { data, response, error in
            // 연결 확인
            guard error == nil else {
                completion(.failure(.transportError))
                print(error?.localizedDescription)
                return
            }
            // 서버 확인
            let successRange = 200..<300
            guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { return }
            if !successRange.contains(statusCode) {
                completion(.failure(.serverError(code: statusCode)))
                return
            }
            // 데이터 확인
            guard let loadData = data else {
                completion(.failure(.missingData))
                return
            }
            // 디코딩
            do {
                let parsingData: WeatherDataModel = try
                JSONDecoder().decode(WeatherDataModel.self, from: loadData)
                completion(.success(parsingData))
            } catch let error {
                completion(.failure(.decodingError(error: error)))
            }
        }.resume()
    }
}

 

최신기술?인 async await을 사용하지않고 escaping 클로저로 비동기 데이터 통신을 처리했습니다.

 

절차는 아래와 같습니다!

  1. URL 생성 (URL생성에 문제가 생기면 오류리턴)
  2. 생성된 URL을 바탕으로 URLComponent를 만들고, URLRequest 생성
  3. URLSession을 통해 데이터 통신 (클로저 안에서 진행되고, 리턴값은 escaping 선언에 따라 해당 함수가 종료되면 실행)
  4. 네트워크 연결, 서버연결, 데이터불러오기 확인절차를 모두 진행하고 실패하면 오류 리턴
  5. 위 사항까지 모두 정상적으로 진행되었다면 디코딩 진행(JSON 모델과 받을 데이터의 모델과 형식이 같은지 주의)
  6. 디코딩이 완료되면 해당 데이터를 리턴

 

이렇게 작성하고 데이터를 받는 부분에서 불러올땐 클로저를 통해 데이터를 받았습니다.

APIManager.shared.dataFetch(nx: location.x, ny: location.y, convenience: false) {[weak self] result in
    guard let self = self else { return }
    switch result {
    case .success(let data):
        self.convertDataFromCategory(response: data)
    case .failure(.decodingError(error: let error)):
        print("decodingError: \(error.localizedDescription)")
    case .failure(.invalidUrl):
        print("invalidURL")
    case .failure(.missingData):
        print("missingData")
    case .failure(.serverError(code: let code)):
        print("serverError \(code)")
    case .failure(.transportError):
        print("transportError")
    }
}

 

이렇게 네트워크 통신작업을 처리했는데 이것을 RxSwift로 리팩토링 하고자 합니다.

 

 

RxSwift URLSession


 

RxSwift는 반응형 프로그래밍으로 이전에 작성했던 코드와는 다른 개념이지만, RxSwift는 Swift에서 사용하던 UI나 프로토콜등 많은 프레임워크가 Reactive 형식으로 확장되서 지원하고있습니다.

URLSession도 Swift나 Objective-C로 작성되어 사용되었지만 Rx버전으로도 확장되었습니다.

extension Reactive where Base: URLSession {
    public func response(request: URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
        return Observable.create { observer in
            let d: Date?
            // shouldLogRequest: RxCocoa확장 라이브러리를 사용할 때, 네트워크 요청 로깅을 제어하기 위해 사용하는 함수?
            // 디버깅 목적으로 사용
            // 정확한 역할은 모르지만,, 코드 흐름을 보고 유추하자면.. 라이브러리 네트워크 연결이 됬다면 true를 리턴하는듯 하다.
            if URLSession.rx.shouldLogRequest(request) {
                d = Date()
            }
            else {
                d = nil
            }
            // URLSession Task
            let task = self.base.dataTask(with: request) { data, response, error in
                if URLSession.rx.shouldLogRequest(request) {
                    let interval = Date().timeIntervalSince(d ?? Date())
                    // HTTPMethod, URLRequest의 url문자열 출력 (올바른 url이 아니면 "<unknown url>"
                    print(convertURLRequestToCurlCommand(request))
                    #if os(Linux)
                    print(convertResponseToString(response, error.flatMap { $0 as NSError }, interval))
                    #else
                    // 네트워크 연결상태 확인 status code (200..<300)
                    print(convertResponseToString(response, error.map { $0 as NSError }, interval))
                    #endif
                }
                // 응답오류, 데이터 불러오기 오류
                guard let response = response, let data = data else {
                    observer.on(.error(error ?? RxCocoaURLError.unknown))
                    return
                }
                // HTTP응답없음 오류 리턴
                guard let httpResponse = response as? HTTPURLResponse else {
                    observer.on(.error(RxCocoaURLError.nonHTTPResponse(response: response)))
                    return
                }
                // 응답값과 데이터 리턴 후 종료
                observer.on(.next((httpResponse, data)))
                observer.on(.completed)
            }
            
            task.resume()
            // Observable Dispose
            return Disposables.create(with: task.cancel)
        }
    }
}

 

※ 코드 분석

  • URLRequest를 인자로 받아 Observable<(response, data)> 형식으로 리턴한다.
  • 라이브러리와 네트워크 통신을 디버깅목적으로 확인하는URLSession.rx.shouldLogRequest(request)을 거친다.
  • 서버 통신확인, 서버로부터 응답과 url응답 확인한다. 오류가 발생한다면 오류 이벤트를 방출한다.
  • 정상적으로 통신이 됬다면 해당 http응답값과 data를 이벤트 방출하고 종료한다.

URLResponse: 네트워크요청에 대한 응답을 나타낸다.

HTTPResponse: HTTP프로토콜을 사용한 네트워크에 대한 응답을 나타낸다. (URLResponse의 서브클래스)

 

 

사용 예시

func getData(nx: Int, ny: Int, convenience: Bool) -> Observable<WeatherDataModel> {
    // components의 리턴값은 Observable<URLComponents>
    return components(nx: nx, ny: ny, convenience: convenience)
        // URLComponents를 URLRequest로 변환
        .map{ url in
            return URLRequest(url: url.url!)
        }
        // 변환된 URLRequest를 통해 통신
        // URLSession.shared.rx.response에는 이미 에러대응이 구현되어있음.
        // 리턴값은 Observable<(response: HTTPURLResponse, data: Data)>
        .flatMap { request -> Observable<(response: HTTPURLResponse, data: Data)> in
            return URLSession.shared.rx.response(request: request)
        }
        // response, data를 인자로 받고 정한 데이터모델로 디코딩 (실패시 임의의 값 넣기)
        .map{ _, data -> WeatherDataModel in
            let decoder = JSONDecoder()
            let decoded = try? decoder.decode(WeatherDataModel.self, from: data)
            return decoded ?? WeatherDataModel(response: DataResponse(header: ResponseHeader(resultCode: "04"), body: nil))
        }
}

 

RxURLSession은 에러대응이 구현되어 있지만, 디코딩 에러에 대한 대응은 따로 구현해줘야 합니다.

또한 리턴값이 Observable이지만 네트워크 통신같은 경우는 성공과 실패의 결과만 있어도 되므로 Traits중 하나인 Single을 리턴하도록 합니다.

 

Single


Observable보다 더 좁은 범위로 .success 와 .error만 방출한다.

데이터 로딩과 같은 1회성 프로세스에 적합하다.

Observable에서 asSingle로 변환할 수 있다.

 

typealias NetworkResult = Result<WeatherDataModel, NetworkError>

func example(nx: Int, ny: Int, page: Int) -> Single<NetworkResult> {
        return Single<NetworkResult>.create {[weak self] single in
            // URL 확인
            guard let components = self?.component(nx: nx, ny: ny, page: page) else {
                single(.failure(NetworkError.invalidUrl))
                return Disposables.create()
            }
            
            let request = URLRequest(url: components.url!)
            
            URLSession.shared.dataTask(with: request) { data, response, error in
                // 에러 확인
                if let error = error {
                    single(.failure(NetworkError.transportError))
                    return
                }
                
                let successRange = 200..<300
                guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { return }
                // 접속 코드 확인
                if !successRange.contains(successRange) {
                    single(.failure(NetworkError.serverError(code: statusCode)))
                    return
                }
                // 데이터 확인
                guard let loaded = data else {
                    single(.failure(NetworkError.missingData))
                    return
                }
                // 디코딩 확인
                do {
                    let decoded = try JSONDecoder().decode(WeatherDataModel.self, from: loaded)
                    single(.success(NetworkResult.success(decoded)))
                } catch {
                    single(.failure(NetworkError.decodingError))
                }
            }.resume()
            
            
            return Disposables.create()
        }
    }

 

문제

RGB거리에는 집이 N개 있다. 거리는 선분으로 나타낼 수 있고, 1번 집부터 N번 집이 순서대로 있다.

집은 빨강, 초록, 파랑 중 하나의 색으로 칠해야 한다. 각각의 집을 빨강, 초록, 파랑으로 칠하는 비용이 주어졌을 때, 아래 규칙을 만족하면서 모든 집을 칠하는 비용의 최솟값을 구해보자.

  • 1번 집의 색은 2번, N번 집의 색과 같지 않아야 한다.
  • N번 집의 색은 N-1번, 1번 집의 색과 같지 않아야 한다.
  • i(2 ≤ i ≤ N-1)번 집의 색은 i-1, i+1번 집의 색과 같지 않아야 한다.

입력

첫째 줄에 집의 수 N(2 ≤ N ≤ 1,000)이 주어진다. 둘째 줄부터 N개의 줄에는 각 집을 빨강, 초록, 파랑으로 칠하는 비용이 1번 집부터 한 줄에 하나씩 주어진다. 집을 칠하는 비용은 1,000보다 작거나 같은 자연수이다.

출력

첫째 줄에 모든 집을 칠하는 비용의 최솟값을 출력한다.

 

접근방법: DP

해당문제를 풀기전 규칙을 먼저 이해해야한다. 규칙은 단순하다.

직선 위 N개의 집이 주어졌을 때, 서로 인접한 집과의 색이 같지 않고, 첫번째와 마지막의 집의 색이 같으면 안된다.

직선을 원으로 만들어 첫번째와 마지막 집을 인접시키면 모든 집은 인접한 집과 다른 색을 갖는다는 점과 같지만

이 문제에선 이 방식은 사용하지않지만 규칙을 이해하는데 쉬웠다.

 

첫번째 집부터 마지막집까지 색칠하는 과정을 거치는데 첫번째와 마지막집의 색이 같으면 안되므로 첫번째집의 색을 알고 있어야한다.

이를 DP방식을 이용하여 풀려면 이전까지 색칠한 비용을 알고 이용하여 그다음 집까지 색칠비용을 구한다.

 

인접한 집과 색이 같으면 안되므로 

첫번째 집이 빨강이면 두번째 집은 파랑과 초록이 가능하다

두번째 집이 파랑이라면 세번째 집은 빨강과 초록이 가능하다.

세번째 집이 초록이라면 네번째 집은 빨강과 파랑이 가능하다.

 

주어진 비용을 2차원 배열로 저장하면 빨강: 0, 초록: 1, 파랑: 2 인덱스를 갖는 배열이 된다.

위 방식을 사용하면 dp배열을 아래와 같이 정의할 수 있다.

dp[n][0]: n번째 집을 빨강으로 색칠한 최소비용

dp[n][1]: n번째 집을 초록으로 색칠한 최소비용

dp[n][2]: n번째 집을 파랑으로 색칠한 최소비용

dp[n][0] = arr[n][0] + min(dp[n-1][1], dp[n-1][2]) (arr: 색칠비용이 담긴 2차원배열)

 

이렇게 마지막 집까지 모두 색칠했을 때, 마지막 집과 첫번째 집의 색이 다른 경우의 비용을 갱신하여 값을 구한다.

 

코드로 구현하면 다음과 같다.

import Foundation

// 입력받기
let N = Int(readLine()!)!
var arr = [[0]]
let INF = 100000000
var ans = INF
for _ in 1...N {
    arr.append(readLine()!.split(separator: " ").map{Int(String($0))!})
}

// 첫번째 집이 어떤색을 칠하는지? 가 기준이 된다.
// 집이 나열된 선분을 순환구조로 나타낸다면, 인접한 집과의 색은 같으면 안된다.
// 첫번째 집을 세가지 색 모두 칠해보고 색칠비용의 최솟값을 구한다.
for i in 0...2 {
    // dp[i][j]: i번째집을 j색으로 칠했을 때의 최소비용
    var dp = Array(repeating: Array(repeating: 0, count: 3), count: N+1)
    // 첫번째 집과 마지막집의 색이 같으면 안되므로, 우리는 첫번째 집의 색을 기억하고 있어야한다.
    // 따라서 첫번째 집의 색 이외의 경우의수는 임의의 최댓값을 넣는다.
    for j in 0..<3 {
        if i == j {
            dp[1][j] = arr[1][j]
        } else {
            dp[1][j] = INF
        }
    }
    
    // 규칙에따라 현재집은 이전의 집과 다른색을 칠해야한다.
    // n번째 집까지 색칠한 최소비용: n-1번째까지 칠한 최소비용 + n번째 집의 색칠비용
    for j in 2...N {
        dp[j][0] = arr[j][0] + min(dp[j-1][1], dp[j-1][2])
        dp[j][1] = arr[j][1] + min(dp[j-1][0], dp[j-1][2])
        dp[j][2] = arr[j][2] + min(dp[j-1][0], dp[j-1][1])
    }
    // 첫번째 집과 마지막번째 집의 색이 같지 않다면, 최소비용을 갱신한다.
    for k in 0..<3 {
        if i != k {
            ans = min(ans, dp[N][k])
        }
    }
}
print(ans)

 

'코딩테스트 > 백준' 카테고리의 다른 글

BOJ-14003 가장 긴 증가하는 부분 수열 5 Swift  (0) 2024.08.16
BOJ-2631 줄세우기 Swift  (0) 2024.06.17
BOJ-1450 냅색문제 Swift  (0) 2024.06.06
BOJ-1644 소수의 연속합 Swift  (0) 2024.06.06
BOJ-11657 타임머신 Swift  (1) 2024.06.04

+ Recent posts