LuckVII_1 (feat.Concurrency)

봄이아빠·4일 전
1

Sparta Project

목록 보기
8/10
post-thumbnail

LuckVII

Sparta에서 진행하는 영화 예매 프로젝트 LuckVII에서 https://www.themoviedb.org 라는 영화 정보 사이트에서 api로 정보를 요청하여 받아오는 부분을 맡았다.

📂 NetworkService  
│  
├── 📄 ImageManager  
│  
├── 📄 MovieData  
│  
├── 📄 MovieDataManager  
│  
├── 📄 MovieData+Extension  
│  
├── 📄 NetworkError  
│  
├── 📄 TypeAlias  
│  
└── 📄 UIImage+Extension  

TMDB

일단 TMDB에서 api에 GET 요청 시 url 베이스는 다음과 같다.

저기 중간의 /now_playing이라는 엔드포인트를 upcoming과 popular로 바꾸어가며 받아올 예정이며 파라미터로 개인 api 요청 키와 언어, 지역 등을 설정한다.

이번엔 코드를 전부 짠 뒤에서야 await/async의 존재를 알게 되어서 작성한 메서드를 기존 방식과 호환 가능한 메서드, 새로운 방식에 맞춘 메서드를 모두 작성하느라 시간이 꽤 걸렸다.

일단 가장 먼저 한 일은 해당 url로 요청 시 받아오는 정보 중 필요한 것들이 뭔지 정하고 Decodable을 준수하는 타입을 정의하는 것이었다.

Response


사이트에 친절히 어떤 데이터를 돌려주는지 알려주고 있으며 뷰를 만들고 있는 팀원들과 상의하여 우선 다음의 데이터만을 가져와보기로 결정했다.

  • adult
  • originalLanguage
  • overview
  • popularity
  • posterPath

이 5가지 정보를 가진 Movie라는 구조체를 만들고
Movie 배열을 가진 MovieData를 만들어 받아온 데이터를 저장했다.
이후 ui구성을 위해 추가적인 데이터를 가져오게 되었다.

MovieData

지난번 포켓몬 연락처에서 작성한 것과 달라진 점이 2가지있다.
그 첫번째가 Decodable이다.
기존에는 큰 생각 없이 Codable을 준수하는 struct를 작성했다.
그러나 GET 요청만 수행하는 내 코드에서 Encodable을 준수할 필요는 없다는 걸 튜터님의 피드백으로 알게 되었다.

Codable


Codable, Encodable, Decodable을 하나씩 알아보자.
Codable은 위 사진처럼 Incodable과 Decodable을 단순히 typealias로 지정해둔 것이다.

Encodable은 데이터를 직렬화 하는데 사용하는 것이다.
Serialization(직렬화)이란 객체나 데이터를 특정 형식(바이트 스트림)으로 변환하여 저장하거나 전송할 수 있도록 만드는 과정이라고 한다.
직렬화를 거친 데이터는 파일, 네트워크, 데이터 베이스 등으로 전달될 수 있고 역직렬화를 통해 다시 원래의 객체로 복원할 수 있다.

스위프트에서만 사용하는 형식을 외부 시스템이 이해할 수 있는 JSON, XML, plist 등으로 변환하거나 외부 시스템이 사용하는 형식의 데이터를 스위프트에서 사용하기 위한 과정들이다.

피드백 받은 Codable과 Decodable의 차이는 결국 Encodable을 준수하는지와 그로 인해 직렬화가 가능한지에서 차이가 난다.

인코딩을 하지 않을 것임에도 인코딩을 할 수 있도록 하는 것은 불필요한 책임을 지게 하는 것이며 코드의 의도가 불명확해질 수 있다.
때문에 명시적으로 인코딩 가능성을 배제하고 읽기 전용 데이터 모델임을 확실히 하는 것이 더 좋은 코드라고 할 수 있겠다.

Codingkey -> .keyDecodingStrategy

