반응형

Swift에서 reduce(into:) vs reduce

 

Swift의 reduce 계열은 컬렉션을 하나의 누적 결과로 접어(fold) 만드는 핵심 API입니다. 비슷해 보이지만 reduce(into:)와 reduce는 메모리/성능 특성 사용 의도가 다릅니다. 실무에서 어떤 것을 선택해야 하는지 예제와 함께 정리합니다.

 

1. 핵심 비교 요약

항목 reduce(into:_:) reduce(_:_:)
누적자(accumulator) 가변(inout) 누적자를 인자로 전달하여 제자리(in‑place) 수정 불변(immutable) 누적자를 매 단계 새로 생성하여 반환
메모리/성능 복사 최소화 (특히 Array/Dictionary/Set 등 COW 타입) 누적자 복사가 잦아질 수 있어 상대적으로 느릴 수 있음
사용 목적 집계/그룹화/누적 갱신  상태를 바꾸며 쌓는패턴 수학적 합성, 순수 함수형 변환  새 값을 만드는 패턴
가독성 명령형(in‑place) 스타일에 명확 함수형(immutable) 스타일에 명확
예시 카운팅, 버킷/그룹핑, 딕셔너리/집합 빌드 합계, 곱, 문자열 결합(하지만 joined가 보통 더 효율적)

 

2. 시그니처와 동작 차이

// 1) reduce(into:)
func reduce<Result>(into initialResult: Result, _ updateAccumulatingResult: (inout Result, Element) throws -> Void) rethrows -> Result

// 2) reduce
func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result) rethrows -> Result
  • reduce(into:): 누적자를 inout으로 받아 제자리 수정합니다. 클로저는 값을 반환하지 않고 누적자만 변경합니다.
  • reduce: 누적자의 새 값을 매 스텝 반환합니다. 불변 값 조합에 적합합니다.

 

3. 왜 into가 더 빠를 수 있나? (COW와 복사 비용)

Swift 표준 컬렉션(Array/Dictionary/Set)은 Copy‑On‑Write(COW) 입니다. reduce는 단계마다 새 누적자를 만들어 반환하므로, 누적자가 커질수록 할당/복사 비용이 커질 수 있습니다. 반면 reduce(into:)는 단일 버퍼를 재사용하며 in‑place로 수정해 불필요한 복사/할당을 줄입니다.

큰 딕셔너리/배열을 누적 생성하는 경우 reduce(into:)가 체감상 유의미하게 빠른 경우가 많습니다.

 

4. 예제 모음


4.1 빈도수 카운트(히스토그램)

let words = ["a", "b", "a", "c", "b", "a"]

// 권장: reduce(into:)
let histogram1 = words.reduce(into: [:]) { acc, w in
    acc[w, default: 0] += 1
}

// 대안: reduce (불변 누적자)
let histogram2 = words.reduce(into: [:]) { acc, w in // <- into가 훨씬 간결/효율적
    acc[w, default: 0] += 1
}

같은 작업을 reduce로만 구현하려면 매 단계 새 딕셔너리를 만들어야 하므로 비효율적이고 장황해집니다.

4.2 그룹핑(카테고리 → 배열)

struct Item { let cat: String; let value: Int }
let items = [Item(cat: "A", value: 1), Item(cat: "B", value: 2), Item(cat: "A", value: 3)]

let buckets = items.reduce(into: [String: [Item]]()) { acc, it in
    acc[it.cat, default: []].append(it)
}

4.3 집합(Set) 빌드

let names = ["kim", "lee", "kim", "park"]
let unique = names.reduce(into: Set<String>()) { acc, n in
    acc.insert(n)
}

4.4 문자열/배열 합성 (하지만 표준 전용 API 권장)

let words = ["Hello", "Swift", "World"]

// reduce로 문자열 결합 (가능하지만 비권장)
let combined1 = words.reduce("") { $0 + ($0.isEmpty ? "" : " ") + $1 }

// 권장: joined (문자열 전용 고성능 API)
let combined2 = words.joined(separator: " ")

4.5 수치 집계는 reduce가 자연스러움

let nums = [1, 2, 3, 4]

// 합/곱/최댓값 등 순수 함수형 조합
let sum = nums.reduce(0, +)
let product = nums.reduce(1, *)
let maxVal = nums.reduce(Int.min, max)

4.6 커스텀 누적 타입 빌드 (성능상 into 선호)

struct Stats { var min = Int.max; var max = Int.min; var sum = 0; var count = 0 }

let stats = nums.reduce(into: Stats()) { s, x in
    s.min = Swift.min(s.min, x)
    s.max = Swift.max(s.max, x)
    s.sum += x
    s.count += 1
}
let avg = Double(stats.sum) / Double(stats.count)

 

5. 언제 무엇을 써야 하나? 선택 기준

상황 권장 API 이유/비고
딕셔너리/집합/배열을 점진적으로 “쌓는” 작업 reduce(into:) in‑place 갱신으로 빠름/간결
카운팅/그룹핑/버킷화 reduce(into:) default: 키 경합에 적합
순수 수치 집계(합, 곱, min/max 등) reduce 불변 조합이 가독성↑
문자열 결합 joined 전용 최적화 API가 더 효율적
존재/첫 매칭 확인 contains/first 조기 종료
중간 자료구조 피하고 싶음 lazy + reduce(into:) 대용량에서 메모리 절감

 

6. Lazy와 함께 쓰기

let big = (1...2_000_000)

// 중간 배열 없이 필터 후 합계
let evenSum = big.lazy
    .filter { $0.isMultiple(of: 2) }
    .reduce(0, +)                 // 전체 순회 필요 (종단 연산)

// 누적 자료구조 빌드는 into 선호
let top100Buckets = Array(
    big.lazy
       .map { ($0 % 10, $0) }     // (키, 값)
       .prefix(1000)              // 조기 종료
).reduce(into: [Int: [Int]]()) { acc, pair in
    acc[pair.0, default: []].append(pair.1)
}

 

7. 흔한 안티패턴과 교정

1) 큰 누적자를 reduce로 생성

 reduce(into:)로 전환하여 복사 비용 절감.

2) 문자열/배열 결합을 reduce로 구현

 joined 또는 append(contentsOf:) 사용.

3) 존재 확인을 reduce로 계산

 contains(where:)/first(where:)가 의미/성능 모두 우수.

4) 부수 효과 많은 클로저

→ 누적 로직은 순수하게 유지. I/O나 로그는 외부로 분리.

 

8. 간단 벤치마크 스케치 (개념 설명용)

import Foundation

let input = (0..<200_000).map { "v\($0)" }

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

// 1) Dictionary 누적: reduce(into:) vs reduce
measure("into-dict") {
    _ = input.reduce(into: [:]) { (acc: inout [String: Int], v) in acc[v] = v.count }
}

measure("reduce-dict") {
    _ = input.reduce([:]) { (acc: [String: Int], v) in
        var next = acc; next[v] = v.count; return next
    }
}

일반적으로 into-dict가 더 빠르고, 메모리 사용량도 적습니다(환경에 따라 다름).

 

9. 참조 링크

 

10. 결론

  • 상태를 쌓는 누적(집계/그룹화/딕셔너리/집합 빌드)  reduce(into:)가 성능·가독성 모두 우세.
  • 순수 함수형 조합(합/곱/최댓값 등)  reduce가 자연스럽고 간결.
  • 문자열 결합/존재 확인/첫 매칭 등은 전용 API(joined, contains, first)를 우선 고려.

 

반응형
Posted by 까칠코더
,