← Study Hub

08 · AsyncStream

취소 시 delegate 정리, 그리고 단일 소비자라는 제약
진행률
0 / 0 완료

← 문항 인덱스

면접 질문
  1. 소비자가 for await 루프를 break 하거나 Task가 취소되면 delegate 리스닝은 어떻게 정리하나요?
  2. 하나의 AsyncStream을 여러 곳에서 동시에 구독하면 어떤 문제가 있나요?

답변 ① — onTermination에서 정리한다 🔥 핵심

소비자가 break하거나 감싼 Task가 취소되면 스트림이 종료되며 continuation.onTermination 클로저가 호출됩니다. 여기서 delegate 해제·observer 제거·타이머나 DispatchSource 취소 같은 정리를 합니다.

중요한 특징이 둘 있습니다. 첫째, AsyncStream에는 명시적 cancel 메서드가 없습니다 — 정리는 전적으로 task 취소와 스트림 종료에 의존합니다. 둘째, 종료 사유(.finished / .cancelled)가 함께 전달되므로, 정상 종료와 취소를 구분해 다르게 처리할 수 있습니다.

이 정리를 빠뜨리면 화면을 벗어났는데도 GPS·타이머·observer가 계속 살아 배터리와 메모리를 먹습니다. 즉 onTermination은 선택이 아니라 리소스 수명 관리의 필수 지점입니다.

onTermination — break·취소 시 자동으로 뒷정리한다 (Swift)
func locations() -> AsyncStream<CLLocation> {
    AsyncStream { continuation in
        let manager  = CLLocationManager()
        let delegate = LocationDelegate { continuation.yield($0) }
        manager.delegate = delegate
        manager.startUpdatingLocation()

        continuation.onTermination = { reason in    // .finished 또는 .cancelled
            manager.stopUpdatingLocation()          // 손님이 나가면 불을 끈다
            manager.delegate = nil
        }
    }
}

// 소비 측: break 하거나 Task가 취소되면 위 onTermination이 호출된다
let task = Task {
    for await loc in locations() {
        if enough(loc) { break }      // ← 여기서 스트림 종료 → 정리 실행
    }
}
task.cancel()                          // ← 이 경우도 정리 실행(.cancelled)

답변 ② — 단일 소비자라서 값이 쪼개진다

AsyncStream은 단일 소비자 전용입니다. 여러 for await가 같은 스트림을 돌면 값이 소비자끼리 쪼개져 배분됩니다. 각 값은 소비자 한 명에게만 가고, 누구에게 갈지는 예측할 수 없습니다. 결과적으로 아무도 전체 시퀀스를 받지 못하는, 조용하고 재현이 어려운 버그가 됩니다.

해결책은 목적에 따라 갈립니다.

⚠️ 주의

AsyncChannel(Async Algorithms)을 "여러 명이 구독하는 해결책"으로 답하면 틀립니다. AsyncChannel은 다중 소비자와 백프레셔를 지원하지만, 각 값을 소비자 하나에게만 전달하므로 멀티캐스트(방송)가 아닙니다. 모두에게 같은 값을 주려면 share/broadcast 연산자가 필요합니다.

📝 노트

기본 버퍼 정책은 .unbounded입니다. 소비자가 생산 속도를 못 따라가면 값이 계속 쌓여 메모리가 무한히 늘 수 있습니다. 위치 업데이트처럼 최신값만 중요하면 .bufferingNewest(1)을 쓰세요.

❌ 하나를 여럿이 구독 vs ✅ 각자 스트림 (Swift)
// ❌ 값이 소비자끼리 쪼개져 나눠진다(누가 받을지 예측 불가)
let stream = makeStream()
Task { for await v in stream { print("A", v) } }
Task { for await v in stream { print("B", v) } }
// 출력 예: A 1 / B 2 / A 3 / B 4  ← 아무도 전부 받지 못한다

// ✅ 소비자마다 스트림을 따로 만든다
Task { for await v in makeStream() { print("A", v) } }
Task { for await v in makeStream() { print("B", v) } }
// (같은 값을 모두에게 뿌려야 한다면 broadcast/share 연산자를 쓴다)

쉽게 이해하기

(질문 ①) onTermination손님이 자리를 뜨면 자동으로 불을 끄고 뒷정리하는 스위치예요. 사용자가 화면을 나가면(break·취소) 이 스위치가 눌려서 "이제 위치 그만 받아" 하고 delegate를 뗍니다. 이걸 안 달아두면 화면을 나갔는데도 GPS가 계속 켜져 배터리를 먹어요. 게다가 AsyncStream에는 "끄기 버튼"이 따로 없어서, 이 스위치가 유일한 정리 지점입니다.

(질문 ②) AsyncStream은 라디오 '방송'이 아니라 1:1 택배라고 생각하면 됩니다. 택배는 받는 사람이 한 명이에요. 여러 명이 같은 택배 상자에 손을 대면 물건이 한 명당 하나씩 나눠져서, 결국 아무도 전부 받지 못합니다. 게다가 누가 뭘 받았는지도 그때그때 달라서 버그를 찾기가 어려워요.

그러니 여러 명이 똑같은 걸 받아야 하면 사람마다 택배를 따로 부치거나, 진짜 '방송'을 해주는 도구를 써야 해요. 여기서 함정이 하나 있는데, "여러 명이 받을 수 있다"고 소개된 도구라도 사실은 나눠주기만 하는 것일 수 있으니 확인해야 합니다.

🔑 한 문장

AsyncStream은 방송이 아니라 택배다. 나눠 갖는 것과 다 같이 받는 것은 다르다.