두 번째는 api 응답으로 돌려 받는 데이터의 이름과 스위프트에서 사용하는 데이터의 네이밍 컨벤션을 일치시키는 작업의 변화다.
기존엔 CodinKey를 사용한 열거형을 정의하여 수동으로 매핑해주었다.
예를 들어 now_playing이 있다면 직접 해당 케이스에 nowPlaying이라는 값을 매핑시킨 것이다.

이러한 방식은 세밀한 매핑이 가능하고 원하는 특정 키만 변환하는 등의 장점이 있다. 대신 이름이 다른 프로퍼티가 많을수록 일일이 작성해야 하는 번거로움이 동반될 수 있다.

대신 이번에 채택한 것은 Decoder의 속성 중 하나인 .keyDecodingStrategy를 설정해주는 것이다.

keyDecodingStrategy는 JSON의 key와 Swift의 프로퍼티 이름 간 차이를 자동으로 처리하여 작업을 줄이고 코드의 양을 감소시켜준다.

이번에 사용한 것은 정확히 keyDecodingStrategy의 설정을 .convertFromSnakeCase로 바꾸어 준 것이다.

JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase는 위 설명대로 3가지의 규칙에 따라 이름을 자동으로 변환시켜준다.

  1. 밑줄 뒤에 오는 첫 글자를 대문자로 변경
  2. 문자열의 시작이나 끝에 있는 밑줄 제거
  3. 밑줄을 모두 제거하고 하나의 문자열로 결합

ex)fee_fi_fo_fum >> feeFiFoFum

간단하게 일반화된 변환처리를 할 수 있어 편리하지만 약어와 이니셜리즘의 대소문자를 정확히 처리하지 못할 수 있다.

예를 들어 base_url을 baseURL로 변환되기를 희망하더라도 baseUrl로만 자동변환될 것이다.

이러한 경우엔 codingKey를 직접 정의해주어 명시적으로 지정해주어야 한다.
(즉 어느 하나만을 사용할 수 있는 것이 아닌 동시 사용이 가능하다는 말)

key 디코딩 전략 외에도 위와 같은 날짜, 데이터, 비호환 부등소수점 값 등에 대한 전략도 설정할 수 있다.
아마 프로젝트 진행 과정에서 날짜 정보가 추가적으로 필요할 것이라 생각되기에 그때 다시 dateDecodingStrategy에 대해 알아보겠다.

MovieDataManager

이제 데이터를 요청하고 MovieData를 반환하여 주는 객체를 살펴보겠다.
이번 fetch메서드를 작성하는데 가장 오래 걸린 부분이 기존의 클로저 콜백을 이용한 메서드에서 await/async를 사용한 메서드로 변경하는 작업이었다.

completion 사용


기존에는 함수가 종료된 뒤에 네트워크 통신이 끝나는 상황(URLSession.shared.dataTask)을 고려하여 @escaping을 붙인 클로저를 돌려주는 방식을 사용했다.
이러한 방식은 여러 비동기 작업을 처리할 때 중첩이 발생할 가능성이 존재했다. 이러한 중첩은 코드의 가독성을 떨어트렸다.
또 반드시 @escaping을 붙여주어 함수 밖에서도 클로저가 실행될 수 있음을 명시해줘야 했다.

completion handler VS async/await

그러나 이번에 사용한 async/await 방식은 비동기 작업이 마치 동기적인 방식처럼 순차적으로 실행하듯 보인다. 덕분에 코드를 더 읽기 쉽고 직관적으로 보이게 한다.
비동기 작업이 많아질 때 콜백이 많아지는 콜백 지옥에서도 탈출하고 try/catch를 사용하여 비동기 함수의 에러를 동기코드처럼 처리할 수도 있다.
(클로저 콜백을 이용할 땐 try/catch를 이용한 에러처리가 제한된다. 대신 위와 같이 반환 타입에 Result 타입을 사용했다.)

https://forums.developer.apple.com/forums/thread/712303
위 개발자 포럼의 토의를 읽어보면 async/await을 사용해 얻는 이점이나 코드의 간결성 차이를 좀 더 알 수 있다.

이 외에도 애플의 공식 영상을 참고할 수 있다.
https://developer.apple.com/videos/play/wwdc2021/10132?time=110
https://developer.apple.com/videos/play/wwdc2021/10095

