Rust 비기너의 주저리

- 4 mins

최근 Rust를 보기 시작했습니다. 특별히 이 언어가 필요한 상황은 아니기에 가볍게 살펴보는 중이지만, 그래도 기왕 보는거 뭐라도 간단하게 만들어보자 라는 생각에 Rust를 이용해 JSON 파서를 만들기로 했습니다. 이번 포스팅에서는 Rust로 JSON 파서를 만들며 Rust 언어가 가지는 몇몇 특이한 특징들에 대한 개인적인 감상을 적어볼까 합니다. 특히 파서를 작성하며 겪은 스트링 처리에 대한 시행착오 또는 주저리가 대부분이 될 것 같습니다.

Unicode 문자 집합

Rust는 언어적으로 기본 문자를 Unicode 형태로 관리합니다. C++에서 문자열 인코딩으로 개고생을 해 본 사람으로써 이것만으로도 Rust가 칭찬받기에 충분하다고 생각합니다. 큰 관점에서 보자면 Unicode 형식의 문자열 관리는 정말 최고에요. 그렇지만 스트링 처리의 관점에서 보자면 이게 꼭 좋기만 할까요?

Rust의 문자열은 str 타입의 string slice와 std::string::String의 표준 컬렉션으로 다룰 수 있습니다. 이 둘은 모두 UTF-8 인코딩으로 문자를 다루죠. UTF-8은 다수의 프로그래머에게 익숙한 1 바이트 ANSI 인코딩과 다르게 문자 하나의 크기가 통일되어 있지 않습니다. 그래서 구조적으로 문자 단위의 인덱싱이 불가능합니다. 스트링 처리를 하는데 문자 단위로 인덱싱이 불가능하니, 지금껏 만들어본 대다수의 스트링 처리 알고리즘 구현 방식들이 무용지물이 되버립니다.

물론, 처리할 대상의 범위를 특정한다면 기존의 처리 방식을 그대로 적용할 수 있습니다. 영문 알파벳만을 처리하겠다면 u8 타입으로 인덱싱하여 처리할 수 있겠고, 한글만 처리한다면 2 바이트 단위로 끊어 처리할 수 있을 것입니다. 하지만 언어 차원에서 Unicode를 지원하는데, Rust를 이용해 스트링 처리 모듈을 만들어 배포하려면 최소한 언어 표준은 따라 줘야겠죠… 당연히 해야 하지만 왠지 할 일이 늘어난 것만 같은 꿀굴한 기분이 듭니다.

Unicode 문자 형식으로 스트링 처리 모듈을 제작한다면, 기본적인 문자열 길이 반환 함수인 len() 조차 믿을 수 없게됩니다. str의 len() 함수는 u8 버퍼의 길이를 반환하는 함수인데, UTF-8 인코딩된 문자열의 길이는 문자 하나가 1 바이트라는 보장이 없기 때문에 len() 결과가 사람이 인식하는 문자열의 길이와 다를 수 있습니다. UTF-8 인코딩된 문자열의 길이를 아는 방법은 문자열 전체를 순회하며 카운팅하는 것 만이 유일합니다. Rust에서는 String의 이터레이터 타입인 std::str::Chars 에서 count() 함수로 전체 길이를 알 수 있습니다. 그런데 얘는 O(n)이죠.

이 외에도 UTF-8 인코딩 문자열을 다루기 때문에 신경써줘야 할 부분들이 꽤 있습니다. 영문 알파벳 처리에만 익숙한 저로서는 Unicode 지원이 반가우면서도, 신경 써야할 것들이 많아져 부담스러운 점도 있네요. 막상 프로그래밍을 하며 모국어인 한글을 비중있게 다룬 경험이 없었던 제 미숙함이 부담의 원인일지도 모르겠습니다.

다양한 이터레이터 타입

Rust에서 JSON 파서를 짜기 시작하며 봉착한 첫 번째 관문은 String이 제공하는 다양한 유형의 이터레이터였습니다. 기본적인 문자열 처리 이터레이터인 Chars 타입은 이터레이팅을 위해 next() 함수만을 가지고, 이 함수는 값 반환과 동시에 이터레이터를 어드밴스(advance)하기 때문에 문자열을 다룰 때 곤란한 점이 많습니다.

