왜 Swift 에서 문자열은 인덱스로 조회하지 못할까?

DongKyu Lee·2021년 10월 10일
2

Swift

목록 보기
1/1
post-thumbnail

Swift 라는 언어를 다루면서 제일 흥미로운 부분 중 하나가 바로 문자열 이었다. 내가 정말 애정하는 공간인 42서울 에서는 Piscine 이라는 선발과정부터 본과정까지 수 없이 많이 다루어야 한다. 그래서 그런지 스위프트의 독특한 문자열 자료구조는 많은 궁금증을 일으켰다.

1. C, C++, Python 과 비교해보기

기본적으로 C, C++ 언어 에서는 문자열은 말 그대로 char 타입의 데이터의 배열 구조 형태로 메모리에 저장된다. 물론 C++ 에서는 std::string 이라는 정말 편리한 자료형을 사용할 수 있기때문에 조금은 다르지만 핵심은 여전히 같다고 볼 수 있다.

여기서 char 타입은 Ascii 표준을 준수하기 때문에 한 글자당 1 바이트 만으로도 저장할 수 있었다.

파이썬은 조금 다르다. 우선 문자열 객체로서 메모리에 적재되고, sys.getsizeof() 로 한 글자가 얼마나 메모리에 적재되는지 확인해보면 알파벳은 1바이트, 한글은 유니코드 방식 으로 저장되기 때문에 2바이트로 저장됨을 볼 수 있다.

  • 파이썬에서 문자의 메모리 사용량 확인해보기
import sys

en_str = "A"
ko_str = "ㄱ"

print(sys.getsizeof(en_str))	#49 + 1
print(sys.getsizeof(ko_str))

""" 
[공식문서 해설]
https://docs.python.org/ko/3/library/sys.html#sys.getsizeof
[참고자료]
https://wikidocs.net/15102
"""
  • C 에서 문자열 선언과 조회(스택에 정적할당 했을 때)
char str[13] = "hello world!";
printf("idx: 6, str: %c\n", str[6]);
  • C++ 에서 문자열 선언과 조회
std::string str = "hello world!";
std::cout << "idx: 6, str: " << str[6] << std::endl;
  • Python 에서 문자열 선언과 조회
str = "hello world!"
print("idx: 6, str: ", str[6])

임의로 인덱스를 정해서 hello world! 문자열에서 한 글자를 조회해서 출력해보았다. 모든 결과는 idx: 6, str: w 로 출력된다.

물론 파이썬의 문자열 객체는 한 글자당 1바이트만 저장하는 시퀀스는 아니지만, 그럼에도 불구하고 여전히 파이썬 또한 Int 자료형 인덱스로 문자열을 접근할 수 있다.

2. 그러면 Swift는 문자열을 어떻게 조회할까?

이 질문을 꺼내기 전에 정말 재밌는 영상 하나를 보면 좋을 것 같다. Ascii 코드유니코드(utf-8) 의 기원에 대해 설명해주는 영상인데, 영상 중간중간 디테일이 정말 재밌다.

Computerphile

이 영상이 필요했던 이유는 애플에서 문자열 자료구조를 구현할 때 유니코드 표준을 준수했기 때문이고, 이게 또 문자열 조회와도 연관이 있기 때문이다. 스위프트 공식문서 참고

다시 돌아와서, Swift 에서 문자열을 정수 인덱스로 조회한다면? 아래처럼 오류를 뱉는다.

인덱스 조회 에러

그러면 Swift 에서는 어떤 방식으로 조회해야 되냐면 아래처럼 String.Index 를 사용해서 접근해야한다.

// 선언
let str = "hello world!"

// 조회
str[str.startIndex]	// 'h'
str[str.endIndex]	// '!' 가 나올지 알았겠지만 바로 에러 뜨지롱

var idx = str.index(str.startIndex, offsetBy: 6)
str[idx]		// 'w'


// 이터레이터(반복문)를 인덱스처럼 뽑아 사용하려면 indicies 메소드 사용해야한다.
for c in str.indices {
    print(str[c])	// Index(_rawBits:) 값이 출력된다. 
}

이걸 보면 마음속 깊은 곳에서 한 마디가 우러나온다.

아니 왜?