위 영상에서 예시로 준 코드를 한 번 살펴보자.

먼저 일반적인 completion handler를 이용한 방식이다.
여기에 Result를 추가하면 다음과 같다.

옵셔널 처리를 하고 불필요하게 nil을 넣어주던 부분이 사라졌으며 정상 동작과 에러에 대한 처리가 좀 더 명확해졌다.

위 코드를 async/awiat를 사용하여 리팩터링 하면 다음과 같아진다.

글을 보는 누구라도 차이를 느낄 수 있을 것이다.
여기서는 URLSession에 적용된 async/await까지 겹쳐져 더 극적으로 보인다고 생각한다. 기존엔 URLSession도 completion handler 기반의 메서드를 사용했다.
그러나 새로운 코드에선 data와 response의 결과를 기다리고 바로 해당 결과를 사용하여 처리하는 간결한 코드가 완성되었다.

그렇다면 대체 async/await이 뭘까?

async는 해당 함수가 비동기 작업임을 나타낸다. 해당 키워드가 붙은 함수는 호출 될 때 스레드를 차단하지 않는다.
대신 실행 중단점을 만들어 호출이 완료될 때까지 기다린다.

aync/await은 Swift Concurrency Model을 사용해 작업을 스케줄링하며 비동기 작업은 GCD(Grand Central Dispatch) 또는 Executor가 관리한다.
(GCD란 간단히 멀티스레드와 동시성 처리를 위한 라이브러리이다. 작업 단위를 큐에 추가하여 병렬 실행해주는데 우리가 자주 쓰는등DispatchQueue, main과 global 큐 등)

Swift Concurrency Model은

또한 기본적으로 메인 스레드와 다른 작업 스레드에서 실행되기에 메인 스레드에서는 안전하게 UI 업데이트를 수행할 수 있다.

await은 async 함수 내에서 다른 비동기 함수가 완료될 때까지 기다리며 비동기 함수를 호출할 때 사용된다. 비동기 함수가 완료될 때까지 실행을 일시 중단하지만 현재 스레드가 차단 되지는 않는다

다만 async를 붙인 함수라고 해서 무조건 await을 붙여 호출해야 하진 않다.
awiat을 붙인다는 건 비동기 작업의 결과를 사용할 때, 그 작업이 끝날 때 까지 기다리겠다는 의도를 나타내는 키워드이다.

Task

이제 async를 사용해 작성한 비동기 함수를 사용해보자.
Task는 앞서 말한 Swift Concurrency Model의 구성 요소 중 하나로 비동기 작업의 실행 컨텍스트를 나타낸다.
async 함수를 호출하거나 비동기 작업 실행을 위해 사용되며 내부에서 async/await을 사용할 수 있다.

추가적로 가진 Task의 특징에는 작업 취소 가능, 작업 상태 추적, 비동기적 작업 실행 등이 있다.

그러나 가장 내 눈에 띈 특징은 Task에는 약한 참조를 사용할 필요가 없단 것이다.
Task가 완료되면 Task가 가진 참조들이 모두 해제되기 때문에 Task 내의 클로저에서 약한 참조로 캡처할 필요가 없다는 것. 이는 클로저를 무한히 유지하지 않는다는 특성과 이어진다.

@frozen

잠시 딴 길로 새자면 Task의 정의에 처음 보는 어트리뷰트가 존재해서 알아봤다.
@fozen attribute는 struct나 enum에 사용되며 해당 타입이 확장 불가능하다는 것을 명시적으로 지정하는 역할을 한다.

특히 열거형에 적용할 때 해당 열거형의 크기나 메모리 구조가 고정되어서 시스템에서 성능 최적화가 가능해진다고 한다.

처음엔 그저 상속을 막는 final처럼 확장을 막는 frozen 정도로 비슷하다 생각했다. 둘 다 유연성을 포기하고 성능과 안정성을 올린다는 점이 특히 비슷했다.

final은 class, method, property, subscript 등에 사용되는데 서브클래싱이나 오버라이드를 하지 못하게 한다. 주로 객체 지향 프로그래밍에서 사용되며 다형성과 상속을 제어하는 방식으로 예측가능한 상태로 유지시킨다.

