소프트웨어 엔지니어에게 프로덕션 코드의 핵심 부분을 리뷰해 달라고 요청하면, 십중팔구 리팩토링이 필요한 부분을 여럿 지적하게 됩니다. 그렇다면 왜 이렇게나 많은 저품질·취약·오해석된 코드가 프로덕션 환경에서 계속 돌아가고 있을까요?
답은 간단합니다. 엔지니어들이 해당 코드를 수정하는 것을 두려워하기 때문입니다. 리팩토링 작업은 문제로 식별되어 백로그에 추가되곤 하지만, 정작 현재 스프린트에 반영되는 일은 드뭅니다.
여기에는 여러 가지 이유가 있습니다. 코드가 몇 년 전 팀을 떠난 엔지니어가 작성한 것이라 아무도 완전히 이해하지 못하는 경우도 있고, 해당 기능이 비즈니스에 너무나 중요해서 누구도 잠재적인 서비스 장애나 수익 손실의 책임을 떠안고 싶어 하지 않는 경우도 있습니다.
이 글에서는 Scientist를 활용해 중요한 Ruby 프로덕션 코드를 자신 있게 마이그레이션하고, 리팩토링하고, 변경하는 방법을 알아보겠습니다.
먼저 이런 의문이 들 수 있습니다. 어차피 테스트가 있는데 굳이 별도 도구가 필요할까요?
Rails 테스트가 바로 그 역할을 하는 것 아닌가?
맞기도 하고, 아니기도 합니다. 배포 전에 코드 변경 사항에 대한 완전한 확신을 얻는 것은 생각보다 어렵습니다. 단위 테스트와 시스템 테스트가 모두 통과했으니 배포해도 된다는 뜻일까요?
현실적으로 실제 운영 환경, 즉 프로덕션을 대체할 만한 것은 없습니다. 데이터 품질이 나쁘거나 테스트가 누락되었다면 어떨까요? 새 코드가 프로덕션 처리량을 감당할 만큼 충분히 잘 동작할지 어떻게 알 수 있을까요?
공개 서비스를 운영하는 팀들은 종종 '버그워드 호환성(bugwards compatibility)' 문제를 겪기도 합니다. 버그가 오랫동안 프로덕션에 남아 있으면 클라이언트 측이 그 잘못된 동작에 의존하는 방식으로 코드를 작성해 버리는 것이죠. 고객은 소프트웨어를 개발자가 예상하지 못한 방식으로 사용하기 일쑤입니다.
Scientist로 Ruby·Rails의 프로덕션 변경 사항 관찰하기
변경 사항에 대한 확신을 얻기 가장 좋은 곳이 프로덕션이라면, 코드가 실제 운영 환경에서 어떻게 동작하는지 관찰해 보는 것이 자연스러운 발상입니다. '프로덕션에서 테스트한다'는 개념이 전통적인 소프트웨어 엔지니어링 관행과 상충하기 때문에 처음에는 다소 위험하게 들릴 수 있습니다.
하지만 다행히 Ruby와 Rails에서는 Scientist 젬을 사용하면 이 과정을 쉽고 안전하게 수행할 수 있습니다. Scientist라는 이름은 주어진 가설을 검증하기 위해 실험을 설계하는 과학적 방법론에서 따온 것입니다. 여기서 우리의 가설은 '새 코드가 제 역할을 해낸다'는 것입니다.
이 접근 방식을 안전하게 쓸 수 있는 이유는, 실험이 기존 코드의 결과물을 계속 사용하기 때문입니다. 새 코드는 정확성과 성능 비교를 위해 관찰 목적으로만 평가됩니다. 실제 운영 데이터와 파라미터로 성능을 측정하기 때문에 앞서 언급한 테스트 커버리지 문제도 자연스럽게 완화됩니다. 실험은 일반적으로 프로덕션에 미치는 영향을 최소화하기 위해 요청 중 일정 비율만 샘플링하지만, 원한다면 모든 요청을 대상으로 평가할 수도 있습니다.
이제 Scientist가 '추상화 기반 분기(Branch by Abstraction)' 방식으로 어떻게 동작하는지 간단히 살펴보겠습니다.
Ruby Scientist의 추상화 기반 분기 패턴
Scientist의 접근 방식은 Martin Fowler가 '대규모 변경을 점진적으로 수행하는 방법'이라고 설명한 Branch by Abstraction 패턴에서 출발합니다.
수정 대상 코드를 고립시키기 위해 추상화 계층을 도입합니다. 이 계층이 어떤 구현체를 사용할지 결정하므로, 실험 과정이 시스템의 나머지 부분에는 투명하게 유지됩니다. 이 기법은 코드 경로를 결정하는 피처 플래그(feature flag) 사용과 맥락을 같이합니다.
GitHub에서 탄생한 Scientist 젬은 '실험(Experiment)'이라는 개념으로 이 패턴을 구현합니다. 기존 코드는 컨트롤(control)이라 불리고, 새 구현은 후보(candidate)라고 불립니다. 두 코드 경로는 무작위 순서로 실행되지만, 클라이언트에는 오직 컨트롤의 결과만 반환됩니다.
Scientist로 Ruby 서비스 리팩토링하기
주어진 숫자의 최대 소인수를 반환하는 Ruby 서비스를 예로 들어 보겠습니다. 후보군을 줄이는 최적화 기법을 적용해 서비스 속도를 개선할 수 있다고 가정합니다.
다만 서비스 소유자는 버그가 유입되지 않았는지 확인하고 싶고, 성능 향상 폭도 관찰하고 싶어 합니다. 다음과 같은 코드를 추가하고 클라이언트가 이 메서드를 호출하도록 수정합니다:
이 시점에서는 use(컨트롤) 표현식만 실행됩니다. 실험에 의미를 부여하려면 커스텀 Experiment 클래스를 정의해 실험을 활성화(아래 예시에서는 100% 실행)하고 결과를 발행(publish)해야 합니다(여기서는 로깅). Scientist는 훌륭한 데이터를 생성하지만 기본값으로는 아무 작업도 수행하지 않습니다. 그 후속 처리는 개발자의 몫입니다.
실험 결과가 로그로 기록되면, 이 피드백을 바탕으로 시간을 두고 개선을 진행할 수 있습니다. 새 코드가 요구 사항을 충족하고 신뢰도가 충분히 올라가면, science 코드를 새 구현으로의 위임(delegate) 한 줄로 교체하는 것만으로 전환(cutover)을 완료할 수 있습니다.
LabTech로 Rails에서 Scientist 실험 간편하게 관리하기
Rails 애플리케이션에서는 LabTech 젬을 사용하면 Scientist 설정과 결과 처리를 손쉽게 구성할 수 있습니다.
AppSignal을 사용하는 애플리케이션이라면 Appsignal.instrument 커스텀 인스트루멘테이션 헬퍼로 Scientist 이벤트 소요 시간을 추적할 수 있습니다. 실험의 각 코드 블록을 이 헬퍼로 감싸면 성능 타임라인에 이벤트가 표시됩니다.
다시 LabTech 이야기로 돌아가면, 아래 웹 페이지는 인수분해할 숫자를 입력받는 단순한 화면입니다.

