[Swift] Unicode Scalar 그리고 문자열 count 시간 복잡도 관계

OQ·2022년 4월 1일
3

Swift

목록 보기
7/11
post-thumbnail

애플 Swift 공식 문서에 나온 자랑스러운 한글 (펄럭~)

위 이미지는 애플 공식문서에 나온 Unicode Scalar 에 대한 설명입니다.

Unicode Scalar란?

  • 크기가 가변적인 String 문자열을 하나하나 개별적으로 접근하기 위한 방법.
  • Unicode기반 21-bit 코드
  • UTF-32랑 거의 동일하다.
  • 하나 이상의 Unicode Scalar가 모여 Character를 이룬다.

여기서 크기가 가변적인 String 이라함은 다음 예제를 보시면 단번에 이해가실겁니다.

let wordA = "A"
print(wordA.utf8.count) // 1
print(wordA.utf16.count)    // 1
print(wordA.unicodeScalars.count)   // 1

let wordKorean = "권"
print(wordKorean.utf8.count)    // 3
print(wordKorean.utf16.count)   // 1
print(wordKorean.unicodeScalars.count)  // 1

let wordXxck = "凸"
print(wordXxck.utf8.count)  // 3
print(wordXxck.utf16.count) // 1
print(wordXxck.unicodeScalars.count)    // 1

let emoji = "👩"
print(emoji.utf8.count) // 4
print(emoji.utf16.count)    // 2
print(emoji.unicodeScalars.count)   // 1

단일 문자에 대해서 UTF-8, UTF-16, Unicode Scalar의 길이를 카운팅 해봤습니다.
Unicode Scalar만 길이를 1을 가지고 UTF-8, UTF-16은 항상 가변적인 걸 확인하실 수 있습니다.

Q) 그렇다면 단일 문자일 경우에는 Unicode Scalar의 크기는 항상 1인가요?
A) 아니요

다음 예제를 보시죠.

let emoji = "👩"
print(emoji.utf8.count) // 4
print(emoji.utf16.count)    // 2
print(emoji.unicodeScalars.count)   // 1

let emojiWithBlack = "👩🏿"
print(emojiWithBlack.utf8.count)    // 8
print(emojiWithBlack.utf16.count)   // 4
print(emojiWithBlack.unicodeScalars.count)  // 2

let emojiWomanFamily = "👩‍👩‍👧‍👧"  // 두명의 엄마와 두명의 딸로 구성된 가족
print(emojiWomanFamily.utf8.count)    // 25
print(emojiWomanFamily.utf16.count)   // 11
print(emojiWomanFamily.unicodeScalars.count)  // 7

아마 Mac이 아닌 OS던가 브라우저 차이로 이모티콘이 좀 다르게 보일 수도 있으니 사진도 같이 첨부하겠습니다.

한 문자에 대해서 Unicode Scalar의 크기가 7까지 되는 경우도 있습니다.
그러면 여기서 emojiWomanFamily의 문자를 바로 카운트해보면 어떨까요?

let emojiWomanFamily = "👩‍👩‍👧‍👧"  // 두명의 엄마와 두명의 딸로 구성된 가족
print(emojiWomanFamily.count)    // 1

오잉? 우리가 원하는대로 1이 나왔습니다???
문자열에 바로 count하면 Character로 카운팅 됩니다.
Character는 하나 이상의 Unicode Scalar가 모인것이고
Character는 우리가 생각하는 그 문자 하나가 맞습니다.
그러니 1이 나오는 것이죠.
간단히 요약해보면

하나 이상의 Unicode Scalar가 모여 Character를 이루고 Character가 모여 String을 이룹니다!

이상 Unicode Scalar에 대한 설명이었습니다.
꾸벅~

아직 끝나지 않았습니다!!

여기까지 글을 읽으셨다면 아마 이런 생각이 드실 것 같습니다.

Unicode Scalar가 뭔지는 알겠는데 이걸 알아서 뭐에 쓰냐?
실무에 하나도 도움 안된다!

실무에 도움이 될 수도 있습니다!
왜냐하면 우리가 Unicode Scalar에 대해서 알고 있어야지 다음 의문에 대해서 알 수 있기 때문입지요.

크게 2가지 이유가 있습니다.

1. String은 왜 subscript[Int]를 지원하지 않는가?
2. String의 길이를 가져올 때 왜 시간복잡도가 O(1)이 아닌 O(N)인가?

먼저 첫번째 이유에 대해서 알아봅시다.

let string = "A권凸👩👩🏿👩‍👩‍👧‍👧"
print(string[1])	// error: 'subscript(_:)' is unavailable: cannot subscript String with an Int, use a String.Index instead.

