반응형

Swift에서 Lazy 컬렉션 

 

Swift에서 컬렉션 연산은 기본적으로 즉시 평가(Eager) 입니다. map, filter 등을 체인으로 연결하면 단계마다 중간 배열이 생성되어 메모리·시간 비용이 쌓일 수 있습니다. 이때 .lazy를 사용하면 지연 평가(Lazy Evaluation) 로 동작하여 중간 배열을 만들지 않고 요소가 필요할 때만 계산합니다.

 

1. 핵심 요약

  • .lazy는 Sequence/Collection 위에 게으른 뷰를 제공합니다.
  • lazy.map / lazy.filter / lazy.compactMap 등은 중간 배열을 만들지 않음.
  • first(where:), contains(where:) 와 함께 쓰면 조기 종료 + 무중간 메모리.
  • 최종적으로 배열이 필요하다면 Array(...) 또는 .map { $0 } 같은 종단 연산으로 materialize.

 

2. 언제 쓰나?

  • 대용량 데이터에서 map → filter → map ... 체인이 길 때
  • contains(where:), first(where:) 처럼 일부만 검사하면 되는 로직
  • 비용이 큰 변환(예: 정규식·JSON 파싱·이미지 처리)의 불필요한 계산 회피
  • SwiftUI·Combine 환경에서 지연된 데이터 변환이 필요할 때

 

3. 기본 문법과 타입

let numbers = Array(0..<10)

let eager = numbers.map { $0 * 2 }                   // 즉시 평가, 중간 배열 생성
let lazyMapped = numbers.lazy.map { $0 * 2 }         // LazyMapSequence<Int>
let resultArray = Array(lazyMapped)                   // 여기서 비로소 materialize

numbers.lazy.map { ... }의 반환 타입은 LazyMapSequence 로, 요소를 접근할 때마다 변환이 수행됩니다.

 

4. 성능 관점: 중간 배열 제거

let users = (0..<1_000_000).map { "user-\($0)" }

// Eager (비추천: 중간 배열 2번 생성)
let e1 = users
    .map { $0.uppercased() }          // 큰 중간 배열 #1
    .filter { $0.hasPrefix("USER-9") } // 큰 중간 배열 #2
    .first                              // 첫 요소만 필요→낭비

// Lazy (추천: 중간 배열 생성 없음, 첫 매칭에서 종료)
let e2 = users.lazy
    .map { $0.uppercased() }
    .filter { $0.hasPrefix("USER-9") }
    .first()                            // LazySequence의 확장 메서드

위 lazy 버전은 첫 매칭에서 즉시 종료하며, 중간 배열이 전혀 생성되지 않습니다.

 

5. 종단 연산(Terminal Operation)

지연 평가를 끝내고 실제 컬렉션으로 만들려면 아래 같이 materialize 합니다.

let out = Array(numbers.lazy.map { $0 * $0 }) // 여기서만 배열 할당 발생


다만 정말 배열이 필요할 때만 materialize 하세요. 그렇지 않다면 지연 상태 유지가 성능상 유리합니다.

 

6. Lazy + 조기 종료(best combo)

let big = 1...10_000_000

// Lazy + first(where:) = 첫 true에서 즉시 종료, 중간 메모리 0
if let n = big.lazy.first(where: { $0 % 777_777 == 0 }) {
    print("hit:", n)
}

// Lazy + contains(where:) = 존재 여부만 빠르게
let hasSquare = big.lazy.contains { x in
    let r = Int(Double(x).squareRoot())
    return r * r == x
}

 

7.  map / filter / compactMap / reduce와의 관계

  • map, filter, compactMap은 lazy에서 지연 변환으로 동작합니다.
  • reduce는 lazy라도 모든 요소를 소비하는 종단 연산이므로 중간 배열은 없지만 순회는 끝까지 합니다.swift let s = (1...10_000_000).lazy .filter { $0.isMultiple(of: 2) } .reduce(0, +) // 전체 순회 필요 (그래도 중간 배열 없음)

 

