오래된 Rails 애플리케이션 유지보수에 대한 실용적인 교훈: 10년 된 대규모 웹 서비스와 함께한 5년의 기록
들어가며
신기술과 최신 프레임워크는 흥미롭지만, 실제 현업에서는 오래되고 복잡한 대규모 애플리케이션을 유지보수하는 경우가 많아요. Harvest의 약 20년 된 Rails 앱 사례는 이러한 현실을 잘 보여줘요.
이번 글에서는 Julia López의 “Beyond the Hype: Practical lessons in Long-Term Rails maintenance” 강연에서 얻은 실용적인 교훈과 제가 10년 된 대규모 웹 서비스를 5년간 함께 개발해온 경험을 결합하여 장기적인 소프트웨어 유지보수의 중요성과 방법에 대해 이야기하고자 해요.
1. 장기적인 소프트웨어 유지보수란 무엇인가?
유지보수의 범위
소프트웨어 유지보수는 단순히 앱을 실행 상태로 유지하는 것을 넘어서요. 기술 부채 관리, 리팩터링, 종속성 업데이트, 보안 확보, 신규 기능 구현 및 버그 수정 등 다양한 활동을 포함해요.
개인적인 경험
저는 현재 10년째 운영되고 있는 대규모 웹 서비스의 코드베이스와 함께 일하고 있으며, 그 중 5년을 직접 함께 개발해왔어요. 이 기간 동안 가장 크게 변화한 부분은 수십 가지의 기능이 마이크로서비스로 분리되었음에도 불구하고, 여전히 MAU 2천만의 대규모 트래픽을 소화하는 Ruby on Rails 코드베이스가 핵심 역할을 하고 있다는 점이에요. 특히 Rails 8, Ruby 3.4까지 꾸준히 버전 업데이트를 챙겨온 것이 가장 의미 있는 변화였다고 생각해요.
2. 기술 부채와 리팩터링: 현실적 접근의 필요성
기술 부채의 불가피성
Harvest 앱의 20년 가까운 역사에서도 기술 부채는 중요한 이슈였어요. 애플리케이션의 연식이 쌓일수록 기술 부채는 필연적으로 발생하며, 이를 어떻게 관리하느냐가 장기적 유지보수의 핵심이에요.
실제 개발 속도에 미치는 영향 (개인 경험)
기술 부채는 새로운 기능 개발 속도에 직접적인 영향을 줘요. 가장 흔한 경우는 구조적으로 확장성이 부족하거나, 다른 사람이 이해하기 어려운 형태로 만들어진 코드를 마주할 때예요. A라는 기능이 생각보다 성공해서 B, C, D로 확장해야 하는데, 애초에 그런 목적으로 만들지 않았기 때문에 데이터베이스 구조부터 다시 봐야 하는 경우도 있었어요.
또 다른 문제는 코드 파악과 이해에 드는 시간이에요. 요구사항은 간단하지만 기존 로직을 이해하는 데서 오는 시간 소모가 상당할 수 있어요. 특히 도메인 단위로 코드 간의 책임이 명확하지 않은 경우, “고구마 줄기”라고 표현할 수 있는 연쇄적인 문제가 발생하기도 해요.
리팩터링의 중요성과 접근법
강연에서 언급된 리팩터링 절차는 다음과 같아요.
먼저 ‘탐색의 날(exploratory days)’을 통해 문제 영역을 파악하고, 스파이크(spike)로 개념을 빠르게 실험해본 뒤, 점진적이고 체계적으로 리팩터링을 진행하는 것이 중요해요.
리팩터링 자체를 즐기는 마인드셋도 큰 도움이 됩니다.
그리고 불필요해진(죽은) 코드를 과감하게 삭제하는 습관은 코드베이스를 깔끔하게 유지하는 데 효과적이에요.
과거 코드에 대한 관점 (개인 경험)
5년간 함께 해온 코드베이스에서 과거의 코드를 마주할 때면, “내가 이렇게 했다고? 왜 이렇게 했지?” 하는 생각이 들기도 해요. 어제 쓴 코드도 오늘 보면 이상하다고 느끼는 경우가 있는데, 5년 전이라면 더 심한 감정을 느끼기도 했어요.
하지만 이런 상황에서는 “그땐 맞고 지금은 틀릴 수 있다”는 관점으로 접근하는 것이 중요해요. 서비스의 스테이지에 따른 우선순위가 늘 다르기 때문에, 지금의 상황에서 코드 품질 문제로 보이는 것도 사실 그때 그렇게 하지 않았다면 서비스가 살아남지 못했을 수도 있다고 생각해요.
기술 부채 vs 기능 개발: 현실적 우선순위 (개인 경험)
동료 사이에서도 늘 찬반이 나뉘는 문제이지만, 저는 이 부분에 대한 가치 판단은 명확하다고 생각해요. 살아남을 수 있는 궤도에 오른 회사나 팀이 아니라면 유저에게 딜리버리해서 PMF를 찾고 유저 경험을 개선하는 것이 최우선이고, 기술 부채는 그 다음이라고 봐요.
현재 MAU 2천만 서비스도 여전히 부족하다고 생각해요. 대부분의 유저가 한국에만 있고 글로벌에서는 임팩트를 내지 못하고 있는 상황에서, 여전히 PMF를 찾는 단계라고 볼 수 있어요. 다만 서비스 규모가 커질수록 코드베이스의 안정성도 함께 확보해야 하는 것이 맞아요.
연쇄적인 문제가 발생했을 때의 대응에는 정답이 있다고 보지 않아요. 개인적으로는 서비스를 담당하는 팀이라면 요구사항부터 처리하고 나중에 정리하는 것이 맞다고 생각해요. 하지만 늘 문제는 “나중에 정리”가 우선순위에서 밀려나면서 실행되지 않는 것인데, 이는 조직 차원에서 지속적으로 챙겨야 하는 부분이에요.
팀 내에서 기술 부채 vs 기능 개발에 대한 의견이 나뉠 때는 항상 어려운 판단이에요. “새로운 기능인가? 그렇다면 빠르게 가야 한다”, “이 부채 해소가 앞으로의 생산성에 큰 도움을 준다? 그럼 풀어보고 간다”는 관점으로 접근하고 있지만, 늘 케이스 바이 케이스라 판단은 어려운 것 같아요.
확장성 관점에서의 기술 부채 (개인 경험)
글로벌 진출이나 더 큰 성장을 목표로 할 때, 현재의 기술 부채가 발목을 잡을 수 있는 상황은 언제든 있을 수 있어요. 적은 트래픽을 사용하는 글로벌 서비스에서는 SaaS도 편하게 쓸 수 있지만, 국내 서비스에서는 비용과 트래픽 문제로 아예 쓸 수 없는 옵션이 되기도 해요. 지금은 트래픽이 적은 글로벌 서비스도 목표를 생각하면 그 상태 그대로 둘 수 없는 상황이에요.
데이터베이스 구조부터 다시 봐야 하는 상황에서는 부담스러워하기보다는 “마음만 먹으면 해낼 수 있는 일”이라고 접근해요. 컬럼을 추가하고, 새로운 데이터를 듀얼라이트하면서 나중에 백필까지 해야 하는 긴 작업이지만, 결국 해낼 수 있는 일이라고 생각해요.
3. 마이크로서비스 분리의 현실: 이상과 실제 사이의 간극 (개인 경험)
많은 회사에서 모놀리스에서 마이크로서비스로의 전환을 고민해요. 하지만 실제 경험해보니 이론과 현실 사이에는 상당한 간극이 있었어요.
분리 기준과 팀의 주도성
저희의 경우, 마이크로서비스 분리는 팀이 주도성을 가지는 기능을 기준으로 이루어졌어요. 더 정확히 말하면, 팀이 담당하는 서비스 성격이 아닌 것들, 주로 플랫폼적으로 더 개선해서 제공할 수 있는 것이거나 전사적으로 쓰이는 기능들을 분리해왔어요. 이미지 처리나 유저 인증 같은 기능들이 대표적인 예예요.
Rails 모놀리스는 결국 핵심 서비스 본연의 역할로 남게 되었고, 나머지 기능들을 마이크로서비스로 ‘내보내는’ 형태로 진행되었어요.
분리 과정에서 마주한 현실적 어려움
하지만 하나의 서비스에서 도메인을 명확하게 분리하는 것은 생각보다 훨씬 어려운 일이었어요. 커뮤니케이션 코스트가 많이 들었고, 결과적으로 제대로 된 분리가 되지 않은 경우도 많았어요.
가장 큰 문제는 기능적 도메인이 완전히 이전되지 못하고 마치 데이터베이스만 분리된 것처럼 애매한 상태로 남거나, 일부 기능은 여전히 기존 모놀리스 프로젝트에서 전사적으로 서빙되고 있는 상황이 발생한 것이에요.
이런 문제가 생긴 근본적인 이유는 서비스를 이관해가는 팀과 분리해야 하는 팀 간의 서비스를 바라보는 범위가 달랐기 때문이에요. 더 중요한 것은 마이크로서비스화의 목적이 명확한 도메인 분리보다는 모놀리스의 부담을 줄여야 한다는 것에 더 초점이 맞춰져 있었다는 점이에요. 결국 모놀리스가 바라보는 데이터베이스의 부하와 트래픽을 줄이는 것이 주목적이 되다 보니, 데이터베이스 수준의 분리에서 멈추는 경우가 많았어요.
마이크로서비스 분리에서 얻은 교훈
이런 경험을 통해 얻은 가장 중요한 교훈은 분리의 목적이 무엇인지를 명확히 하는 것이에요. 그리고 만약 분리를 한다면 관련 키워드나 로직이 모놀리스에는 최대한 남아있지 않게 하는 것이 중요해요.
관성적인 분리보다는 왜 나누어야 하는지를 명확히 하고, 그 관점을 이해관계자들이 모두 싱크를 맞춰야 해요. 이것이 제대로 되지 않으면 시간도 오래 걸리고, 결과적으로도 만족스럽지 않은 결과를 낳게 돼요.
4. Rails/Ruby 버전 업데이트: 달리는 기차의 바퀴 교체하기 (개인 경험)
대규모 서비스에서 Rails와 Ruby 버전을 꾸준히 최신 상태로 유지하는 것은 쉽지 않은 일이에요. 저희가 MAU 2천만 규모에서 Rails 8, Ruby 3.4까지 꾸준히 업데이트해올 수 있었던 가장 큰 요인은 동료들의 관심이었어요. 프레임워크나 언어의 버전을 올리는 것이 서비스 기능을 개발하는 것만큼 중요하다는 인식을 조직 전체에 전파해온 것이 핵심이었어요.
전담 조직의 중요성
가장 중요한 시점에 이런 업무를 전담할 수 있는 플랫폼팀이 세팅된 것도 큰 원동력이었어요. 모놀리스 코드베이스를 개선하는 목표를 가진 팀이었고, 현실적으로는 마이크로서비스화 과정을 돕는 경우가 많았지만, 이런 기반 작업을 할 수 있는 명분과 시간이 주어진 것이 중요했어요.
흥미롭게도 Rails 업데이트와 마이크로서비스 분리 작업은 서로 충돌하지 않았어요. 버전을 업데이트하는 것과 레거시 코드를 제거하는 것은 오히려 상호 보완적이었고, 오래된 코드가 제거될수록 업데이트에 도움이 되었어요. 마이크로서비스화에서는 주로 기존 로직을 설명하고 히스토리와 맥락을 전달하는 역할을 했기 때문에 막대한 리소스가 필요하지도 않았어요.
조직의 이해와 지원
다행히 이런 작업에 대한 조직의 이해도가 높았어요. 팀을 세팅한 조직장부터 이런 기반 작업의 가치를 높이 평가했고, 보통 ‘달리는 기차의 바퀴를 교체한다’고 표현하는 이 작업을 앞으로 더 빠르고 잘 나아가기 위한 기반 작업으로 이해해주었어요.
실제 업데이트 과정에서의 도전과제
10년 된 코드베이스에서 가장 까다로웠던 부분은 gem 호환성이나 deprecated된 기능이 아니었어요. 이런 것들은 마음먹고 고치면 되는 부분이었죠. 진짜 어려운 것은 너무 오래된 코드라서 실제로 사용되는지, 정말 필요한지 판단할 수 없는 경우였어요. 안 쓰면 지우면 되는데 누구도 그것을 장담할 수 없는 상황이 가장 곤란했어요. 이런 경우에는 실제로 호출이 되는지를 로깅을 통해 길게 확인하는 기간을 두기도 했어요.
5. 종속성 관리 및 업데이트: 지속적인 관심이 필요
종속성(gem, 라이브러리 등)을 정기적으로 관리하고 업데이트하는 것은 보안 및 안정성 측면에서 필수적이에요. 종속성 업데이트뿐만 아니라, Rails 자체의 기능 발달에 따라 더 이상 필요하지 않은 종속성을 제거하는 것도 중요해요.
Harvest에서는 Renovate를 통해 종속성 업그레이드를 체계적으로 관리하고, 보안 관련 업데이트에는 Dependabot을 활용해요. 새로운 기능 구현 시 해당 종속성을 먼저 업데이트하는 등 proactive한 접근 방식이 권장돼요. bundle outdated 명령어와 같이 종속성 상태를 파악하는 도구도 유용해요.
6. 팀 역학 및 유지보수의 공동 책임
유지보수는 특정 팀이나 개인의 전담 업무가 아닌, 엔지니어링 팀 전체의 공동 책임이에요. “Boy Scout Rule” (코드를 발견했을 때보다 더 나은 상태로 두는 규칙)은 지속적인 코드 품질 향상에 도움이 돼요.
프로젝트 계획 단계에서 유지보수 작업에 필요한 시간을 충분히 고려하는 것이 중요해요. 특히 오래된 코드베이스 작업 시 예상치 못한 복잡성으로 인해 시간이 더 소요될 수 있어요. PM과 엔지니어가 협력하여 기능의 복잡성과 실현 가능성을 논의하는 것이 필요해요.
7. 개발 워크플로우에 유지보수 통합하기
Pull Request(PR) 및 코드 리뷰는 유지보수가 자연스럽게 이루어지는 중요한 단계예요. 코드 리뷰를 통해 종속성 업데이트 여부 등을 확인하고 유지보수 개선점을 제안할 수 있어요.
PR 설명은 상세하게 작성하여, 몇 달 혹은 몇 년 후 코드를 다시 보게 될 동료(또는 미래의 자신)를 배려해야 해요. 테스트 스위트 관리는 필수적이에요. 신뢰할 수 있는 테스트는 코드 변경 및 배포의 자신감을 높여줘요. 테스트 커버리지를 높이고, flaky 테스트를 제거하며 CI 시스템 속도를 관리하는 것이 중요해요.
CI/CD 파이프라인 구축과 피처 플래그 활용은 안전하고 잦은 배포를 가능하게 해요. 배포 후 문제가 발생했을 때 빠르게 대응할 수 있는 롤백 전략도 필요해요. 배포 후에는 관찰성 도구(Observability tools, 예: DataDog, Grafana)를 통해 애플리케이션 상태를 모니터링해야 해요.
8. 핵심 로직 변경의 자신감: Scientist Gem
기존 코드와 새 코드의 결과를 프로덕션 환경에서 안전하게 비교하는 것은 핵심 로직을 리팩터링할 때 큰 도움이 돼요. GitHub에서 개발한 Ruby 라이브러리인 Scientist gem은 이러한 ‘실험’을 가능하게 해요.
Scientist를 사용하면 기존 코드(control)와 새 코드(candidate)를 함께 실행하고, 결과 불일치(mismatch)를 보고받아 문제점을 파악할 수 있어요. Harvest의 사례에서는 권한 확인 로직 변경, SQL 쿼리 비교, 캐싱 제거 가능성 탐색 등에 Scientist를 활용했어요. 실험 결과 보고 및 분석을 위한 시스템 구축도 필요해요 (예: Redis 저장, Grafana 연동).
마치며
오래된 Rails 애플리케이션을 성공적으로 유지보수하는 것은 새로운 기능을 개발하는 것만큼, 아니 그 이상으로 중요하며 전략적인 접근이 필요해요.
부지런한 유지보수와 과거 경험으로부터의 지속적인 학습이 효과적인 엔지니어링의 기반이 돼요. 개인적인 노력(“Boy Scout Rule”, PR 설명 상세 작성 등)과 팀 차원의 시스템 및 문화 구축(공동 책임, 계획, 도구 활용 등)이 결합될 때 장기적인 유지보수는 성공할 수 있어요.
특히 마이크로서비스 분리 경험을 통해 배운 것은 “분리의 목적을 명확히 하고, 이해관계자 간 싱크를 맞추는 것”의 중요성이에요. 관성적인 분리보다는 명확한 목적 의식을 가지고 접근해야 하며, 관련 로직과 키워드가 모놀리스에 남아있지 않도록 철저하게 분리해야 해요.
Julia López의 강연에서 얻은 교훈과 10년 된 대규모 웹 서비스를 5년간 함께 키워온 경험을 통해, 오래된 애플리케이션을 ‘유지’하는 것을 넘어 ‘발전’시키기 위한 실질적인 방법들을 다시 한번 생각해볼 수 있었어요.
궁극적으로, 꾸준한 개선과 학습을 멈추지 않는 지속가능한 엔지니어링 문화가 바로 오랜 시간 살아남는 서비스를 만드는 힘이라고 믿습니다.
참고 자료
- López, Julia. “Beyond the Hype: Practical lessons in Long-Term Rails maintenance” (2024). Available at: https://www.youtube.com/watch?v=uublj1pAkOg