UP 코드베이스 내 커맨드 패턴의 진화: 8년간의 여정 분석

Retrospective on the Command Pattern at Ferocia - Tom Dalling

작성자
Ruby Australia
발행일
2025년 08월 11일

핵심 요약

  • 1 UP 디지털 뱅킹 앱의 대규모 Ruby 모놀리스 백엔드에서 비즈니스 로직을 관리하기 위한 '커맨드 패턴'의 도입과 8년간의 진화 과정을 상세히 설명합니다.
  • 2 초기에는 순수 Ruby 객체로 시작하여 Sorbet 통합, 백그라운드 작업, DataDog 트레이싱 기능을 포함하는 CommandRunnable 모듈로 발전하며 재사용성과 관측 가능성을 높였습니다.
  • 3 명확한 개념 정의와 초기 문서화 부족으로 인해 '커맨드'의 의미가 모호해지고 유효성 검사 및 백그라운드 작업과의 역할 혼동 등 개선점이 존재함을 지적합니다.

도입

본 발표는 Ferocia/Bendigo Bank의 UP 디지털 뱅킹 애플리케이션 백엔드에 적용된 '커맨드 패턴'의 역사와 진화를 다룹니다. UP은 지점 없는 디지털 뱅킹 서비스로, 그 백엔드는 방대한 Ruby 모놀리스로 구축되어 있습니다. Rails 애플리케이션이 성장함에 따라 비즈니스 로직의 복잡도가 증가하고, 기존 MVC(Model-View-Controller) 패턴 내에서 이를 적절히 배치하기 어려운 '네 번째 버킷(fourth bucket)' 문제가 발생했습니다. 본 발표는 UP 코드베이스가 이 문제를 해결하기 위해 커맨드 패턴을 어떻게 도입하고 발전시켜 왔는지, 그리고 그 과정에서 얻은 교훈과 개선점을 탐구합니다.

Rails MVC와 ‘네 번째 버킷’의 필요성 (0:58)

Rails는 기본적으로 모델(Model), 뷰(View), 컨트롤러(Controller) 세 가지 구조를 제공하며, DHH(Rails 창시자)는 이 세 가지면 충분하다고 주장합니다. 특히 ‘Fat Model’ 패턴을 선호하여 대부분의 비즈니스 로직을 Active Record 모델에 집중시키도록 권장합니다. 그러나 이 방식은 애플리케이션이 복잡해지고 규모가 커지면서 ‘God Model’(단일 모델에 모든 로직이 집중되어 거대하고 다루기 어려워지는 현상)을 초래하는 문제점이 있습니다. 컨트롤러는 재사용성이 낮고 HTTP 요청에 의존한다는 단점이 있어 비즈니스 로직을 담기에는 부적합합니다. 이에 따라 많은 개발자들은 모델, 뷰, 컨트롤러 외에 비즈니스 로직을 분리할 ‘네 번째 버킷’을 모색하게 되었고, 서비스 객체(Service Objects)나 인터랙터(Interactor)와 같은 다양한 패턴이 제안되었습니다. UP 코드베이스에서는 이를 ‘커맨드(Commands)’라고 명명했습니다.

UP 커맨드 패턴의 역사 (4:15)

1. 초기 도입 (2017년)

  • 최초 커맨드: 2017년 Langers에 의해 Engine::Command 네임스페이스에 첫 커맨드가 작성되었습니다.
  • 구조: 상속받는 부모 클래스 없이 순수 Ruby 클래스로, 단일 클래스 메서드 run을 가졌습니다. 이 메서드는 인자를 받아 데이터베이스를 변경하는 등의 작업을 수행했습니다.
  • 의도: 커밋 메시지(“새로운 기능을 더 명확하게 추가하기 위해 create_transaction 클래스를 별도 파일로 이동”)에서 알 수 있듯이, 복잡해지는 도메인 로직을 분리하려는 의도였습니다. GraphQL에서 이미 ‘뮤테이션(mutation)’이라는 용어를 사용하고 있었기 때문에, 혼동을 피하고자 ‘커맨드’라는 이름을 선택했습니다. 이는 CQRS(Command Query Responsibility Segregation)의 ‘커맨드’처럼 시스템을 변경하는(mutate) 역할을 의도했음을 시사합니다.

