TL;DR

  • ruby-enum의 상속 열거형 병합 기능은 정확했지만, 하위 클래스의 조회를 상위 클래스보다 약 5배 느리게 만들었음.
  • Copilot CLI에 한 문장으로 요청해 벤치마크 스크립트를 작성하면서 성능 회귀를 발견하고, 재실행 가능한 rake benchmark:inheritance 작업으로 저장함.
  • 병합된 해시를 메모이제이션하고 define으로 새 항목을 추가할 때만 캐시를 무효화해 상속 깊이와 무관하게 조회 성능을 맞춤.
  • Ruby::Enum의 상수 접근은 일반 루비 상수와 같은 속도이며, 해시 기반 조회는 직접적인 Hash#[]보다 3~5배, Case 매처는 기본 case 문보다 50~100배 느림.
  • 테스트가 정확성만 확인하면 성능 회귀를 놓치기 쉽지만, 에이전트로 벤치마크를 작성하는 비용이 거의 사라짐.

상속 지원이 불러온 성능 회귀

  • 이전 글에서는 관리 중인 젬 ruby-enum의 버그 네 가지를 다뤘으며, 모두 클래스 수준 인스턴스 변수가 하위 클래스로 상속되지 않는 데서 비롯됨.
  • 세 번째 수정인 #59는 keys, key?, value?, key, value, to_h, parse, each가 상위 클래스를 거슬러 올라가 부모 열거형을 병합하도록 변경함. 하위 클래스가 모든 조상 클래스의 정의를 확인할 수 있도록 한 올바른 수정이며 테스트를 모두 통과해 배포됐지만, 하위 클래스에서 해당 메서드가 모두 대략 5배 느려짐.
  • _enum_hash는 호출할 때마다 상위 클래스의 해시를 가져와 자체 해시와 병합하며, 결과를 캐시하지 않음. 한 단계 하위 클래스에서 .value를 호출하면 새 해시를 만들고, 상위 클래스도 같은 작업을 반복함.
  • 테스트는 정확성을 확인할 뿐 속도를 확인하지 않으며, 이 변경은 정확성에 문제가 없어 테스트에서 성능 저하가 드러나지 않음.

벤치마크로 성능 문제 발견

  • README의 새 기능 옆에 ‘벤치마크’ 섹션을 어떻게 작성할지 궁금해 Copilot CLI에 상속 깊이에 따른 조회 성능을 비교하는 벤치마크 스크립트를 요청함. 프롬프트 하나와 약 1분 만에 작동하는 스크립트를 얻음.
  • 벤치마크는 기본 클래스 Colors, 한 단계 하위 클래스 SubColors, 두 단계 하위 클래스 SubSubColors를 만들고, 각 클래스에서 .value(:RED)를 호출해 비교함.
  • 첫 실행에서 한 단계 하위 클래스의 .value 호출은 기본 클래스보다 대략 5배 느렸고, 두 단계 하위 클래스는 더 느렸음.
  • #59를 병합하기 전에 이 벤치마크를 작성하지 않은 이유는 일회성 성능 확인에 쓰는 임시 스크립트 작성이 그만한 가치가 있는 것보다 번거롭게 느껴졌기 때문임. 이제는 에이전트에게 한 문장으로 요청할 수 있으며, 스크립트는 누구나 다시 실행할 수 있는 rake benchmark:inheritance 작업으로 저장됨.

캐시 적용과 조회 성능 개선

  • 수정은 병합된 해시를 메모이제이션하고, define이 새 항목을 추가할 때만 캐시를 무효화하는 방식임.
  • 수정 후 #60에서 같은 벤치마크를 다시 실행한 결과는 기본 클래스 0.0627, 한 단계 하위 클래스 0.0608, 두 단계 하위 클래스 0.0606임.
  • 이제 상속 깊이와 관계없이 하위 클래스의 조회 성능은 기본 클래스와 구별할 수 없는 수준임.

README에 공개한 성능 비용

  • 젬의 오버헤드에 관한 README 설명을 작성하기 전에 근거 없이 ‘미미하다’고 쓰지 않기 위해, Copilot CLI로 #61 벤치마크를 추가 작성함. 이 벤치마크는 기본 Ruby::Enum 연산과 일반 루비 연산을 비교함.
  • 상수 접근은 비용이 없으며, Colors::RED는 일반 루비 상수 접근과 정확히 같은 속도임.
  • .value와 .key? 같은 해시 기반 조회는 Hash#[]를 직접 호출하는 것보다 3~5배 느림. 이는 Ruby::Enum의 추가 메서드 디스패치와 객체 래핑에 따른 비용임. 알아둘 만한 수치지만 젬 사용을 피할 정도는 아님.
  • 전수 매칭을 수행하는 Ruby::Enum::Case는 네이티브 case 문보다 50~100배 느림. 이 수치와 다른 성능 결과는 README의 전용 성능 섹션에 공개돼 있어 질문하거나 추측할 필요가 없음.

벤치마크 작성 비용의 변화

  • 테스트가 정확성만 확인하면 성능 회귀는 도입하기도 쉽고 놓치기도 쉬움.
  • 달라진 점은 성능 회귀를 찾는 데 드는 비용임. 예전에는 ‘그냥 getter 메서드’ 변경을 확인하기 위한 벤치마크가 금요일 오후에 생략할 법한 작업이었지만, 이제 벤치마크 작성 비용은 거의 없음.