8. Lazy에서 주의할 점

  1. 기저 컬렉션 변경에 민감: lazy 뷰는 기저 데이터에 대한 입니다. 기저를 변경하면 지연 결과도 달라집니다.
  2. 다중 순회 비용: LazySequence는 필요할 때마다 계산합니다. 같은 lazy를 여러 번 순회하면 중복 계산이 발생할 수 있습니다. 재사용하려면 materialize.
  3. 부수 효과 금지: lazy 변환 클로저에서 외부 상태를 변경하는 부수 효과는 피하세요. 평가 시점이 지연되어 예측이 어려워집니다.
  4. 측정은 릴리즈 빌드에서: 성능 평가는 반드시 Release + 최적화로 확인.

 

9. 실전 패턴


9.1 Lazy + 파일 스트림(의사 코드)

// 의사 코드: 한 줄씩 읽히는 시퀀스를 lazy 변환한다고 가정
let lines: AnySequence<String> = FileLineSequence(url: logURL).eraseToAnySequence()

let firstError = lines.lazy
    .compactMap { $0.range(of: "ERROR:") != nil ? $0 : nil }
    .first

9.2 Lazy + 대용량 JSON 배열 변환

struct User: Decodable { let id: Int; let name: String }
let raw: [Data] = loadLargeJsonArray()  // 수만 개 Data 조각

// 중간 배열 없이 필요한 때만 디코딩
let stream = raw.lazy.compactMap { try? JSONDecoder().decode(User.self, from: $0) }
if let firstAdmin = stream.first(where: { $0.name == "admin" }) {
    print(firstAdmin)
}

9.3 Lazy + 중첩 컬렉션 평탄화

let matrix = [[1,2,3],[4,5,6],[7,8,9]]
let evens = matrix.lazy
    .joined()             // LazyFlattenSequence
    .filter { $0 % 2 == 0 }
    .map { $0 * 10 }
    .prefix(3)            // 3개만 필요
print(Array(evens))       // [20, 40, 60]

9.4 Lazy + 문자열 처리

let text = "   Hello, Swift Lazy!   "
let tokens = text.split(separator: " ").lazy
    .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
    .filter { !$0.isEmpty }
print(Array(tokens))   // ["Hello,", "Swift", "Lazy!"]

9.5 Lazy + 이미지 파이프라인(의사 코드)

// 의사: 썸네일 URL 시퀀스를 lazy 로 내려받고 디코딩
let thumbs: [URL] = loadManyThumbURLs()
let decoded = thumbs.lazy
    .compactMap { try? Data(contentsOf: $0) }
    .compactMap { UIImage(data: $0) }      // iOS
    .prefix(20)                            // 필요한 개수만
let first20 = Array(decoded)

 

10. eager vs lazy 비교 테이블

구분 Eager 체인 Lazy 체인
중간 배열 단계별 생성 생성 안 함
메모리 사용 커짐 최소
조기 종료 어려움 쉬움 (first, contains)
계산 시점 즉시 접근 시
코드 복잡도 동일 동일(선두에 .lazy만 추가)

 

11. 벤치마크 스켈레톤

import Foundation

let data = Array(0..<2_000_000)

func measure(_ name: String, _ block: () -> Void) {
    let t1 = CFAbsoluteTimeGetCurrent()
    block()
    let t2 = CFAbsoluteTimeGetCurrent()
    print(name, ":", t2 - t1, "sec")
}

measure("eager-first") {
    _ = data.map { $0 * 2 }.filter { $0 % 3 == 0 }.first
}

measure("lazy-first") {
    _ = data.lazy.map { $0 * 2 }.filter { $0 % 3 == 0 }.first()
}

measure("eager-materialize") {
    _ = data.map { $0 * 2 }.filter { $0 % 3 == 0 }
}

measure("lazy-materialize") {
    _ = Array(data.lazy.map { $0 * 2 }.filter { $0 % 3 == 0 })
}

 

12. 참조 링크

 

13. 결론

  • .lazy는 중간 배열 제거 지연 평가로 성능과 메모리를 동시에 잡는 강력한 도구입니다.
  • 조기 종료가 가능한 종단 연산(first, contains, prefix, prefix(while:))와 결합하면 효과가 극대화됩니다.
  • 부수 효과 없는 순수 변환을 작성하고, 정말 필요할 때만 materialize 하세요.
반응형
Posted by 까칠코더
,