Metadata
# 클랭커와 데이터 경합
방금 [콰미나(Quamina)](https://github.com/timbray/quamina)의 심각한 데이터 경합을 수정하는 PR을 병합했다. 기술적으로 어려울 일은 아니었지만, 작업하다가 에를랭(Erlang), 단위 테스트의 함정, LLM이 생성한 PR을 계속 받아야 할지 여부, 나이 들어가며 프로그래밍하는 문제로 곁길을 샜다.
마지막 문제부터 이야기해 보자.
클랭커 PR에는 “아니요(No)” · 2026년 말인 지금, 우리는 다음 사실을 알고 있다.[¶](https://www.tbray.org/ongoing/When/202x/2026/09/01/Un-Racing#p-1)
1. 클로드(Claude)와 경쟁 모델들은 꽤 훌륭한 코드를 작성할 수 있다. 특히 작업 범위를 좁힐 수 있을 때 그렇다.
2. 취약점을 탐지하는 능력도 좋은 편이며, 때로는 탁월하다.
3. 그 바탕이 되는 생성형 AI(GenAI) 기술은 적어도 환경 영향과 지식 재산권 분야에서 심각한 부정적 외부효과를 일으킨다.
4. 이 기술을 사용하면 내가 경멸하고 두려워하는 사람들이 부와 권력을 얻을 수도 있다.
5. 내가 아끼고 존중하는 사람 대부분은 생성형 AI의 큰 흐름을 보며 실제로 괴로워하고 있다.
나는 개인적으로 클랭커(clanker)를 본격적으로 활용한 적이 없으며, 당분간은 계속 그러지 않을 생각이다. 클로드가 작성한 PR을 받아 _오기는 했다_. 하지만 식별할 수 있는 한 이제부터는 받지 않겠다. 여기에는 분명한 대가가 따른다. 그런 PR 중 일부는 성능을 크게 높였다. 게다가 내가 직접 하는 것보다 훨씬 빠르게 완성됐다.
하지만 결과가 늘 좋은 것만은 아니다. 솔직히 말해, 비결정적 유한 오토마톤의 엡실론 폐포(epsilon closure)를 계산하는 클랭커의 [코드](https://github.com/timbray/quamina/blob/main/epsi_closure.go)와 [테스트](https://github.com/timbray/quamina/blob/main/epsi_closure_test.go) 약 500줄을 나는 제대로 이해하지 못한다. 얼마나 이해하지 못했는지, 이를 다시 검토하고 이해한 뒤 운이 좋으면 단순화하려고 [이슈까지 작성했다](https://github.com/timbray/quamina/issues/580).
흥미롭게도 현재의 엡실론 폐포 코드는 겉보기에는 합리적인 PR이 연달아 들어오면서 만들어졌다. 나는 각 PR을 검토했고 대부분에 수정을 요청했다. 하지만 큰 틀에서 보면 내 검토는 효과가 없었다. 결국 내가 이해하지도, 최종적으로 신뢰하지도 못하는 거대한 코드 덩어리만 남았기 때문이다. 이런 결과가 얼마나 흔할지 궁금하다.
새 정책에도 예외는 있다. 바로 취약점 보고서다. 출처가 마음에 들지 않는다는 이유로 보고서를 무시하고 콰미나 사용자에게 피해를 주는 것은 무책임하다고 생각한다. 누군가 [클로드로 스캔을 실행해](https://github.com/timbray/quamina/issues/437) 심각도가 제각각인 문제 14개를 찾아냈다. 나는 각각에 관한 이슈를 만들었고, 모두 조만간 해결하기를 바란다.
프루너 경합(pruner race) · 클랭커가 찾아낸 가장 골치 아픈 문제는 콰미나의 [프루너(pruner)](https://github.com/timbray/quamina/blob/main/pruner.go)라는 부분에 있던 데이터 경합이었다. 대부분의 사용자는 이 기능을 쓰지 않는다. 그래서 한 고루틴에서 읽기 작업 수천 회를 실행하고, 다른 고루틴에서는 같은 횟수의 갱신 작업을 수행하는 단위 테스트를 재빨리 만들었다. 아니나 다를까, 첫 실행부터 데이터 경합 경고를 무더기로 쏟아내며 즉시 폭발했다.[¶](https://www.tbray.org/ongoing/When/202x/2026/09/01/Un-Racing#p-3)
문제는 복잡하지 않았고 첫 번째 수정안도 잘 작동했다. 단위 테스트는 아무 문제 없이 실행됐다. 다만 엄청나게 느렸다. 나는 방금 추가한 뮤텍스 주변에서 경합이 발생하는지 찾아보기 시작했다. 당시에는 합리적인 판단이라고 생각했다. 프로파일러는 콰미나의 핵심 매칭 메서드에서 속도가 떨어진다고 알려주는 듯했다. 하지만 전혀 말이 되지 않는다고 여겨 Go의 동기화 기본 요소를 점점 더 깊이 파고들었다. 그래도 쓸 만한 단서는 찾지 못했다.
동시성 버그는 유난히 짜증스럽다. 게다가 나는 반쯤 은퇴한 상태로 재미 삼아 이 일을 한다. 그래서 몇 주 동안 손을 놓고 외면했다.
함수형 프로그래밍 사랑 · 이 우울한 이야기에도 밝은 부분은 있었다. 처음에는 뮤텍스로 보호되는 필드 여러 개를 가진 통계 수집 타입이 문제라고 잘못 생각했다. 그래서 신이 나서 낄낄거리며, 그 필드들을 Go 채널 메시지로 갱신하는 [에를랭식 누산기(Erlang-flavored accumulator)](https://github.com/timbray/quamina/blob/main/pruner_stats.go)로 바꿨다. 언제나 마음을 훈훈하게 해주는 프로그래밍 방식이다.[¶](https://www.tbray.org/ongoing/When/202x/2026/09/01/Un-Racing#p-4)
성능에는 아무런 차이가 없었지만, 코드는 더 짧고 보기 좋아졌다.
_[업데이트: 유용성에는 지장이 없지만 미묘한 버그가 있다. 이슈를 만들었으며 수정할 예정이다.]_
단위 테스트는 좋다. 그렇지 않을 때만 빼고 · 짜증을 가라앉히고 문제로 돌아온 뒤에는 먼저 단위 테스트부터 살펴봤다. 이런. 읽기 전용 스레드가 갱신 스레드보다 20배 빠르게 실행되고 있었다. 놀랄 일은 아니다. 두 스레드가 같은 횟수의 작업을 수행하도록 했기 때문에 읽기 스레드는 순식간에 작업을 끝냈고, 갱신 스레드만 계속 맹렬하게 돌아갔다. 여기서 자세히 설명할 필요는 없는 이유로, 이 벤치마크 시나리오에서는 통계를 읽는 작업 없이 무거운 추가와 삭제 작업만 이어지면 엄청나게 크고 끔찍하게 복잡한 비결정적 유한 오토마톤(NFA)이 만들어진다. 그러면 매칭 속도가 느려진다. 프로파일러는 바로 그 사실을 알려주려 했지만, 나는 내가 더 잘 안다고 생각했다.[¶](https://www.tbray.org/ongoing/When/202x/2026/09/01/Un-Racing#p-5)
그래서 다른 스레드가 끝날 때까지 읽기 스레드도 계속 실행되게 했다. 짜잔. 경합은 여전히 해결된 상태였고, 모든 작업이 콰미나다운 평소 속도로 실행됐다. 다시 말해 아주 빨랐다.
(그렇다고 에를랭식으로 바꾼 통계 패키지를 없애지는 않았다. 너무 마음에 들었다.)
나는 평소 단위 테스트를 만들 때 어느 정도 엉성하고 즉흥적인 욜로(YOLO)식 파워 코딩을 해도 [대체로 관대하다](https://www.tbray.org/ongoing/When/202x/2021/05/15/Testing-in-2021#p-8). 테스트는 많을수록 좋으니까! 지금도 그런 생각은 변하지 않았다. 하지만 이번 경험은 고통스러웠고 앞으로는 피하고 싶다. 여기서 얻을 교훈이 있을까? 아마 두 가지일 것이다. 첫째, 프로파일러가 알려주는 사실을 믿어라! 둘째, 단위 테스트 결과를 이해하기 어렵다면 조금 더 파고들어 테스트가 정말 생각한 대로 작동하는지 확인하라.
백발의 프로그래머 · 내 소프트웨어에 화가 날 때조차도, 여전히 이 일을 즐길 수 있다는 사실에 감사한다. 은퇴 후 내 주된 목표는 두뇌가 계속 움직이게 하는 것인데, 프로그래밍이 그 목표에 도움이 된다고 생각한다. 또 누가 알겠는가. 이 소프트웨어가 어딘가의 누군가에게 도움이 될지도 모른다.[¶](https://www.tbray.org/ongoing/When/202x/2026/09/01/Un-Racing#p-6)
무엇보다 좋은 점은, 돈을 받고 하는 일이 아니라면 내키지 않을 때 언제든 몇 주씩 쉴 수 있다는 것이다.
* * *
* * *