반응형

 iOS 개발자가 많이 하는 실수 - Array append 반복 사용 vs reserveCapacity / Array(repeating:count:) 성능 문제

 

1. 흔히 쓰는 패턴

var list: [Int] = []

for i in 0..<10_000 {
    list.append(i)
}

이 코드는 문법적으로 전혀 문제가 없고, 작은 데이터에서는 성능 문제도 잘 느껴지지 않습니다.

그래서 많은 초보자는 “배열은 그냥 append 하는 거지” 정도로 이해하고 넘어갑니다.

하지만 요소 개수가 많아질수록, 그리고 이 패턴이 여러 곳에 등장할수록

성능과 메모리 효율 측면에서 손해를 볼 수 있습니다.


2. Swift Array 내부 동작 개념

Swift의 Array는 내부적으로 동적 배열(Dynamic Array) 구조입니다.

  • 처음에는 어느 정도 크기의 버퍼를 할당
  • 공간이 부족해지면 더 큰 버퍼를 다시 할당하고, 기존 데이터를 복사한 후 사용
  • 이 과정에서:
    • 새 메모리 할당
    • 기존 원소 복사
    • 이전 메모리 해제
      가 일어나기 때문에 비용이 큼

append를 반복하면:

  • capacity가 꽉 찰 때마다 위 과정을 반복
  • 특히 수만~수십만 개 단위로 쌓이면 누적 비용이 무시 못 할 수준이 될 수 있음

3. 성능 문제 예시

var list: [Int] = []

for i in 0..<100_000 {
    list.append(i)  // 필요 시마다 capacity 늘림
}
  • append 자체가 느린 게 아니라,
  • 중간중간 발생하는 “capacity 확장 + 복사”가 누적되어 성능을 떨어뜨립니다.

Swift는 내부적으로 성장 전략(예: 2배씩 증가 등)을 사용해 재할당 횟수를 줄이지만,

“처음부터 몇 개를 넣을지 알고 있는 경우”라면 미리 capacity를 확보하는 쪽이 훨씬 효율적입니다.


4. 해결 방법 1: reserveCapacity(_:) 사용

4-1. 기본 패턴

var list: [Int] = []
list.reserveCapacity(10_000)   // 미리 10,000개 용량 확보

for i in 0..<10_000 {
    list.append(i)
}

이렇게 하면:

  • 재할당 횟수 감소
  • 전체 복사 비용 감소
  • 반복적으로 사용될 때 성능 안정성 향상

4-2. 언제 쓰면 좋은가?

  • 반복문으로 정확히 또는 대략적인 개수를 알 때
  • 파서/필터/정렬 등에서 배열을 많이 만드는 코드
  • 대규모 데이터 처리 (로그, 통계, 검색 결과 등)

5. 해결 방법 2: Array(repeating:count:) 사용

5-1. 동일한 값으로 초기화할 때

let zeros = Array(repeating: 0, count: 10_000)
  • 10,000개의 0으로 채워진 배열 생성
  • 이 패턴은 다음과 같이 append 반복으로 만드는 것보다 훨씬 효율적입니다.
var zeros: [Int] = []
zeros.reserveCapacity(10_000)
for _ in 0..<10_000 {
    zeros.append(0)
}

위와 같은 코드를 직접 짤 필요 없이

Array(repeating:count:)이 최적화된 형태로 처리해 줍니다.


6. 언제 append 반복만으로도 괜찮은가?

다음과 같은 경우에는 굳이 reserveCapacity까지 신경 쓰지 않아도 됩니다.

  • 원소 개수가 매우 작은 컬렉션 (수십 개 수준)
  • 성능이 전혀 중요하지 않은, 일회성 유틸 코드
  • 이 부분이 전체 성능에서 병목이 되지 않는 것이 확실한 경우

그러나:

  • 루프 안에서 수천·수만 번 호출
  • UI 스크롤이나 실시간 처리(검색, 필터링)와 결합되었을 때
    에는 capacity 전략을 의식적으로 설계하는 것이 좋습니다.

7. 실무 스타일 예시

나쁜 예 (무지성 append)

func buildIndices(from items: [Item]) -> [Int] {
    var indices: [Int] = []
    for (idx, item) in items.enumerated() {
        if item.isValid {
            indices.append(idx)
        }
    }
    return indices
}

개선 예 (대략적인 개수라도 고려)

func buildIndices(from items: [Item]) -> [Int] {
    var indices: [Int] = []
    indices.reserveCapacity(items.count)

    for (idx, item) in items.enumerated() {
        if item.isValid {
            indices.append(idx)
        }
    }
    return indices
}
  • 실제로는 isValid 조건 때문에 items.count보다 적게 들어갈 수 있지만,
  • 그럼에도 미리 capacity를 넉넉히 잡는 편이 재할당을 줄이는 데 도움이 됩니다.

8. 정리

  • append를 반복하면 동적 배열의 특성상 capacity 확장 + 복사 비용이 누적된다.
  • 많은 원소를 다루는 코드에서는:
    • reserveCapacity(_:)로 미리 용량 확보
    • 동일 값 배열은 Array(repeating:count:)로 초기화
  • 작은 데이터나 비성능-critical 코드에서는 단순 append도 괜찮지만,
    “반복적으로 많이 쓰이는 코드”라면 capacity 설계까지 보는 것이 좋다.
반응형
Posted by 까칠코더
,