아시다시피 다른 언어에서는 다 되는데 Swift는 subscript로 문자열에 접근하는걸 지원하지 않습니다.
위에서 설명했다시피 이유는 문자 크기가 가변적이기 때문입니다.

여기서 또 하나의 의문이 드실겁니다.

UTF-8, UTF-16, Unicode Scalar 다 가변적인건 알겠다.
근데 Character 하나의 길이는 항상 1이지 않는가?
그냥 Character 단위로 가져오면 되는거 아닌가?

네 맞습니다. Character 단위로 체크하면 "A권凸👩👩🏿👩‍👩‍👧‍👧".count 의 길이도 6으로 나올거도 만사 OK 입니다.
여기서 subscript[5]로 접근하면 "👩🏿" 가 출력되겠네요.
실제로 이런 방식으로 접근 가능하게해주는 extension은 스택오버플로우에 많이 널려있습니다.
그런 것들 복붙해서 가져다 쓰셔도 무방합니다!

하지만 왜 Swift에서 공식적으로 제공하지 않을까요?

일단 그 전에 알아두고 가셔야할게 있습니다.
Collection에 접근하는 방식에 관한 프로토콜로는 크게 두가지가 있습니다. (두가지 밖에 몰라욧)

BidirectionalCollection - 후방, 전방 순회를 둘 다 지원하는 컬렉션(Collection) Protocol / 시간복잡도 O(N) 접근
RandomAccessCollection - 바로 접근 가능한 컬렉션 Protocol / 시간복잡도 O(1) 접근

여기서 String은 BidirectionalCollection을 상속하고 있습니다.

extension String : BidirectionalCollection

Unicode Scalar로 접근하는 특성상 직접 접근이 아닌 순차적인 접근으로 가야합니다.
왜냐하면 한 Character가 Unicode Scalar를 몇개 가지고 있을지 알 수 없기에 순차적으로 접근하면서 체크해야하기 때문입니다.

Q) String은 그냥 Array<Character> 아닌감?
A) No

다른 언어에서는 Array<Character> 이런식이기 때문에 바로 subscript로 접근할 수 있었겠지만
Swift는 퍼포먼스에 아주 신경을 많이 쓴 언어기 때문에 그러지 않았습니다.
String은 Unicode Scalar로 이루어져 있고 단지 Character로 뽑아쓸 수 있을 뿐입니다.

여기서 첫번째 질문으로 다시 돌아옵시다.

1. String은 왜 subscript[Int]를 지원하지 않는가?
String은 BidirectionalCollection을 따르고 있기 때문에 직접 접근이 안되기 때문입니다.
여러 오픈소스에서 많이 사용하고 있는데로 공식적으로 제공할 수도 있었겠지만 개발자가 마치 String 배열에 직접 접근하는 듯한 오해를 살 수도 있기 때문에 공식적으로는 지원하지 않는 듯 합니다.

이제 두번째 질문도 답변 가능하게 되었습니다!

2. String의 길이를 가져올 때 왜 시간복잡도가 O(1)이 아닌 O(N)인가?
String은 BidirectionalCollection이기 때문!

그럼 기존 String count 방식과 Character Array로 변환 후 count 방식의 시간복잡도를 확인해봅시다.

var longStr = ""
(0...999999).forEach { _ in longStr += "A권凸👩👩🏿👩‍👩‍👧‍👧" } // 총 6백만 길이 문자열 생성
let longStrArr = Array(longStr) // Character Array로 변환

let start01 = CFAbsoluteTimeGetCurrent()
print(longStr.count)
let diff01 = CFAbsoluteTimeGetCurrent() - start01
print("Took \(diff01) seconds") // Took 3.1775630712509155 seconds

let start02 = CFAbsoluteTimeGetCurrent()
print(longStrArr.count)
let diff02 = CFAbsoluteTimeGetCurrent() - start02
print("Took \(diff02) seconds") // Took 0.00018596649169921875 seconds

기존 String count 방식 - 3.1775630712509155 초

Character Array로 변환 후 count 방식 - 0.00018596649169921875 초

단지 count만 세었을 뿐인데 이렇게나 큰 차이를 보였습니다!

이렇게 Unicode Scalar에 대해서 심층적으로 알아보았습니다.
생각보다 글이 길어졌네요...

오히려 그냥 스택오버플로우 코드 복붙해서 막 쓰면 되지 뭐하러 이런 복잡한 이유를 알아야되나 하실지도...

profile
덕업일치 iOS 개발자

0개의 댓글