콘솔에 접근할 수 있다면 시작은 간단합니다. 먼저 Gemfile에 LabTech 젬을 추가하고 bundle install을 실행합니다.
결과와 실험 설정을 저장할 테이블이 필요하므로 데이터베이스 마이그레이션을 실행합니다.
추상화 계층은 동일하며, LabTech 모듈을 사용하도록 바꾸면 됩니다. 전체 코드는 GitHub에서 확인할 수 있습니다.
이 시점에서는 실험이 비활성화 상태이므로, 콘솔을 통해 모든 요청 또는 일정 비율의 요청에 대해 실험을 활성화합니다.
이제 테스트를 실행하면 실험이 함께 평가됩니다. 결과를 텍스트로 확인하려면 Rails 콘솔에서 다음 명령어 중 하나를 사용하면 됩니다.
몇 차례의 성공적인 실행과 일부러 만든 에러 하나를 거친 후, 결과 요약은 대략 다음과 같습니다. 성공과 실패에 대한 개요와 함께 성능 차이를 보여주는 ASCII 차트가 포함됩니다.
Blazer 젬을 활용하면 결과를 손쉽게 분석할 수 있습니다. 설치가 간단하고 테이블을 대상으로 SQL 쿼리를 실행할 수 있습니다. 아래 쿼리는 후보 구현이 원본보다 훨씬 빠르다는 것을 보여줍니다.

앞선 소인수분해 서비스 예제에서 개선된 구현의 속도 향상은 검토할 후보 인수를 줄여주는 휴리스틱 덕분입니다. 큰 수를 다루면서 소인수를 찾으면, 목표 숫자를 해당 인수로 나눈 값까지만 탐색하면 됩니다. 새 코드 경로에는 이를 위한 단 한 줄의 문장만 추가됩니다.
실행 시간의 감소 역시 Blazer로 LabTech 테이블을 조회하면 확인할 수 있습니다.