2. 패턴의 진화 (초기)

  • 인스턴스 기반 실행: 몇 주 후, run 클래스 메서드가 인자를 받아 인스턴스를 생성하고 해당 인스턴스의 run 메서드를 호출하는 패턴으로 변경되었습니다. 이는 Ruby에서 흔히 사용되는 ‘함수형 객체(function as an object)’ 디자인 패턴으로, 자연스러운 발전 과정이었습니다.
  • Interactor Gem 제거: 관리자 영역에서 복잡한 도메인 로직을 처리하기 위해 interactor Gem이 도입되었으나, 6개월 후 해당 로직이 단순화되면서 Gem을 유지할 필요성이 사라졌습니다. Langers는 interactor Gem을 제거하고 관련 로직을 기존 ‘커맨드’ 패턴으로 전환했습니다.

3. 네임스페이스 및 기반 모듈 변경 (2019년 - 2021년)

  • 네임스페이스 제거 (2년 후): 94개의 커맨드가 존재하던 시점에 Engine 네임스페이스의 개념이 불필요해져 삭제되었고, 현재와 같은 전역 Command 네임스페이스가 확립되었습니다.
  • Sorbet 도입과 T::Struct: 약 480개의 커맨드가 작성된 시점에서 코드베이스에 Sorbet이 도입되었습니다. 커맨드들은 여전히 순수 Ruby 객체였으나, Sorbet의 T::Struct를 사용하여 타입이 지정된 속성을 정의하기 시작했습니다.
  • CommandRunnable 모듈 도입: T::Struct와 기존 ‘클래스 메서드에서 인스턴스 메서드 호출’ 패턴 간의 타입 추론 문제(Sorbet이 클래스 메서드의 타입을 제대로 인식하지 못함)가 발생했습니다. 이를 해결하기 위해 CommandRunnable 모듈이 도입되었습니다. 초기에는 이 모듈 자체가 코드 로직을 제공하기보다는, Sorbet 커스텀 플러그인이 해당 클래스를 인식하고 타입 문제를 해결하도록 ‘태그’하는 역할을 했습니다.

4. 기능 추가 및 현재 (2022년 - 현재)

  • 백그라운드 작업 지원: CommandRunnable 모듈에 메서드가 추가되기 시작했습니다. 첫 번째로 추가된 기능은 커맨드를 백그라운드 작업으로 실행하는 것이었습니다. define_job DSL을 통해 우선순위를 지정하고, run_later를 호출하여 커맨드를 백그라운드 큐에 추가할 수 있게 되었습니다.
  • DataDog 트레이싱: 이후 모든 커맨드에 기본적으로 DataDog 트레이싱 기능이 추가되었습니다. 이는 프로덕션 환경에서 성능 특성 및 호출 빈도 등 관측 가능성을 높여 디버깅에 큰 도움을 줍니다.
  • 현재 상태: 2023년 현재 약 1,135개의 커맨드가 존재하며, CommandRunnable 모듈은 108라인으로 성장했습니다. 이 모듈은 Sorbet 호환성, 함수형 객체 패턴, 백그라운드 작업 실행, 그리고 자동 DataDog 트레이싱 기능을 제공합니다.

회고: 잘된 점과 개선점 (11:57)