hello world! 라는 문자열에서 w 를 접근하는데 Python 에서는 str[6] 이면 끝났는데 Swift 에서는 str[str.index(str.startIndex, offsetBy: 6) 을 해야 비로소 글자 하나에 접근할 수 있는거다. 겉으로만 보면 str str str... 변수명을 무려 세 번이나 써야된다.

분명히 C, C++ 언어부터 정수 인덱스로 접근하는 효율적인 방법이 이미 있는데, 현대 언어의 패러다임을 섞어놓았다는 Swift 에서는 왜 이런 비효율이 생긴걸까

3. Swift의 문자 저장 구조

앞선 언어들의 문자열은 각각 인덱스마다 동일한 크기의 데이터 값을 가지고 있었다. Swift 공식문서 에 따르면, Character 타입의 데이터는 하나의 확장된 문자소 집합(Extended Grapheme Clusters) 으로 표현된다.

Extended Grapheme Clusters

Swift 에서 Character 자료형은 '사람이 읽을 수 있는 하나의 글자' 를 기준으로 한다. 그래서 하나의 글자가 여러 개의 유니코드 스칼라 값을 가질 수 있고, 이에 따라 각 글자마다 메모리가 다를 수 있다.

위의 공식문서에서 하나의 Character 타입이 여러 스칼라 값을 가질 수 있다는 것을 한글로 예를 들었다.

let precomposed: Character = "\u{D55C}"                  // 한
let decomposed: Character = "\u{1112}\u{1161}\u{11AB}"   // ᄒ, ᅡ, ᆫ
// precomposed is 한, decomposed is 한

예시를 보면, 으로 합쳐있는 글자는 하나의 유니코드 스칼라 값을 갖는데 아래는 3 개의 유니코드 스칼라 값을 갖는다. 하지만 중요한 점은 둘 다 똑같은 하나의 Character 라는 것이다.

이런 이유 때문에 다른 언어처럼 str[6] 으로 접근하려 했다면, 구체적으로 어딜 접근하려했는지 의도가 명확하지 않게 되어버린다.

왜냐면, 사실 str[6] 에서 6 이라는 숫자는 메모리를 배열처럼 점유해서 그 중 6번째 인덱스를 보는 것이기 때문이다. Swift 처럼 한 글자마다 여러 개의 유니코드 스칼라 값을 가질 수 있다면 각 글자마다 메모리가 다르기 때문에 정확히 i 번째는 아닌 것이다.

인덱스로 접근을 못한다고 했을 때 또 하나의 의문점 이 든다. 바로, String.Index 를 리턴하는index(_:offsetBy:) 메소드 이다. 해당 메소드는 Swift 가 제공하는 첫 번째 파라미터로 부터 offsetBy: 만큼 떨어진 위치를 반환한다.

여기서 "offsetBy: 만큼 떨어진" 이 라는 것이 무슨 말인지 중요하다. 왜냐면 인덱스로 조회하지 못한다고 했는데, 이 메소드를 사용하면 어쨌든 인덱스로 조회하는 '것 처럼' 보여지지 않는가.

우선 String.Index 가 무엇인지 부터 공식문서를 봐보자.

String.Index 1String.Index 2

위 공식문서에 따르면 String 자료형에서 Index 라는 구조체는 BidirectionalCollection 이라는 프로토콜을 채택한 구조체이다.

BidirectionalCollection & RandomAccessCollection

String.Index 구조체가 BidirectionalCollection 이라는 프로토콜을 채택한 것은 문자열의 인덱스 탐색 구조가 양방향 순회방식 이라는 것이다. 그 말인 즉슨, 무조건 자신의 앞, 뒤로만 탐색할 수 있다는 것이다.

보통은 문자열을 Random Access 방법으로 임의의 거리만큼 떨어진 원소를 O(1) 의 시간복잡도로 참조할 수 있는 조회 성능을 가진다. 하지만, 애플은 각 글자마다 유연한 메모리 크기를 가져간 대신 이런 성능을 포기한 것 같다.

BidirectionalCollection 프로토콜로 인덱스 접근을 구현함으로써 O(n) 의 시간복잡도가 필요해졌다.

이러한 이유로, 문자열의 시작점으로 부터 임의의 거리만큼 떨어진 원소를 참조하기 위해서는 이전 단계를 모두 거쳐야 하는 것이다.

이러한 접근 방식으로 구현되어 각각의 문자소 집합 만큼 떨어진 distance 를 계산한다. 물론, 원한다면 utf-8, utf-16, unicodeScalars 방식으로 계산할 수도 있겠다.

4. 결론

Swift 는 다양한 방식의 언어와 특수문자들에 대응하기 위해 확장된 문자소 집합(Extended Grapheme Clusters) 을 채택해서 한 글자당 유연한 메모리 크기를 갖게하고, 임의의 지점으로 부터 그 크기만큼 떨어진 원소를 Bidirectional 하게 탐색하여 문자열의 원소에 접근하는 것이다.

Swift 의 이런 문자 저장 구조는 한정된 메모리 내에서 전 세계 사람들에게 서비스 해야되는 고민들 속에서 나온 구조라고 생각된다.
물론 그래도 별로다.

profile
Junior iOS Developer

0개의 댓글