Scientist의 활용 사례와 한계
Scientist가 가장 빛을 발하는 경우는 검색, 계산 등 부수 효과(side effect)가 없는 코드입니다. 트랜잭션 갱신이나 이메일 발송 같은 외부 연동이 포함된 코드는 기능이 두 번 실행되기 때문에(구·신 구현 모두) 이 모델에 깔끔하게 맞지 않습니다.
이는 결코 가볍지 않은 제약으로, 상당수 활용 사례를 배제합니다. 그럼에도 실험이 프로젝트의 성패를 좌우한다면 몇 가지 우회 방법이 있습니다. 부수 효과가 실질적으로 중요한지, 중복 실행이 문제가 되는지 먼저 판단해 보세요. 예컨대 평가 과정에서 이메일이 두 번 발송되더라도 문제되지 않는 상황도 있을 수 있습니다. 또 다른 선택지는 새 코드가 결과를 산출하되 저장하지 않는 것입니다. 이 경우 의미 있는 성능 비교는 불가능하지만, 정확성 검증은 가능합니다.
그 외 한계는 Scientist가 반환 값에 초점을 맞춘 데서 비롯됩니다. 응답에 타임스탬프가 포함되거나 특정 요인이 달라지는 등 유효한 결과라도 시점에 따라 차이가 날 수 있습니다. 이런 경우 실험에 커스텀 비교 로직을 작성해 단순 문자열 비교를 넘어선 정확성 검증을 수행할 수 있습니다.
마지막으로, LabTech의 한계는 글 작성 시점 기준 아직 Rails 7로 포팅되지 않았다는 점입니다.
Rails에서 효과적인 Scientist 실험을 위한 모범 사례
실험을 구현할 때 다음 사항들을 고려해 보세요:
- Rails 프로젝트에서 Scientist는 이니셜라이저에 직접 설정하거나 LabTech 같은 래퍼 젬으로 구성할 수 있습니다. 대부분의 Rails 앱은 이미 데이터베이스를 갖추고 있으므로, LabTech는 ActiveRecord를 활용해 결과를 저장합니다.
- 개발 및 테스트 환경의 속도 저하를 피하려면 실험은 스테이징과 프로덕션 환경에서만 활성화하세요.
- 프로덕션에 미칠 잠재적 영향을 최소화하려면 요청의 일정 비율에 대해서만 실험을 실행하세요. LabTech는 실험 활성화 시 선택적 파라미터로 이를 기본 지원합니다(초기 상태는 비활성화). 순수 Scientist를 사용한다면 실험의
enabled?메서드에 이 로직을 쉽게 구현할 수 있습니다. - 일부 로직은 리소스를 많이 소모하므로 낮은 샘플링 비율부터 시작하는 것이 좋습니다. 결과에 대한 신뢰가 쌓이면 점진적으로 평가 비율을 늘려가세요.
- 컨텍스트 속성을 추가하면 결과를 최대한 활용할 수 있습니다. 실험 컨텍스트를 Symbol 키를 사용하는 Hash로 설정하면, 발행되는 결과에서 해당 데이터를 확인할 수 있습니다. 예:
마무리: Scientist로 Ruby 앱을 관찰하고 모니터링하기
이 글에서는 Scientist 젬을 사용해 프로덕션 환경의 Ruby 코드를 변경, 마이그레이션, 리팩토링하는 방법을 살펴봤습니다.
Branch by Abstraction 패턴에 뿌리를 둔 Scientist의 철학을 확인한 뒤 실제 리팩토링 과정을 살펴보았고, 이어서 LabTech가 결과 수집과 실험 구성을 어떻게 도와주는지 확인했습니다.
또한 Scientist의 몇 가지 한계를 짚어본 후, 효과적인 실험을 위한 모범 사례까지 정리했습니다.
시스템에서 벌어지는 일을 반드시 관찰하고 모니터링하세요. Scientist를 개발 프로세스에 통합하면 중요한 Ruby 코드 변경을 훨씬 더 큰 자신감으로 수행할 수 있습니다.
즐거운 코딩 되세요!
P.S. Ruby Magic 글이 발행되는 대로 읽고 싶으시다면 Ruby Magic 뉴스레터를 구독하고 어떤 글도 놓치지 마세요!
Darren Broemmer
Darren은 글로 영감을 전하고 복잡한 것을 쉽게 풀어내는 일을 즐깁니다. 과학과 물리학, 그리고 이 둘을 이해하는 데 필요한 만큼의 수학에 관심이 많으며, 고품질 콘텐츠와 기술 솔루션을 만들고 가끔 트위터에서 관련 이야기를 공유합니다.