클로저라는 훌륭한 도구와 영속 불변 자료 구조
훌륭한 목수는 연장 탓을 하지 않는다지
“훌륭한 목수는 연장 탓을 하지 않는다”고 한다. 이 말은, 훌륭한 목수는 연장에 좋고 나쁨에 영향 없이 훌륭한 결과를 낸다는 것으로 이해할 수 있다. 자칫 연장 관리에는 무심해도 된다고 해석할 수 있으나, 사실 훌륭한 목수는 평소에 연장 관리를 잘해서 좋은 결과를 내야지, 나쁜 결과가 나왔을 때 연장을 핑계 대며 변명하지 않는다는 뜻일 수도 있다. 훌륭한 결과를 위해 언제나 좋은 연장을 잘 관리하고 있어야 한다는 뜻이다.
세상에 훌륭한 지혜의 말들이 있고, 그걸 우리는 자기 마음 편한 대로 해석한다. 예를 들어, “천재는 1%의 영감과 99%의 노력으로 이루어진다”는 에디슨의 말을, ‘어차피 천재는 노력이 대부분이고 영감은 고작 1%에 불과하기에, 여러분도 노력하면 천재가 될 수 있다’고 해석하는 사람들이 있지만, 사실 에디슨 주장의 방점은 “1%의 영감”에 있었을지 모른다. 아무리 노력해도 그 1%의 영감이 없다면 천재가 될 수 없다는 말이다. 물이 99℃가 된다고 한들 1℃의 영감이 없으면 끓지 못한다. 무모한 노력하지 말자.
다시, 훌륭한 목수의 연장 얘기로 돌아와서 ‘연장 탓’을 하지 않는다는 걸, 내가 편한 대로 해석하는 오류를 범하지 말자. 즉, 늘 좋은 연장, 자기에게 잘 맞는 연장을 찾고 관리하고 상황에 맞는 도구를 잘 골라 쓸 수 있어야 한다는 거다. 훌륭한 목수가 연장 가리지 않고 잘 할 수 있을지는 몰라도, 연장 신경 쓰지 않고 열심히 한다고 해서 훌륭한 목수가 될 수 있는 것은 아니다. Bang!
- 훌륭한 목수 → 연장 가리지 않고 잘 할 수 있음 (어쩌면 참!)
- 연장 무관 열심히 노력함 → 훌륭한 목수가 됨 (WRONG!!!)
그리고 무엇보다 난 훌륭한 목수가 아니지 않나?! 도구라도 좋은 걸 써서 무능함을 만회하자!
구글에서 “훌륭한 목수는 연장탓”으로 검색해보니, 결과 첫 페이지에서 아래 두 글을 찾을 수 있었다.
첫 번째 글은, 비슷한 취지의 개발자의 글(도구의 대상은 조금 다르지만)이라서, 나만 이런 생각을 한 건 아니라는 걸 알 수 있었고, 두 번째 글은, 진짜 목수(!)의 글이라서 이 주장의 정통성(?)을 확보할 수 있었다.
아니면, 어쩌면 내가 훌륭한 목수가 아니라서, 좋은 연장에 집착하는 것일지도 모르겠다. 그리고 나 역시 내가 편한 대로 지혜의 말을 맘대로 해석하는 것일 수도 있고. 암튼, 훌륭한 목수가 아니면 어떠한가? 그저 평범한 목수로서도, 내가 원하는 결과를 효과적으로 만들어 낼 수 있는 그 모든 조치를 다 하는 것이 좋겠다.
앞선 얘기가 길었는데, 클로저는 참 여러모로 훌륭한 도구다. 이제껏 몇몇 장점을 얘기했는데, 오늘 언급할 장점은 “영속 불변 자료구조(persistent immutable data structures)”를 적극 활용한다는 점이다.
불변 자료 구조 — Immutable Data Structures
불변 자료 구조는 쉽게 이해할 수 있다. 예를 들어 Java의 String 객체는 불변이다. 변하는(mutable) 문자열 객체가 필요한 상황이라면 StringBuffer나 StringBuilder를 써서 처리하고 마지막에 String으로 받아와 쓴다. 불변인 String 객체의 장점은 해당 레퍼런스(포인터)를 가진 어떤 주체도 그 값이 영원히 바뀔 걱정 없이, 마음 놓고 믿고 쓸 수 있다는 뜻이다. 바뀌지 않으니 참조하는 순서(order)나 시점(time)을 고려할 필요가 없고, 그러니, 스레드 안전성(thread-safety) 문제도 고민할 필요가 없다는 뜻이다. 그리고 시점(time)에 따라 값이 변하는 것도 아니니, 실제값 내용은 확인할 필요 없이도, 레퍼런스의 포인터 값만 비교해서 같으면, 해당 값도 같다고 쉽고 빠르게 판단하는 잇점도 있다.
참조하는 포인터가 같다면, 그 값은 (나중에라도) 반드시 같다. (주의: 역은 성립 X)
문자열(String)과 마찬가지로 리스트, 맵, 트리, 셋, 맵등의 기본 자료 구조 역시 불변(immutable)이라면 여러모로 편리하게 사용할 수 있다. (사실 문자열도 논리적으로 문자의 리스트인 자료구조이다. 물리적으로 배열의 형태든 연결리스트의 형태든...) 문자열의 경우야 커봐야 얼마나 크겠는가? 레퍼런스 포인터가 64bit라고 할 때, 64bit(8byte)를 비교하는 것과 문자열 값을 일일이 비교하는 것과의 차이는 얼마 크지 않기에 그 효과도 별반 차이가 없지만, 1,000개의 엔트리가 있는 리스트를 비교한다면 그 차이가 드러날 것이다. 불변 리스트라면 일일이 그 1천 개의 엔트리를 비교할 필요 없이, 64비트 포인터가 같다면 전체 값이 같은 것이다. Period!
그러나 아쉬운 점은 그 값의 변화가 필요할 때, 원래 값을 바꿔서는 안 되므로, 값을 조금 바꾼 새로운 복사본을 만들어 내야 한다는 점이다. 이럴 때 가장 쉬운 방법은, 전체를 일일이 복사한 복사본을 만들면서 필요한 부분만 교체해서 만들어내는 방법인데, 이렇게 하면, 리스트에 단 하나의 요소를 추가하는데도 O(n)의 시간이 걸리는 데다, 메모리도 중복으로 낭비되는 문제가 있다.
영속 자료 구조 — Pesistent Data Structures
그런 불변 자료구조의 단점을 해결하고자 영속(persistent) 자료 구조를 쓴다. 영속(persistent)이라는 단어가, 흔히 메모리에 있는 자료구조를 디스크 같은 반영구적 매체에 저장하는 일을 지칭하는 경우가 많아 오해의 여지가 있지만, 여기서 말하는 영속은 DB나 Disk 등과는 무관한 어휘이다.


