작성
·
87
·
수정됨
0
📌 자주하는 질문 모음
자주하는 질문을 먼저 확인 부탁드리며, 질문은 최대한 구체적으로하셔야 빠르게 답변드릴 수 있습니다.
[질문 예시]
(1) 몇강, 몇초의 내용이 잘 이해가 안갑니다. (제가 적어놓은 강의 번호 "10강, 7분 강의 내용 중에... " )
(2) 강의자료 몇페이지의 내용이 잘 이해가 안갑니다.
(3) 정확하게 어떤 포인트에 대한 내용이 이해가 안갑니다. 다시 설명해주실 수 있나요?
안녕하세요 엘런님, 강의를 들으면서 swift concurrency를 적극 활용해보고 있는 중입니다.
코드를 작성하고 나서 테스트 코드를 작성하면서 의문이 생기게 되었는데요,
class viewmodel {
@Published private(set) var count = 0
enum Event {
case buttonTapped
}
// viewcontroller에서 호출하는 함수
func dispatch(_ event: Event) {
Task {
switch event {
case .buttonTapped:
count += await fetch()
}
}
}
// 네트워크를 통해 받아오는 값이라고 가정.
private func fetch() async -> Int {
return 100
}
}
struct viewmodelTests {
var sut: viewmodel!
init() {
sut = viewmodel()
}
@Test
func testButtonTapped0() async throws {
sut.dispatch(.buttonTapped)
try await Task.sleep(for: .seconds(1))
#expect(sut.count == 100)
}
@Test
func testButtonTapped1() async throws {
sut.dispatch(.buttonTapped)
await Task.megaYield()
#expect(sut.count == 100)
}
@Test
func testButtonTapped2() async throws {
sut.dispatch(.buttonTapped)
try await Task.yield()
#expect(sut.count == 100)
}
@Test
func testButtonTapped3() async throws {
try await withMainSerialExecutor {
sut.dispatch(.buttonTapped)
try await Task.yield()
#expect(sut.count == 100)
}
}
@Test
func testButtonTapped4() async throws {
try await withMainSerialExecutor {
sut.dispatch(.buttonTapped)
try await Task.megaYield()
#expect(sut.count == 100)
}
}
}
다음과 같은 viewmodel을 테스트하는 코드를 작성했을 때,
0번째 테스트의 경우는 현재 testButtonTapped0가 실행되는 Task를 suspend했지만, dispatch 함수 내부의 Task는 suspend되지 않고 그대로 진행되기 때문에 정상적으로 작동을 할 것으로 예상을 했습니다.
https://github.com/pointfreeco/swift-concurrency-extras
1번째의 경우는 위 라이브러리를 사용해서 하나의 Task를 여러번 yield시켜주도록 해서 동작하도록 해서 거의 높은 확률로 작동을 했습니다.
그런데 testButtonTapped2의 경우 yield를 해주더라도 남아있는 스레드가 존재해서 바로 다시 일을 진행을 해서 틀릴 때가 있는 걸까요???
또한 5번째는 거의 항상 맞는 것이 보장되는데 3번째는 보장이 되지 않는 이유도 궁금합니다.
궁극적으로 궁금한 점은 현재 위 코드의 viewmodel의 dispatch함수를 테스트하기 위해서 어떠한 방법을 사용해야 항상 성공을 보장할 수 있을 지 궁금합니다.
또한 Task.yield()를 했을 때 어떤 Task를 yield하는지 정확하게 이해가 가지 않습니다..
긴 글 읽어주셔서 감사합니다!
답변 2
0
네 안녕하세요 영균 님!
일단 제가 보기엔 지금 2가지 포인트를 잘 못 짚고 계시는 것 같은데요,
(1) Task.yield()의 의미를 정확하게 이해하기 위해 다시 생각해 보시면 좋을 것 같습니다. (8강 22분 전후의 내용 다시 보시는 것도 추천을 드리고요.)
원칙적으로 async/await 의 비동기(async) 함수는.. 함수 자체가 운영체제에 오래 걸리는 일처리 동안.. 자기가 사용하는 쓰레드를 양보하게 됩니다. 따라서, 기본적으로 yield( )를 사용하지 않아도 비동기 함수는 양보하게 되어 있습니다.
(따라서 보통은 yield( ) 메서드를 사용 하실 필요가 없지만) 어떤 오래 걸리는 일처리 전후로 쓰레드를 양보하고 싶다면.. 선택적으로 사용할 수도 있는 것입니다.
예를 들어..
func 비동기로오래걸리는함수() async -> Data {
await 비동기함수실행() // 비동기적으로 1초 걸린다고 가정
실제오래걸리는함수1실행() // 동기적으로 10초 걸린다고 가정
실제오래걸리는함수2실행() // 동기적으로 10초 걸린다고 가정
실제오래걸리는함수3실행() // 동기적으로 10초 걸린다고 가정
await 비동기함수실행() // 비동기적으로 1초 걸린다고 가정
}
위의 비동기 함수는 대략 32초걸리는 비동기작업이라고 가정해보겠습니다. (설명을 드리기 위해 일부러 함수1,2,3은 동기작업으로 가정한거예요.) 위의 작업은 최소한 32초 이상이 걸릴 수 밖에 없습니다. 동기작업은 30초가 무조건 걸리고.. 비동기는 최소 2초가 걸릴텐데.. 비동기작업에서는 쓰레드를 양보했다가.. 나중에 재개가 될 수 있으니 (쓰레드 사용에 의해서 내부적으로 멈췄다가.. 재개될 수 있으니) 2초보다 더 걸릴 수 있겠죠.
(그래도 중간의 동기작업은 한번 일을 시작하면 쓰레드를 양보하는 일이 없을테니, 무조건 30초가.. 스트레이트로 일어날 수 밖에 없습니다. 중간에 양보 시점이 아예 없습니다.)
그러면... 위와 같은 상황에서 30초가 스트레이트로 걸리는 일은.. 너무 오래 걸리는 일이니까..
func 비동기로오래걸리는함수() async -> Data {
await 비동기함수실행() // 비동기적으로 1초 걸린다고 가정
실제오래걸리는함수1실행() // 동기적으로 10초 걸린다고 가정
try await Task.yield()
실제오래걸리는함수2실행() // 동기적으로 10초 걸린다고 가정
try await Task.yield()
실제오래걸리는함수3실행() // 동기적으로 10초 걸린다고 가정
await 비동기함수실행() // 비동기적으로 1초 걸린다고 가정
}
중간에 쓰레드를 양보하면.. 10초씩마다.. 혹시나 다른 일처리가 있다면.. 쓰레드를 양보하는 코드를 넣어주면.. 스트레이트로 30초가 일어나는 일처리를 막을 수 있다는 뜻입니다.
(위의 예제를 잘 생각해보시고 다시 질문주세요. 지금 yield메서드의 사용 예시가 잘못 되었습니다.)
(2) 아래 Task 함수를 잘 못 설계하셨어요.
func dispatch(_ event: Event) { // 2번쓰레드에서 시작
Task { // 3번 쓰레드에서 작업 생성
switch event {
case .buttonTapped:
count += await fetch()
}
}
}
위와 같이 하시면, 예를 들어 dispatch 동기함수가 2번쓰레드에서 작업을 시작해도, 작업을 생성하면서 3번 쓰레드에서 일을 시키고 기다리지 않습니다. Task는 작업을 생성하는 것이기도 하지만.. DispatchQueue.global().async { } 와 비슷하게 일을 시작 시키고 (작업을 생성하고) Task { } 내부의 일이 끝날때까지 기다리지 않는 코드 입니다.
따라서 제가 봤을때.. 원하시는 코드가.. dispatch함수가.. 함수 내부의 작업(Task)이 끝날때까지 기다리길 원하시는 것 같은데.. 예를 들어 아래 처럼 작성 하셔야 합니다.
func dispatch(_ event: Event) async { // 2번쓰레드에서 시작
let task = Task {
switch event {
case .buttonTapped:
count += await fetch()
}
}
await task.value // 일이 끝날때까지 기다렸다가.. dispatch함수 종료 가능
}
위의 코드 내용이 이해가 안가시면, 작업(Task)의 종료 부분의 내용을 강의에서 천천히 코드를 다시 보셨으면 좋겠습니다. (이런 실수들을 하실까지봐.. 제가 처음부터 작업(Task)에 대해서 설명드릴 때.. task.value에서 작업을 기다리는 방법들도 다 설명드린 것입니다.)
위의 내용들을 천천히 다시 생각 해보시고.. 이해가 안되시는 부분이 있으시면 다시 질문 주세요 !
고맙습니다. :)
0
안녕하세요, 인프런 AI 인턴입니다. 김영균님께서 제기하신 Task 관련 Swift concurrency에 대한 질문을 확인했습니다.
보내주신 코드와 테스트 시나리오를 통해 이해한 바를 요약해드리자면, 현재 viewmodel
의 dispatch
함수가 비동기 환경에서의 동작을 이해하고, 테스트에서 안정적으로 검증하기 위한 방법을 찾고 계신 것 같습니다.
dispatch
함수는 Swift의 Task
를 활용하여 비동기 작업을 수행하고 있습니다. Task
내부에서 fetch()
함수를 호출하며, 이는 비동기 함수로 await
을 통해 비동기 작업이 완료될 때까지 기다립니다.
Task Yielding: Task.yield()
나 Task.megaYield()
는 스레드를 다른 작업에 넘겨줄 수 있도록 하는 역할을 합니다. 그러나 yield()
는 단순히 현재 작업의 기회를 스케줄러에 양보할 뿐, 현재 작업이 재개되는 시점은 스케줄러에 의해 결정됩니다. 따라서 특정 테스트들이 작은 변동에 의해 성공/실패할 수 있습니다.
완벽한 성공 보장: 다양한 테스트 환경에서 항상 성공을 보장하기 위해서는, 비동기 작업이 완료된 후 검증을 진행할 수 있어야 합니다. dispatch
함수 내의 비동기 작업이 확실히 완료되는 시점을 기다린 후 검증하면, 성공률을 높일 수 있습니다.
동기화 포인트 설정: fetch
의 결과 반영을 확실히 기다리기 위해, Task.wait()
등의 방법을 사용하여 해당 비동기 작업이 완료된 후 테스트를 진행하는 것이 중요합니다. 필요 시 추가적인 DispatchSemaphore 또는 Combine의 도구를 활용하여 비동기 작업이 완료되었음을 확인할 수 있습니다.
Custom Async Methods: 기존의 라이브러리 활용뿐만 아니라, Swift 6 이후 제공되는 다양한 async/await 패턴을 잘 활용하시기 바랍니다.
아래 리소스에서 비동기 작업 테스트에 대해 더 많은 정보를 찾을 수 있습니다:
- Swift.org: Swift Concurrency
- Apple Developer Documentation: Testing with XCTest
저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.