맛동산이

Swift) Delegate, Completionhandler 를 async await을 통해서 리팩토링하는법(by. continuation) 본문

앱/Swift

Swift) Delegate, Completionhandler 를 async await을 통해서 리팩토링하는법(by. continuation)

진ddang 2024. 6. 6. 15:00

[Swift] async / await & concurrency

기본적으로 이전에는 async await을 api 통신에서만 사용한다고 생각을 했지만, 최근에 asnyc await을 알게되면서, 해당 코드를 통해서 delegate나, completionHandler방식으로 처리되고 있던것 또한 i/o 입력에 의한 비동기 처리기 때문에 이를 async 스타일로 리팩토링이 가능하다는것을 알게되어 정리해보려고 한다.

먼저 async awai는 예전에 정리한적이 있기 때문에 넘어가도록 하고

async 와 await을 연결해주는 continuation 이라는 개념을 최근에 알게되었기 때문에 정리해보자!


What’s continuation?

동기 코드와 비동기 코드를 이어주는 인터페이스이다.

기존에는 delegate와 completionHandler를 통해서 비동기 코드가 실행된 후 동기적으로 이를 받아서 처리하도록 하였지만 이는 흐름을 파악하기에 쉽지 않았다.

이를 해결하기 위해서 나온것이 바로 continuation이다!

async await, swift concurrency

private func fetchData(url: URL) async throws -> Data {
  let (data, response) = try await URLSession.shared.data(from: url)
  guard let response = response as? HTTPURLResponse, response.isOk else {
    throw URLError(.badServerResponse)
  }
  return data
}

먼저 스위프트 concurrency를 이해해야 continuation에 대해서 알수 있기 때문에 간단하게 정리하고 넘어가자면,

  1. 메소드 뒤에 async 를 붙였기 때문에 비동기라는걸 알수 있음
  2. await은 ‘suspending point’기 때문에 해당 데이터가 들어올때까지 해당 스레드는 suspending상태로 두고 다른 스레드로 가서 비동기 작업을 실행함
  3. Swift는 이러한 await가 된 메소드를 heap영역에 저장하고 “continuation”을 생성한다. 이를 통해서 비동기 작업이 끝나고, 해당 continuation을 resume 해주도록 설계했음
  4. 당연하게도 heap에 저장될때는 해당 상태를 캡쳐링 하기 때문에 신경써야함

checked, unchecked Continuation

  • withCheckedContinuation(_:)
  • withCheckedThrowingContinuation(_:)
  • withUnsafeContinuation(_:)
  • withUnsafeThrowingContinuation(_:)

Continuation에는 이렇게 4개의 메소드가 존재하고 다시 분류해보자면 checked와 unchecked, throwing과 noThrowing으로 나뉜다.

checked와 uncheck는 좀있다 확인해보자~

Resume()

이렇게 continuation이 한번 생성되고 나서는 이제 델리게이트나 completionHandler의 해당 메소드를 콜 해주는 부분이 바로 resume이다.

  • resume() : result를 전달하지 않고 suspend된 task만 resume
  • resume(returning: T) : 제공된 타입의 value 전달 및 resume
  • resume(throwing: E) : 제공된 Error 전달 및 resume
  • resume(with: Result<T, E>) : 제공된 Result<T, E> 전달 및 resume

Delegate와 비교

기존의 delegate는 다음과 같은 응답으로 나온다.

한개의 화면에서 delegate를 주입하고, 해당 delegate를 전달해주면 위임받은 화면에서 해당 데이터를 이용해서 처리해주는 일련의 과정을 가진다.

하지만 continuation은 이처럼 async한 메소드를 콜하는 화면과 이를 await하는 화면으로 나누어서, 위의 그림에서는 ContentViewModel이 await을 하고 있고, UIImagePicker가 async한 메소드를 콜 하여서 해당 데이터를 resume으로 전달해주는 일련의 과정이다.

이를 코드로 보면 다음과 같다.

예시

먼저 비동기 동작을할 클래스를 하나 만들어준다.

final class service {
	static func fetchImage(with urlString: String) async throws -> UIImage? {
		return try await withCheckedContinuation { continuation in 
			guard let url = URL(string: urlString) else {}
			...
			비동기작업
			switch result {
				case .success(let imageResult):
					image = imageResult.image
					return continuation.resume(returning: image)
				case .failure(let error):
					return continuation.resume(throwing: error)
		 }
	}
}

이제 이거를 사용하는 부분에서의 코드를 보면

func configureImage(___) {
	Task { 
		let image = try await service.fetchImage(with: imageURL)
		imageView.image = image
	}
}

이렇게 사용할수 있게 된다.


그렇다면 이게 기존의 Swift concurrency가 있는데 왜쓰지???????싶은데

이는 delegate와 completionHandler를 대체하기 위해서 사용하는 방법인것이다.

기존의 swift concurrency로 바꾸기 위해서는 모든 비동기를 바꿔줘야 했는데 이는 부분적으로 swiftConcurrency처럼 사용할수 있기 때문에 편리해 보인다.

번외로

checked uncheck ??

가장 중요한 내용중 한개지만