예를 들어, 고려말→조선초의 왕위 흐름을 역순 리스트로 표현해보자 (왜 역순인지는 잠시 후에 설명해 드리겠다. 일단은 최근 왕위와 그 선대 왕들의 리스트라고 하자.). 왼쪽 그림에는 고려의 마지막 왕 “왕요”가 마지막 엔트리로 있고, 그다음 태조 이성계가 조선의 초대 왕이 된다. 즉, 리스트A는 (이성계→왕요)를 표현하고 있다. 이제 리스트의 맨 앞에 세자로 책봉된 이방석을 추가해보자. 오른쪽 그림의 리스트B가 새로 예정된 왕위 계승도(이방석→이성계→왕요)이다.
이렇게 리스트의 맨 앞에 한 요소를 추가할 때는, 단지 새로운 이방석 엔트리를 만들고, 그다음(next) 포인터를 이성계를 가리키면 된다. O(1)에 새 요소를 추가했고, 리스트A를 참조하던 그 누구도 영향을 받지 않고 불변 자료 구조를 계속 쓸 수 있다. 리스트B도 불변으로 계속 쓸 수 있고, 리스트 A도 불변으로 계속 쓴다. 이렇게 원래 버전의 자료 구조를 유지한 채 약간 변형된 새 버전을 만들어내면서 대부분의 원래 자료를 공유하는 형태의 자료 구조를 영속 자료 구조(persistent data structures)라고 한다.
그렇게 원래 자료 구조를 대부분 유지하고 일부만 살짝 바꾸고 원래 버전을 유지하므로, 연산 비용 측면이나 메모리 사용에 있어서 효과적인, 불변(immutable) 자료구조로 쓸 수 있다.

