Gleam의 비교적 장황해 보이는 decode 방식이 왜 일관성과 명시성 면에서 매력적인지, 실제 예제를 통해 살펴보는 글
2025년 4월 7일
Gleam이 Gleam 바깥 세계와 데이터를 인코딩/디코딩하는 방식(decode을 중심으로)을 처음 접했을 때, 나(그리고 많은 Gleam 초보자들)는 이렇게 현대적이고 잘 설계된 언어가 여전히 비교적 장황하고 수동에 가까운[1] 방식을 쓰는 게 좀 이상하게 느껴졌다. 그런데 이제는, 나는 이 접근이 각종 난해한 축약, 전용 문법, 매크로, 마법 같은 것들을 섞어 쓰는 방식보다 훨씬 좋다는 걸 깨달았다.
재미 삼아(그리고 공부할 겸) 진행 중인 작은 사이드 프로젝트에서, 어떤 “재미있는” 데이터 구조를 마주쳤고 그 안에서 필드 몇 개를 뽑아내고 싶었다. 처음에는 기본적인 딕셔너리 같은 구조라고 생각했는데, 곧 좀 더 꼬여 있다는 걸 알게 되었다.
약간의 뇌 싸움을 거친 끝에, 대충 아래처럼 별로 읽기 좋지 않은 코드 조각을 작성해, 겉보기에는 원하는 동작을 하게 만들었다. 그때는 크게 신경 쓰지 않았다. 일단 돌아가는 걸 만들었다는 사실에 대체로 만족한 상태였다.
gleamfn decoder() { let file_decoder = { use name <- decode.field("name", decode.string) decode.success(name) } use length <- decode.field("length", decode.int) let idcs = list.range(0, length - 1) |> list.map(fn(i) { int.to_string(i) }) let assert [first, ..rest] = idcs let initial = fn(acc: List(String)) -> decode.Decoder(List(String)) { use next <- decode.field(first, file) decode.success([next, ..acc]) } let final = list.fold(rest, initial, fn(acc_fn, index) -> fn(List(String)) -> decode.Decoder(List(String)) { fn(acc: List(String)) { use next <- decode.field(index, file) acc_fn([next, ..acc]) } }) final([]) }
그런데 하루쯤 지나고 나서, 이 코드가 얼마나 읽기 어려운지 다시 살펴보고 싶어졌다. 구현을 비교적 최근에 작성한 상태였는데도, 한눈에 봐서는 이 코드가 어떻게 동작하는지 완전히 이해하고 쪼개 보는 데 애를 먹었다. 좋은 출발점(맞다, 좋은 출발점이다)은 우선 들어오는 데이터부터 제대로 이해하는 일이다. 한 번 보자.
json{ "length": 3, "0": {"name": "file1.txt"}, "1": {"name": "file2.txt"}, "2": {"name": "file3.txt"} }
객체의 엔트리들은 동적이며, length 값에 의존한다. 여기서 깨달음이 왔다. 디코더를 그럴듯하게 만드는 데 그렇게 고생한 이유는, 들어오는 데이터 구조가 잘해야 “재미있는” 수준이고, 사실은 내 취향과는 전혀 맞지 않는 형태였기 때문이다.
또 하나 깨달은 점은, Rust를 약 6–7년 동안 업무에서 써 왔음에도, 이런 데이터 구조에 대한 디코더를 구현해야 한다면 잠깐이나마 질색부터 했을 것 같다는 사실이다(물론 곧 “그렇게 어렵진 않지, serde가 훌륭하니까!” 하고 마음을 고쳐먹긴 하겠지만). 그런데 Gleam의 decode를 사용할 때는, 들어오는 데이터가 얼마나 불편한 형태인지 완전히 자각하지도 못한 채 구현을 끝냈다. 그저 한 걸음씩 차근차근 밟아 나갔을 뿐이다.
length를 디코드한다.이제 이 해결책을 다시 살펴보면서, 패닉을 유발할 수 있기 때문에 웬만하면 항상 피해야 하는 assert를 없애고 싶어졌다.
gleamfn decoder() { let file_decoder = { use name <- decode.field("name", decode.string) decode.success(name) } use length <- decode.field("length", decode.int) let indices = list.range(0, length - 1) |> list.map(int.to_string) let files = list.fold(indices, decode.success, fn(acc_fn, index) { fn(acc: List(String)) { use next <- decode.field(index, file_decoder) acc_fn([next, ..acc]) } }) files([]) }
결과물도 여전히 읽기 쉽고 바로 이해되는 코드는 아닐 수 있다(특히 Gleam이나 함수형 프로그래밍, 혹은 use의 의미론에 익숙하지 않다면 더더욱 그렇다). 하지만 나는 이 코드가 단순한 구조를 디코드할 때와 비교했을 때 얼마나 일관되게 느껴지는지가 정말 마음에 들었다. 특별한 전용 디코드 구현이 필요하지 않다. 그저 기존의 조립품들을 조금 다른 방식으로 조합할 뿐이다!
Gleam의 decode는, 다른 언어의 디코더나 역직렬화기들에 비해 덜 간결하고 덜 편리해 보일 수 있다. 하지만 그 대신 얻는 것은 다음과 같다.
구현 세부 사항이나 Gleam 전반에 대해 더 알고 싶다면, 직접 파고들어 보기를 강력히 추천한다! Gleam은 언어 자체도, 에코시스템도, 커뮤니티도 정말 훌륭하다. 다만, 예전에 쓰던 주력 언어로 다시 돌아가면 다음과 같은 증상이 나타날 수 있으니 주의하자(이에 국한되지 않는다):
use가 그립다…)