  • Continuation안에서는 resume을 단 한번만 호출해야한다.
  • 그렇지 않으면 계속해서 suspending상태에 빠진다.

여기에서 checkedContinuation이다.

checkedContinuation은 정확성 체크를 통해 누락되거나 두번 resume된 코드가 없는지, 런타임에 체크를 해주는 방법을 의미한다.

반면 uncheckedContinuation은 오버헤드가 적은것이 목표기 때문에 불변성을 체크하지 않는다.

기본적으로 이전에는 async await을 api 통신에서만 사용한다고 생각을 했지만, 최근에 asnyc await을 알게되면서, 해당 코드를 통해서 delegate나, completionHandler방식으로 처리되고 있던것 또한 i/o 입력에 의한 비동기 처리기 때문에 이를 async 스타일로 리팩토링이 가능하다는것을 알게되어 정리해보려고 한다.

먼저 async awai는 예전에 정리한적이 있기 때문에 넘어가도록 하고

async 와 await을 연결해주는 continuation 이라는 개념을 최근에 알게되었기 때문에 정리해보자!


What’s continuation?

동기 코드와 비동기 코드를 이어주는 인터페이스이다.

기존에는 delegate와 completionHandler를 통해서 비동기 코드가 실행된 후 동기적으로 이를 받아서 처리하도록 하였지만 이는 흐름을 파악하기에 쉽지 않았다.

이를 해결하기 위해서 나온것이 바로 continuation이다!

async await, swift concurrency

private func fetchData(url: URL) async throws -> Data {
  let (data, response) = try await URLSession.shared.data(from: url)
  guard let response = response as? HTTPURLResponse, response.isOk else {
    throw URLError(.badServerResponse)
  }
  return data
}

먼저 스위프트 concurrency를 이해해야 continuation에 대해서 알수 있기 때문에 간단하게 정리하고 넘어가자면,

  1. 메소드 뒤에 async 를 붙였기 때문에 비동기라는걸 알수 있음
  2. await은 ‘suspending point’기 때문에 해당 데이터가 들어올때까지 해당 스레드는 suspending상태로 두고 다른 스레드로 가서 비동기 작업을 실행함
  3. Swift는 이러한 await가 된 메소드를 heap영역에 저장하고 “continuation”을 생성한다. 이를 통해서 비동기 작업이 끝나고, 해당 continuation을 resume 해주도록 설계했음
  4. 당연하게도 heap에 저장될때는 해당 상태를 캡쳐링 하기 때문에 신경써야함

checked, unchecked Continuation

  • withCheckedContinuation(_:)
  • withCheckedThrowingContinuation(_:)
  • withUnsafeContinuation(_:)
  • withUnsafeThrowingContinuation(_:)

Continuation에는 이렇게 4개의 메소드가 존재하고 다시 분류해보자면 checked와 unchecked, throwing과 noThrowing으로 나뉜다.

checked와 uncheck는 좀있다 확인해보자~

Resume()

이렇게 continuation이 한번 생성되고 나서는 이제 델리게이트나 completionHandler의 해당 메소드를 콜 해주는 부분이 바로 resume이다.

  • resume() : result를 전달하지 않고 suspend된 task만 resume
  • resume(returning: T) : 제공된 타입의 value 전달 및 resume
  • resume(throwing: E) : 제공된 Error 전달 및 resume
  • resume(with: Result<T, E>) : 제공된 Result<T, E> 전달 및 resume

Delegate와 비교

기존의 delegate는 다음과 같은 응답으로 나온다.

한개의 화면에서 delegate를 주입하고, 해당 delegate를 전달해주면 위임받은 화면에서 해당 데이터를 이용해서 처리해주는 일련의 과정을 가진다.

하지만 continuation은 이처럼 async한 메소드를 콜하는 화면과 이를 await하는 화면으로 나누어서, 위의 그림에서는 ContentViewModel이 await을 하고 있고, UIImagePicker가 async한 메소드를 콜 하여서 해당 데이터를 resume으로 전달해주는 일련의 과정이다.

이를 코드로 보면 다음과 같다.

예시

먼저 비동기 동작을할 클래스를 하나 만들어준다.

final class service {
	static func fetchImage(with urlString: String) async throws -> UIImage? {
		return try await withCheckedContinuation { continuation in 
			guard let url = URL(string: urlString) else {}
			...
			비동기작업
			switch result {
				case .success(let imageResult):
					image = imageResult.image
					return continuation.resume(returning: image)
				case .failure(let error):
					return continuation.resume(throwing: error)
		 }
	}
}

이제 이거를 사용하는 부분에서의 코드를 보면

func configureImage(___) {
	Task { 
		let image = try await service.fetchImage(with: imageURL)
		imageView.image = image
	}
}

이렇게 사용할수 있게 된다.


그렇다면 이게 기존의 Swift concurrency가 있는데 왜쓰지???????싶은데

이는 delegate와 completionHandler를 대체하기 위해서 사용하는 방법인것이다.

기존의 swift concurrency로 바꾸기 위해서는 모든 비동기를 바꿔줘야 했는데 이는 부분적으로 swiftConcurrency처럼 사용할수 있기 때문에 편리해 보인다.

번외로

checked uncheck ??

가장 중요한 내용중 한개지만

  • Continuation안에서는 resume을 단 한번만 호출해야한다.
  • 그렇지 않으면 계속해서 suspending상태에 빠진다.

여기에서 checkedContinuation이다.

checkedContinuation은 정확성 체크를 통해 누락되거나 두번 resume된 코드가 없는지, 런타임에 체크를 해주는 방법을 의미한다.

반면 uncheckedContinuation은 오버헤드가 적은것이 목표기 때문에 불변성을 체크하지 않는다.

반응형