마지막 그림은, 결국 이방원이 1차 왕자의 난을 일으켜 예정된 왕위계승도 리스트B를 바꿔서 리스트C(이방원→이방과→이성계→왕요)를 만들어 낸 것이다. 역사에 남아있듯 리스트B도 기록은 남아있지만(persistent), 실제 왕위는 리스트C로 흘러갔다.
만약 역사의 기록이 없다면, 이방석 세자책봉에 관련한 자료는 남아있지 않았을 것이다. 마찬가지로, 리스트B를 참조하는 레퍼런스가 아무것도 없다면, 리스트B 레퍼런스와 이방석 엔트리는 Garbage Collector에 의해 회수되었을 것이다.
그리고 또 하나, 왜 역순으로 표현했는가? 왜냐하면 리스트의 뒷부분에 엔트리를 추가하는 것은 얘기가 달라지기 때문이다. 왕요 엔트리의 다음(next)포인터가 nil로, 리스트의 마지막을 뜻하는데, 여기에 새 엔트리를 연결하는 순간, 그 앞의 리스트들의 레퍼런스는 모두 무용지물이 된다. 위 그림의 구조에서 리스트A, 리스트B, 리스트C모두에 새 엔트리가 추가되는 꼴이 된다. 원래의 레퍼런스들을 원래 값으로 유지한 채 뒷부분에 엔트리가 추가된 리스트를 만들려면, 다시 전체를 복사해서 만들어야하고, 이는 O(n)연산이 된다. 그래서, 영속 리스트를 잘 쓰려면, 어떤 연산을 주로하는지에 따라 입력 위치를 고려할 필요가 있다.
리스트만 언급했는데, 맵과 셋의 경우에도 마찬가지로 구현할 수 있다. 해쉬맵을 더 예로 들면, 클로저에서는 해쉬맵을 트리 구조로 처리해서 영속성을 유지하는데, 그러면 각종 연산 비용이 O(1)이 아니라, O(log N)이 된다. 그러나, 클로저에서 구현한 방식은 log의 밑수가 2가 아니라 32이다. 해쉬맵을 트리구조로 구현하면서 트리의 한 노드마다 32개 버켓을 관리해서, O(log32 N)이 된다. 사실상 O(log32 N)은 실질적으로 O(1)에 다름없으므로 해쉬맵의 경우도 효율적으로 영속성/불변성을 유지하며 쓸 수 있다. 대신 한 요소가 바뀔때 함께 위치한 31개 요소의 레퍼런스를 복사해야 할 것이다 (그러나 이 역시 뭐 사실상 O(1)이므로 문제 없다).
이상, 다소 생소한 내용일 수 있지만, 요점은 클로저는 불변 자료 구조를 적극 활용하며 장려한다는 점이다. 불변(immutable) 자료 구조를 쓰면서도, 이를 영속(persistent) 자료 구조 형태로 쓰기에 비용의 낭비가 크지도 않다. 이로써 프로그래머는 시점과 상태에 큰 고민을 덜고 편하게 개발할 수 있다. 프로그래머 머리의 CPU 사용량이 대폭 줄어서, 정작 중요한 일에 머리 회전을 할애할 수 있다.
저의 부족한 설명으로 여러 아쉬움이 남으셨다면 아래 제대로 된 전문적인 자료를 참고해주시기 바란다.
더 읽을 자료
클로저는 훌륭한 도구
클로저는 영속 불변 자료 구조를 비롯해 갖가지 개발에 유용한 도구를 제공한다.
사실, 다른 프로그래밍 언어도, 이런 자료 구조의 장점을 받아들여서 많이 쓰고 있기는하다. 그러나, 선택적으로 가져다 쓸 수 있는 것과 기본으로 널리 쓰는 것은 조금 차이가 있다. 그 언어 커뮤니티 개발자 모두가 그런 “좋은” 자료구조를 쓰는 게 기본이 되어버린다.
어떤가? 이를 써서 함께 훌륭한 목수가 되어 보지 않겠는가? 일단 함께 훌륭한 목수가 되고 나서 함께 이렇게 얘기하는 거드름을 피워보자.
클로저가 중요한 게 아니야, 니 능력이 중요한 거지.
마무리 여담
최근 종종 만나는 개발자 K 씨로 부터, 제 미디엄 글에 대한 솔직한 피드백을 받았다. 몇몇 제 글이 실제 경험한 것보다 부풀려져 허세가 느껴진다는 고마운 의견이었다. 다시 읽어보니 스스로 생각해 봐도 그런 점이 적지 않다. 아마, 이번에 적은 영속 자료 구조도, 아직 많은 부분을 어설피 알고 적는, 거친 정리일 것이다. 부디 그 점을 참고하시어, 간략히 훑어 읽기만 하시고 혹시나 더 관심이 가신다면 더 제대로 된 자료를 읽어주시길 바란다.
무언가를 제대로 알고 쓰려고 미룬다면, 결국 쓰지 않게 되더라는 판단에 조금 설익은 글을 쓰게되는 편인 것 같다. 이러다보면 조금씩 더 익어가는 것이지, 그냥 홀로 산속에 들어가 내공을 닦는 재야고수가 되긴 틀려 먹은 성정인가 보다.
클로저를 쓰다보니 참 좋은 도구를 만났다는 고마운 마음에 함께 누리려는 불필요한 이타심과, 자랑하고픈 심정이 있음을 인정한다. 부디 너그럽게 봐주시길. 뭐 사실, 클로저가 뛰어난거고, 클로저를 만든 리치 히키가 대단한거지, 그걸 쓴다고 제가 대단해지는 것은 아니니까 말이다.