WWDC/Swift

[Swift] WWDC18 - Using Collections Effectively(1)

유정주 2022. 12. 19. 23:05
반응형

서론

Collection은 개발에 빠질 수 없는 필수 요소입니다.

이번에 살펴볼 WWDC18 - Using Collections Effectively에서는 Collection을 효과적으로 사용하는 방법을 알려줍니다.

함께 알아봅시다~

 

Collection

Collection 없이 개발하는 것은 매우 힘듭니다.

요소 하나하나 출력해줘야 하거나 if - else if ... 를 이용해 모든 케이스를 직접 관리해야 합니다.

 

Collection은 이런 불편함을 줄여줍니다.

위처럼 하나하나 관리했던 문제는

Array를 이용해 간편하게 처리가 가능합니다.

 

Collection은 프로토콜 중 하나로,

요소들이 여러 번 통과할 수 있는 시퀀스면서, subscript를 통해 접근할 수 있습니다.

 

Collection은 startIndex와 endIndex를 지원하고, 이를 이용해 처음부터 끝까지 탐색할 수 있습니다.

subscript를 이용해 Index를 접근하여 해당 원소에 접근할 수도 있습니다.

startIndex와 endIndex, subscript는 이렇게 구현되어 있습니다.

Index는 Comparable 해야 하고, subscript는 Index를 받아서 Element를 반환하네요.

 

Index를 전달받아서 i번 째 뒤의 Index를 반환해주는 index(after:)도 존재합니다.

이 index(after:)는 매우 중요한데요.

이를 이용해 한 index에서 다른 index로 이동할 수 있고,

프로토콜 확장해서 많은 동작을 정의할 수 있게 해주기 때문입니다.

 

Extension Collection

프로토콜 확장 예시를 봐보겠습니다.

배열을 순회할 때 모든 값을 탐색하는 게 아니라 한 개씩 값을 건너뛰어 볼게요.

여기서는 index(after:)에 초점을 맞춰 봐주세요.

startIndex와 endIndex를 이용해 start 부터 end까지 순회를 하는데,

index(after: next)를 이용해 다음 Index로 이동하여 하나의 값을 건너 뜁니다.

출력 -> 건너뛰기 -> 출력 -> 건너뛰기... 가 되는거죠.

 

이렇게 Collection 프로토콜은 다양하게 확장이 가능하며,

모든 Collection 프로토콜 상위에는 Collection 프로토콜이 존재합니다.

이들은 모두 Collection이 제공하는 기본 특성에 무언가 확장이 된 것입니다.

예를 들어,

BidirectionalCollection은 index(before:)를 이용해 뒤로도 이동할 수 있고,

RandomAccessCollection은 여기서 더 확장해서 특정 위치로 바로 이동할 수도 있습니다.

 

Swift에서는 여러 곳에서 Collection을 채택하여 사용합니다.

Array, Set, Dictionary, NSArray, NSSet, NSDictionary 등등에서요.

이들에 대해 더 자세히 알아봅시다.

 

Indices

각 Collection에는 고유한 Index가 존재합니다.

이 인덱스는 Comparable 해야합니다.

많은 곳에서 인덱스를 정수로 사용하지만, 꼭 정수만을 사용해야 하는 것은 아닙니다.

Dictionary만 하더라도 String을 인덱스로 사용하죠? ㅎㅎ

 

그렇다면, 배열의 첫 번째 요소는 어떻게 얻을까요?

Array에서는 array[0] 으로 대답할 수 있겠죠.

 

하지만 Set에서도 그럴까요?

전혀 그렇지 않죠 ㅠㅠ

집합은 순서가 존재하지 않으니까요.

 

이런 문제때문에 Swift는 startIndex를 지원합니다.

둘은 모두 잘 동작할 것처럼 보여요.

정렬에 상관없이 모두 첫 번째 아이템을 가져오니까요.

 

그렇지만!

이 코드는 런타임 에러, Index out of range가 발생할 수 있습니다.

왜냐하면 Collection에 내용이 없을 수도 있기 때문입니다.

Collection이 비어있어도 시작 인덱스는 존재하기 때문에 컴파일 에러는 발생하지 않지만 런타임 에러는 발생할 수 있습니다.

 

이럴 때를 대비해 Swift는 first 프로퍼티를 제공합니다.