frozen은 struct나 enum에 사용하여 타입을 고정시켜 케이스나 프로퍼티를 추가할 수 없게 한다. 확장할 수 없다는 점을 통해 타입 안정상을 높이고 예기치 않은 확장을 방지할 수 있다.

정리하고 봐도 비슷해보이긴 한다.

Actor

앞서 언급한 Swift Concurrency Model에는 Task, async/await 말고도 많은 것들이 있는데 이미 너무 길어져서 Actor만 간단히 더 알아보겠다.
(위의 프로토콜이 Actor 그 자체에 대한 정의는 아니고 actor라면 준수하는 프로토콜이다.)

간단히 말하자면 Actor는 경쟁조건(race conditions)이나 데이터 손상을 방지해줄 수 있는 역할을 한다.

class처럼 정의되지만 동시성 안전을 제공하는 특수한 타입이다.
정확히 어떤 차이가 있을까?

클래스는 다중 스레드에서 공유될 수 있고 race condition이 발생할 수 있기 때문에 동기화를 직접 관리해야 한다.
구조체는 값 타입으로 복사되기에 비교적 안전하지만 그래도 여러 스레드에서 변경할 수 있는 상태를 다룬다면 여전히 문제가 발생할 수 있다.

Actor는 참조타입이지만 내부 데이터에 한 번에 하나의 스레드만 접근할 수 있도록 보장한다. 이를 통해 동시성을 안전하게 처리할 수 있게 된다.
대신 actor 내부 상태에 접근하기 위해선 비동기적으로 접근해야 하고 내부 데이터나 메서드는 기본적으로 비동기로 호출해야 하며 awiat 키워드를 사용해야 한다.

예를 들어 다음과 같은 코드에서 2가 나오는 것을 보장해줄 수 있다.

