← Study Hub

01 · Actor 재진입

await 앞의 검사가 뒤에서 깨지는 이유와 in-flight Task 캐싱
진행률
0 / 0 완료

← 문항 인덱스

면접 질문
  1. await 앞에서 검사한 조건(예: 캐시에 없음 확인)이 await 뒤에도 유효하다고 가정하면 어떤 버그가 생기나요?
  2. 실무에서 어떻게 방어하나요? (상태 재검증, in-flight Task 캐싱 패턴 등)

답변 ① — await 뒤에는 그 가정이 깨진다 🔥 핵심

actor는 한 번에 한 task만 내부 상태에 접근하도록 serial executor로 직렬화합니다. 하지만 await를 만나면 그 task는 잠시 멈추고(suspend), 그동안 executor는 다른 task를 실행할 수 있습니다. 이것이 재진입(reentrancy)입니다.

그래서 await 앞에서 "캐시에 없다"를 확인했더라도, 재개된 시점에는 다른 task가 이미 캐시를 채워 넣었을 수 있어 그 가정이 깨집니다. 구체적으로 두 가지 버그가 납니다.

핵심은 await 사이의 동기 코드만 원자적이고, await 지점마다 상태가 바뀔 수 있다는 것입니다.

⚠️ 주의

"actor니까 직렬 실행이라 안전하다"는 흔한 오답입니다. 직렬성이 보장되는 건 awaitawait 사이 구간뿐입니다.

📝 노트

재진입은 버그가 아니라 의도된 설계입니다. actor가 await 동안에도 다른 task를 받지 않으면 서로를 기다리다 데드락이 나기 때문입니다. 대신 "상태가 바뀔 수 있다"를 개발자가 책임집니다.

❌ 재진입 버그 — await 앞의 검사가 뒤에서 깨진다 (Swift)
actor ImageLoader {
    private var cache: [URL: UIImage] = [:]

    func image(for url: URL) async throws -> UIImage {
        if let cached = cache[url] { return cached }   // (1) "캐시에 없음" 확인
        let image = try await download(url)            // (2) await 동안 다른 task도 같은 url을 download!
        cache[url] = image                             // (3) 중복 fetch + 마지막 값이 덮어씀
        return image
    }
}

답변 ② — 상태 재검증과 in-flight Task 캐싱

실무 방어는 두 가지를 씁니다.

실전에서는 둘을 겹쳐 씁니다. in-flight 캐시로 동시 요청을 하나로 합치고, await에서 돌아온 뒤 최종 상태를 확정합니다.

✅ 방어 — in-flight Task 캐싱 (+ await 뒤 상태 확정) (Swift)
actor SafeImageLoader {
    private var cache: [URL: UIImage] = [:]
    private var inFlight: [URL: Task<UIImage, Error>] = [:]

    func image(for url: URL) async throws -> UIImage {
        if let cached = cache[url] { return cached }
        if let running = inFlight[url] {
            return try await running.value      // 같은 요청은 같은 Task를 공유(중복 제거)
        }
        let task = Task { try await download(url) }
        inFlight[url] = task
        defer { inFlight[url] = nil }

        let image = try await task.value
        cache[url] = image                      // await에서 돌아온 뒤 상태를 확정
        return image
    }
}

쉽게 이해하기

actor를 한 명만 들어가는 1인 화장실이라고 생각해 보세요. 규칙은 "한 번에 한 명"입니다. 그런데 await는 그 안에서 잠깐 문을 열어 다른 사람을 들여보내는 순간이에요.

(질문 ①) "안에 휴지가 없네?" 하고 확인한 다음, 휴지를 가지러 문을 열고 나갔다 옵니다. 그 사이에 다른 사람이 들어와서 휴지를 이미 채워 놨을 수 있어요. 그래서 내가 돌아와서 또 채우면 휴지가 두 번 채워지고(중복 작업), 안의 상태가 내가 알던 것과 달라져 버립니다(상태 불일치).

(질문 ②) 그래서 두 가지를 합니다. 첫째, 돌아오면 다시 한 번 확인하기. 둘째, 같은 심부름을 여러 명이 시키면 새로 보내지 말고 먼저 나간 사람 하나에게 다 같이 결과를 기다리기(in-flight Task 캐싱). 두 번째가 더 좋은 이유는, 첫 번째는 이미 헛걸음을 한 뒤에 알아차리는 거라면 두 번째는 헛걸음 자체를 막기 때문이에요.

🔑 한 문장

await는 "잠깐 문을 열어주는 순간"이다. 그 앞에서 본 것을 그 뒤에도 믿지 마라.