first를 이용하면 Collection이 비어 있으면 nil을, 원소가 존재하면 첫 번째 원소를 반환합니다.

 

그렇다면 Collection을 확장하여 두 번째 원소를 제공하는 second 프로퍼티를 만들어 볼까요?

  1. startIndex와 endIndex를 비교하여 Collection이 비어 있는지 확인합니다.
  2. index(after:)를 이용해 startIndex 뒤의 index를 구합니다.
  3. 2번에서 구한 index가 endIndex인지 확인하고,
  4. endIndex가 아니라면 반환합니다.

인덱스의 특성을 이용해 안전하게 second를 구했습니다.

 

Slice

Swift는 Slice라는 더 좋은 방법을 제공합니다.

Slice는 Collection의 일부분을 의미하는 타입입니다.

 

모든 Slice는 각자의 startIndex와 endIndex를 가지고 있는데,

이를 이용해 secondIndex를 구해봅시다. 

원본 Array에서 맨 앞 원소를 없앤 Slice를 생성하면,

그 Slice의 startIndex는 원본 Array의 secondIndex와 동일하므로 같은 결과가 나오게 됩니다.

이 원리를 이용하면

이렇게 간편하게 처리가 가능합니다.

 

Slice는 매우매우 긴 Collection에서 일부분만 필요할 때도 유용합니다.

예를 들어, 절반의 Collection 부분만 필요하다고 합시다.

그럼

이렇게 절반을 날린 Slice를 반환하도록 구현할 수 있습니다.

 

Slice는 원본 Collection을 메모리에 살려둔다.

Slice는 원본 Collection을 메모리에 그대로 살려둡니다.

firstHalf를 만든 뒤 array에 []를 대입한다고 합시다.

array에 빈 []를 대입했음에도 array는 메모리에 그대로 남아있습니다.

이 array는 firstHalf도 함께 빈 []로 할당해야만 메모리에서 사라집니다.

 

lazy

이러한 특성은 다른 Context에서 정말 유용하게 사용됩니다.

아래 예시를 보세요.

이 코드는 1 ~ 4000까지의 숫자에 2를 곱한 후 10보다 작은 수를 구합니다.

최종 결과는 2, 4, 6, 8 뿐이지만 4004개의 요소를 위한 공간을 할당했습니다.

최종 결과를 얻기 위한 과정에서 메모리 손해가 발생합니다.

이를 방지하는 방법을 바로 lazy라고 부릅니다.

 

lazy를 적용했습니다.

1. 이제 (1...4000)은 LazyCollection<Range<Int>>로 wrapping 됩니다.

2. map 단계에서는 LazyMapCollection<Range<Int>>로 wrapping 됩니다.

3. filter 단계에서는 LazyFilterCollection<LazyMapCollection<Range<Int>>> 로 wrapping이 됩니다.

 

여기서 주의할 점은!

지금 이 과정은 그저 Wrapping을 하는 것 뿐이지 처음 예시처럼 메모리 할당을 하는게 아니라는 점입니다.

 

이제 items에서 첫 번째 원소를 요청해보겠습니다.

first 요청은 filter -> map -> range 순서로 처리됩니다.

1...4000에서 first 원소인 1을 가져오고, LazyMapCollection에서 2가 되서 LazyFilterCollection에서 조건 판단을 거친 후 결과가 출력됩니다.

 

Range -> Map -> Filter 순서로 처리되었던 첫 번째 예시와는 정반대 순서이고,

덕분에 4004개의 메모리 할당이 큰 폭으로 줄어들었습니다.

 

이렇게 lazy를 이용하면 필요한 만큼만 계산할 수 있습니다.

 

또한 중간 저장소를 생성하면 반복적인 처리를 줄일 수 있습니다.

위 예시는 first를 요구할 때마다 grizzly -> Gummy Bears까지 순회하는 문제를

중간 저장소를 생성하여 해결한 모습입니다.

 

lazy를 사용해야 하는 순간

lazy는 연쇄적인 map, filter에서 오버헤드를 줄이는 정말 좋은 방법입니다.

Collection에서 계산된 결과 일부분만 필요한 경우 lazy가 유용합니다.

하지만 lazy는 세부적인 구현을 요구한다는 점을 고려해야 합니다.

 


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

반응형