actor Foo {
	private var value = 0
    
    func increment() {
		value += 1
	}
    
    func getValue() -> Int {
		return value
	}

let foo = Foo()

Task {
	await foo.increment()
}

Taks {
	awiat foo.increment()
}

foo.getValue() // 2가 보장됨 

만약 비동기 작업으로 수행했다면 동시에 value에 접근하면서 2가 아닌 1이 반환될 가능성이 존재했다.
그러나 actor에는 한 번에 한 스레드만 접근가능하므로 첫 번째 Task가 접근하는 동안 비동기적으로 두 번째 Task가 접근하려 해도 기다려야 한다.

이러한 안정성을 얻는 대신 값 복사가 불가능하고 다른 곳으로 actor를 전달하려면 비동기 접근이 필수, 상태를 변경할 땐 await이 사용되어야 한다는 제약이 있다.
추가로 직렬화 된 접근 방식이 가져오는 성능 저하 가능성이 존재한다.

actor와 비슷한 키워드를 본 적이 있는데 바로 @MainActor이다.
@MainActor는 UI 작업을 메인 스레드에서 실행하도록 보장하기 위해 있는 어트리뷰트(attribute)(annotation)으로 주로 UI업데이트와 다른 스레드와의 경쟁을 피하기 위해 사용된다.

await URLSession

돌고 돌아 다시 URLSession으로 돌아왔다.
URLSession에는 async/awiat 지원을 위해 다음과 같은 메서드가 추가되었다.

  • data(from:): 데이터를 다운로드하는 비동기 메서드
  • download(from:): 파일을 다운로드하는 비동기 메서드
  • upload(from:): 파일을 업로드하는 비동기 메서드

내가 사용한 건 data로 (data, response)라는 튜플로 받아 사용했다.
비동기 작업이지만 동기적으로 보여서 코드가 읽기 쉽고 직관적이다.
앞서 언급햇 듯 try를 통해 에러처리도 할 수 있고 중첩된 클로저 사용 없이 await을 이용해 순차적인 실행이 가능하다.
또한 실패했을 때 에러를 반환받을 수 있게 try를 사용하는데 이때 반환되는 error는 기존의 completion handler를 사용하며 받던 에러와 동일하다.

이제 완성된 메서드를 사용할 때는?
앞서 말했듯 Task 컨텍스트 내부에서 사용해주면 된다.

위 코드는 팀원이 작성한 코드의 일부로 코드가 Task로 묶인 것을 볼 수있다.
또한 내가 async throw로 반환했기에 try/catch도 사용하고 있다.
그리고 내부에서 fetchData를 사용할 때 await을 사용하여 비동기로 처리할 것을 명시해주는 중이다.

추가적인 async 활용_CheckedContinuation

async/await을 알게된 뒤 추후에 겪은 트러블에서 이를 활용해 해결했다.

겪었던 문제는 결제 완료 페이지에서 사용자에게 알럿을 띄울 때 알럿은 비동기적으로 실행되어 확인 버튼이 눌러지길 대기 중이었지만 알럿창 뒤로 뷰가 변경되는 동작이 실행되었다.
즉 알럿의 확인 버튼을 누르지 않더라도 뒤에 있던 rootViewController로 이동하는 코드가 실행되어버렸다.
이는 알럿에 관한 코드가 기본적으로 비동기적인 처리가 되기 때문이었다.

문제를 해결하기 위해선 알럿창의 확인이 실행되기 전까지 뒤쪽 코드의 지연이 필요했다.
기존엔 일반적인 알럿 present코드였으나 async와 await 그리고 witCheckedContinuation을 활용하여 해결하였다.

withCheckedContinuatoin은 또 무엇인가 하면..

Continuation은 opaque representation of program state라고 한다.

Opaque는 강의 시간 때 Boxed Protocol과 함께 배우면서 본 단어인데 대략적으로 불투명한, 보이지 않는 정도의 뜻이다.
스위프트 문서에서는 내부 구현 세부사항을 감춘다는 의미로 사용되는 듯.
외부에서 봤을 때 불투명해서 내부가 안 보인다 뭐 그런 의미..?

그리고 representation of program state를 이어서 번역하면 불투명하게 프로그램 상태를 표현한다는 말이 된다.

여러 해석을 살펴보았을 때 프로그램이 중단된 시점의 프로그램 상태를 캡쳐하고 표현하지만 내부 상태를 외부에서 알 수는 없다는 말인 거 같다.(캡슐화 한다는 말같음)

다시 공식문서의 개요를 이어서 전달하자면 다음과 같은 코드로 continuation을 생성할 수 있다고 한다.

  • withUnsafeContinuation(function:_:)
  • withUnsafeThrowingContinuation(function:_:)

그리고 비동기 작업을 다시 시작할 때는

  • resume(returning:)
  • resume(throwing:)
  • resume(with:)
  • resumt()

등을 호출하면 된다.

반드시 주의할 점은 프로그램의 실행 경로에서 반드시 한 번만 resume메서드를 호출해야 한다는 것이다.

Continuation이 여러번 resume 메서드를 호출하는 것은 정의되지 않은 동작(Undefined Behavior)을 초래한다고 한다.
그렇다고 resume을 한 번도 호출하지 않는다면 continuation은 무한 대기 상태에 머물고 이와 관련된 resource에 leak이 발생할 것이다.

이와 관련하여 앞서 나온 Continuation에서 차이가 발생하는데
CheckedContinuation은 누락되거나 여러번 호출 된 resume을 런타임에 검사한다.

그러나 UnsafeContinuation은 런타임에서 검사를 수행하지 않는다.
대신 낮은 오버헤드로 작업을 이벤트루프, 델리게이트, 콜백 또는 다른 비동기 예약 매커니즘과 연결하기 위한 매커니즘에 활용된다.
안전하지 않은만큼 테스트가 중요해질 것이다.

그럼 당연히 나는 withCheckedContinuation 메서드를 사용해야겠지.

withCheckedContinuation은 비동기 코드를 정의하면서 직접 제어할 수 있는 Continuation객체를 클로저를 통해 제공해준다.

알럿처리를 위해 사용한 이유는 Continuation이 주로 콜백 기반의 API를 async/await으로 변환할 때 유용하기 때문이다.
또한 콜백을 사용하여 코드의 흐름이 분리되는 것보다 훨씬 가독성이 좋기 때문에 적극적으로 async/await을 활용하려 했다.

근데 진짜 갈수록 쉽지 않다.
쓸 때야 시간이 없으니 사용방법만 살펴보고 적용하는데
이후 다시 공부하면서 살펴볼 때면
끝없이 나오는 새로운 키워드들이 자꾸 밤을 새게 만든다...
지금도 새벽 4시인데,,

일단 내가 처음보고 이해안되는 부분만 간략히 정리하겠다.
(물론 간략히 정리한다고 이해도 간단히 되진 않는다.)

isolated (any Actor)? = #isolation

먼저 isolated라는 키워드는 특정 Actor의 격리상태를 나타낸다.
여기서 격리 상태란 앞서 말한 Actor의 특징인 단일 실행 컨텍스트에서만 접근 가능하다는 점을 보장해준다는 의미이다.

아니 앞에서 애초에 actor는 한 번에 한 스레드에서만 접근가능하게 설계되었다고 했는데
굳이굳이 다시 저렇게 보증해주는 이유가 뭘까?

그건 매개변수로 전달 된 Actor의 격리를 명확히 나타내기 위함과
이미 안전한 격리 컨텍스트에서 실행되고 있음을 보장함으로써 추가적인 await 키워드 없이 사용할 수 있게 해주기 위함이다.

그렇다면 이미 격리된 상태에서 actor가 실행된다는 것은 뭘까?
격리된 actor는 actor 내부의 상태에 접근하거나 수정하려면 반드시 해당 actor의 실행 컨텍스트 내에서 이루어져야 한다는 뜻이다.
즉 다른 컨텍스트(스레드나 actor 등)에서는 격리된 actore의 상태에 직접적으로 접근할 수 없다.

코드로 살펴보는 게 제일 와닿으니 코드를 보자

actor Foo {
	var value: Int = 0
    
    func increment() {
		value += 1
	}
}

func bar(foo: Foo) {
	foo.increment // 불가능. awiat 필수
}

func baz(foo: isolated Foo) {
	foo.increment // 격리된 actor임이 보장되어 가능함
    // await 없이 동기적으로 접근
}

//그러나 외부에서 호출 시에는 반드시 await 필요함

어느정도 이해가 됐다면 이제 #isolation은 뭔지 살펴보자.
이후의 #function과 함께 컴파일 타임 메타데이터를 의미하는 키워드로
컴파일러가 실행 시점의 특정 정보를 자동으로 제공해준다.

#이 붙으면 컴파일 타임 키워드라는 것으로 #isolation, #function외에도 #file, #line, #column 등이 있다.

#isolation은 Swift Concurrency Model에서 현재 실행 중인 Actor의 격리 상태를 캡처하는 키워드이다.
즉 실행 중인 actor의 격리 컨텍스트를 자동으로 가져와준다는 것.

String = #function

#function은 디버깅용 컴파일타임 메타 데이터 키워드로 함수 이름을 문자열로 반환해준다.(그래서 타입이 String)
함수가 호출될 때 해당 함수 이름을 자동으로 문자열로 제공해줘서 디버깅이나 로깅 때 사용된다고 한다.

솔직히 앞의 #isolated는 잘 쓸지 모르겠지만 #function은 종종 쓸 거 같다.
매개변수가 아니더라도 다음처럼 쓸 수 있다.

func printFunctionName() {
	print("name: \(#function)")
}

printFunctionName() // name: printFunctionName

sending T

sending이란 키워드는 Concurrency Model에서 타입 T가 안전하게 호출자에게 전달(sending)됨을 명시해준다.

다시말해 동시성 컨텍스트(Task, Actor 등) 간에 데이터가 안전하게 전달됨을 말한다.
컴파일러가 타입의 동시성 안정성 확인하고 필요시 복사하는데
만약 값이 전달될 대 값타입(복사 가능)이거나 동시성 안전한 참조타입인지 검증하고 안전성이 보장되지 않을 때 컴파일 에러를 발생시킨다.

이번에도 예시코드로 살펴보자

actor Foo {
	func getString() async -> sending String {
    	return "String" // 값 타입으로 동시성 안전함.
	}
    
class Bar {
	var value: Int = 0
}

actor Baz {
	func getValue() async -> sending Bar {// error 발생
		reuturn Bar()// 일반 참조타입은 안정성 보장이 안 됨
	}

만일 참조타입이 안전성을 보장받고 싶다면 Sendable 프로토콜을 준수하면 된다.
이외에는 모든 상태가 불변(Immutability)이거나 명시적으로 동기화 처리를 해주어도 된다.
참고로 @Sendable로 클로저도 동시성 안전성 보장 가능

이제 다시 본래의 알럿 처리로 돌아가자..
withCheckedContinuation을 쓸 때 기본값은 전부 지정되어 있으므로 바로 클로저를 열어 사용했다.

continuation은 앞서 얘기한 resume을 통해 작업을 완료하고 값이나 에러를 반환하고 중단된 작업을 재개하여준다.


알럿 액션에 conitnuation.resume()을 넣어
중단된 작업을 확인버튼을 눌렀을 때 다시 재개할 수 있게 하였다.


이후 알럿에 대한 처리는 맨 처음에 본 코드처럼 간결하고 흐름을 잃지 않으면서도 알럿 처리가 된 후에 이후 동작이 실행될 것을 확인할 수 있다.

Generic

이번 fetchData는 처음엔 nowPlaying이나 upcoming 등 엔드포인트만 다를 뿐 동일한 반환 형식을 가진 데이터를 받아왔다.
그러나 이후 조금 더 상세한 정보가 필요하게 되었고 기존의 MovieData 뿐만 아니라 DetailData, VideoData 등도 가져와야 하는 상황이 생겼다.

처음엔 ViedoData의 경우 URL을 반환받는 것이 목적이어서 거의 동일한 코드에 URL을 만드는 코드, 영상 URL을 만들어 반환하는 코드를 가진 fetchViedo()가 생겼다.

그러나 곧바로 중복되는 코드가 너무 많고 URL로 변환하여 반환하는 것만 뺀다면 기존의 fetchMovie()를 재활용할 수 있음을 깨달았다.

그래서 반환 타입은 제너릭으로 어떤 타입이든 Decodable만 준수하면 되도록 하였고 ViedoData를 가지고 URL을 만들어낼 수 있는 코드는 ViedoData 구조체 내부로 분리했다.

Generic 덕분에 중복되는 코드 없이 이후에 필요해진 DetailData 등도 간단하게 fetchData()를 사용할 수 있게 되었다.

URLComponents, URLQueryItem

fetchData()를 여러 URL에 맞게 사용하기 위해 URL의 구성 요소 중 엔드포인트와 파라미터를 다양하게 받고 조합해서 URL을 만들어낼 수 있어야 했다.
단순히 String으로 이어붙일 수도 있지만 이번엔 buildURL이라는 헬퍼메서드를 만들어 활용했다.(URLParameters는 typealias)

URLComponents는 초기화가 실패할 경우를 대비하여 옵셔널로 선언한다.
URL을 구성하는 스킴, 호스트, 경로, 쿼리 등을 관리하며 URL을 반환받을 수 있다.

URLQueryItem은 파라미터를 추가하기 위한 것들로 키와 밸류를 넣어주어 파라미터를 만들어준다.

필요한 데이터를 주는 엔드포인트와 파라미터를 전달받아
URLComponents에 넣어주는 직관적인 방식으로 URL을 만들 수 있어 편리했다.


여담이지만
https://developer.apple.com/documentation/swift/updating_an_app_to_use_swift_concurrency
위 링크를 보면 코드 리팩터링에 대한 가이드도 제공해주는 걸로 보아 애플에서도 async/awiat을 좀 더 선호하는 듯.

이 외에도 Swiftlint와 코어데이터의 Relationship, UserDefaults 등에 대해서도 정리하고 싶은데 시간이 따라줄지 모르겠다..

2개의 댓글

comment-user-thumbnail
3일 전

어떻게 딱 공부한게 챌린지 수업에 나오지 신기방기

1개의 답글