잘된 점

  • ‘네 번째 버킷’ 역할 수행: 모델, 컨트롤러, GraphQL 로직을 단순하게 유지하며 비즈니스 로직을 효과적으로 분리했습니다.
  • 재사용성: GraphQL이나 Rails 컨트롤러 등 다양한 호출 지점에서 동일한 커맨드를 재사용할 수 있습니다.
  • 낮은 복잡도: 8년 동안 단 108라인의 CommandRunnable 모듈만으로 모든 기능을 제공하며, 학습 곡선이 낮아 신규 개발자들이 쉽게 패턴을 익힐 수 있습니다.
  • 테스트 용이성: 비즈니스 로직을 호출 지점과 분리하여 테스트하기 용이하며, 테스트 속도도 향상됩니다.
  • 뛰어난 관측 가능성: DataDog 트레이싱이 기본 제공되어 프로덕션 환경에서의 디버깅 및 성능 모니터링이 매우 효율적입니다.

개선점

  • 모호한 이름: ‘커맨드’라는 이름이 너무 일반적이어서 Gang of Four의 커맨드 패턴, CQRS의 커맨드, 이벤트 소싱의 커맨드 등 다양한 개념과 혼동을 야기합니다. 이는 신규 입사자들에게 학습의 어려움을 줍니다.
  • 개념의 모호화: 원래 시스템을 변경하는(mutate) 역할을 의도했으나, 시간이 지나면서 비즈니스 로직이 없거나, 시스템을 변경하지 않는 순수 함수, 심지어 쿼리(query)로 사용되는 경우도 생겨 원래의 의도가 희석되었습니다.
  • 초기 문서화 부족: 초기에 ‘커맨드’의 개념과 사용 목적에 대한 명확한 문서화가 이루어지지 않아, ‘부족 지식(tribal knowledge)’에 의존하게 되었고, 이는 조직 규모가 커지면서 개념의 모호화를 심화시켰습니다.
  • 유효성 검사 메커니즘 부재: 커맨드 내에 내장된 유효성 검사 메커니즘이 없어, 개발자들이 예외(exception)를 던지는 방식으로 처리하고 있습니다. 이는 유효성 검사 로직의 재사용성을 저해하고, GraphQL이나 Rails 응답에서 에러를 구조화하여 반환하기 어렵게 만듭니다.
  • 비즈니스 로직과 백그라운드 작업의 혼용: 비즈니스 로직(커맨드)과 백그라운드 작업(job)의 개념이 혼용되어 있습니다. 모든 비즈니스 로직이 백그라운드 작업으로 실행될 필요는 없으며, 모든 백그라운드 작업이 비즈니스 로직을 포함하는 것은 아닙니다. 이로 인해 개발자들이 단순히 백그라운드 작업을 만들고자 할 때 CommandRunnable을 포함시켜 불필요하게 ‘커맨드’의 역할을 부여하는 경우가 발생합니다. 또한, 백그라운드 작업은 인자 직렬화가 필수적인데, 모든 커맨드의 인자가 직렬화 가능한 것은 아니어서 문제가 발생할 수 있습니다.

결론

UP 코드베이스의 커맨드 패턴은 '네 번째 버킷'으로서의 역할을 성공적으로 수행하며, 재사용성, 테스트 용이성, 그리고 관측 가능성 측면에서 긍정적인 기여를 했습니다. 8년 동안 1,000개 이상의 커맨드로 확장되었음에도 불구하고, 핵심 모듈의 복잡도는 낮게 유지되었습니다. 그러나 '커맨드'라는 이름의 모호성, 초기 개념 정의 및 문서화 부족으로 인한 역할 혼동, 그리고 유효성 검사 및 백그라운드 작업과의 불명확한 경계는 개선이 필요한 부분으로 지적됩니다. 향후에는 커맨드의 역할을 더욱 명확히 정의하고, 유효성 검사 메커니즘을 표준화하며, 비즈니스 로직과 백그라운드 작업의 역할을 분리하는 방향으로 발전할 필요가 있습니다. 전반적으로 B학점 수준의 '충분히 좋은(good enough)' 솔루션으로 평가됩니다.

댓글 0

댓글 작성

0/1000
정중하고 건설적인 댓글을 작성해 주세요.

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!