다음 값을 확인만 하고싶은 상황에서는 구현된 Iterator trait의 peekable() 기능으로 std::itr::Peekable 타입의 이터레이터를 생성할 수 있습니다. 그런데 재미난 것은, Rust에서는 기본적으로 값의 대입이 move semantic으로 처리되기 때문에 Chars 타입의 이터레이터 it를 peekable() 하면 기존 변수인 it는 더 이상 사용할 수 없게되죠.

원본 Chars 타입 이터레이터의 소실을 원치 않는다면 clone() 후 peekable() 할 수 있겠습니다만, 이렇게 한다면 Peekable 타입 이터레이터를 어드밴스 할 때 Chars 타입 이터레이터도 같이 어드밴스 해줘야 하는 번거로움과 코드 중복이 생깁니다.

게다가 String의 Chars 이터레이터로부터 만들어진 Peekable은 Peekable<Chars> 타입입니다. 만약 take_while() 까지 쓴다면 TakeWhile<Peekable<Chars>> 타입이 되겠죠. 그런데 이 타입을 벗겨낼 방법이 없습니다. 원본을 갖고있지 않는 한 원본으로 돌아갈 방법이 없는 것이죠.

가장 난감한 부분이었는데, 이건 제가 Rust의 이터레이터 타입에 대한 이해가 부족했기 때문이었습니다. 원본을 잃지 않으려면 by_ref()로 레퍼런스 타입의 이터레이터를 만들어야 하는 것이죠. 이렇게 이터레이터를 바로잉(borrowing) 하여 어드밴스 하면 뮤터블(mutable) 바로잉한 원본도 같이 어드밴스 되는 것입니다!

mutable borrowing을 두 번 이상 할 수 없기 때문에 바로잉하는 부분을 스코프로 감쌌습니다.

처음에는 이걸 몰랐고, Rust 공식 도큐먼트에도 관련된 언급이 없었기에 (제가 놓쳤을지도..?) 굉장히 헤맸었습니다. 사실 이터레이터 타입이 다양해 불편한 게 아니라 도큐먼트가 불친절한 게 진입 장벽을 높인 셈이었죠.

Rust Iterator Cheat Sheet. 러스트의 다양한 이터레이터 타입에 대한 치트 시트가 있습니다. 이터레이터가 너무 많아 뭘 써야할 지 혼란스러울 때 도움이 됩니다.

강 타입

Rust는 강 타입 언어입니다. 언어적으로 아주 빡빡하게 타입을 검사하는데, 이게 시스템의 신뢰도를 높이는 것은 맞지만 프로그래밍을 하는 시점에는 짜증 유발 요소로 작용합니다. 파서를 작성하며 굉장히 거슬렸던 한 부분이 레퍼런스 타입에 대한 문자 비교였습니다. 위에서 Peekable이 Chars 이터레이터를 뮤터블 바로잉했기 때문에 peek()이 반환하는 값은 char가 아니라 &char 타입이죠. 그래서 문자 비교도 레퍼런스 타입으로 해줘야 합니다.

좀 더 극단적인 예를 들어보자면, str 스트링 슬라이스와 String을 비교하기 위해 String을 레퍼런스타입으로 바꿔줘야 하는데, 만약 이터레이터 안에서 이를 하려면 이터레이터가 레퍼런스 타입으로 아이템을 관리하므로 이중 레퍼런스 타입의 비교를 해줘야 합니다.

여러 타입의 컨테이너를 쓰다보면 타입 맞춰주는 작업도 만만찮게 신경이 쓰입니다. 시스템의 신뢰도가 중요하지만 생산성을 낮추는 이런 언어적 요소가 득이 될지 독이 될지 현재 저의 수준으로는 판단하기가 쉽지 않습니다.

그 외 잡다한 생각들

적당히 마무리

사실 아직 Rust를 깊이 있게 사용한 것은 아니기에 이런 주저리를 쓰기엔 적절한 타이밍이 아닐지도 모르겠습니다. C++에 익숙해져 있기에 C++와 자꾸 비교하게 되는 것도 있고, 몸에 익지 않은 방식이라 뇌에서 걸리적거리는 것들도 있습니다. 아직 스레딩은 해보지 못했고, 다른 기능들도 직접 구현해 본 경험이 적으니 좀 더 알아가다 보면 Rust의 참 매력을 깨닫게 될지도 모르겠습니다. 두어 달 뒤에는 이 포스팅을 변론할 수 있기를 바라며 글을 마칩니다.

alleysark

alleysark

A computer game programmer who likes cats

comments powered by Disqus
rss facebook twitter github youtube mail spotify instagram linkedin google pinterest medium vimeo