Dry Monads와 Dry Transaction을 활용한 클린 Rails 컨트롤러

Clean Rails Controllers with Dry Monads and Dry Transaction | by Luciana Mascarenhas | Medium

작성자
jeff
발행일
2025년 04월 28일

핵심 요약

  • 1 Rails 컨트롤러는 비즈니스 로직을 분리하여 HTTP 요청 및 응답 처리만을 담당하는 씬(thin) 컨트롤러로 유지해야 합니다.
  • 2 dry-monads는 Success 및 Failure 객체를 통해 명시적인 성공/실패 처리를 제공하여 코드의 예측 가능성과 가독성을 높입니다.
  • 3 dry-transaction은 비즈니스 로직을 단계별로 구성하고 실패 시 자동으로 흐름을 중단시켜 복잡한 작업을 체계적으로 관리할 수 있게 돕습니다.

도입

Rails 애플리케이션 개발 시 컨트롤러에 유효성 검사, 데이터베이스 작업, 외부 API 호출 등 많은 비즈니스 로직이 포함되어 이른바 '비대해진 컨트롤러(Fat Controller)' 문제가 발생하기 쉽습니다. 이러한 컨트롤러는 가독성을 떨어뜨리고, 테스트 및 유지보수를 어렵게 하며, 코드 재사용성을 저해합니다. 이상적인 컨트롤러는 단일 책임 원칙(Single Responsibility Principle, SRP)에 따라 HTTP 요청 및 응답 처리에만 집중해야 합니다. 본 글은 이러한 문제를 해결하기 위해 dry-monads와 dry-transaction 라이브러리를 활용하여 Rails 컨트롤러를 더욱 깔끔하고 효율적으로 구성하는 방법을 제시합니다.

씬(Thin) 컨트롤러의 필요성 및 문제점

Rails 컨트롤러에 비즈니스 로직이 포함될 경우 다음과 같은 문제점이 발생합니다. * 테스트의 어려움: HTTP 요청/응답과 비즈니스 로직이 섞여 있어 단위 테스트가 복잡해집니다. * 이해하기 어려움: 코드의 응집도가 낮아지고, 특정 로직의 흐름을 파악하기 어렵습니다. * 변경의 어려움: 작은 변경이 예상치 못한 부작용을 일으킬 수 있으며, 로직 재사용이 어렵습니다.

컨트롤러는 오직 HTTP 요청을 받고 응답을 반환하는 역할에 집중하고, 모든 비즈니스 로직은 서비스 객체나 유스케이스(Use Case)와 같은 외부 계층에서 처리되어야 합니다.

dry-monads 활용

dry-monadsSuccessFailure와 같은 특별한 객체를 사용하여 오류를 명시적으로 처리하고 작업을 연결하는 깔끔한 방법을 제공합니다. * if/else 문이나 nil 반환 대신 항상 의미 있는 결과를 반환하여 코드의 예측 가능성과 가독성을 높입니다. * yield 키워드를 통해 각 단계의 결과를 전달하며, 중간에 Failure가 발생하면 즉시 실행을 중단하고 Failure를 반환합니다. 이를 통해 복잡한 오류 처리 로직 없이도 깔끔한 흐름 제어가 가능합니다.

```ruby class CreateUserService include Dry::Monads[:result, :do]

def call(params) user = yield validate(params) result = yield save_user(user) Success(result) end

private def validate(params) if params[:email].nil? Failure(“Email can’t be blank”) else Success(params[:email]) end end

def save_user(email) user = User.create(email: email) user.persisted? ? Success(user) : Failure(“User creation failed”) end end ```

dry-transaction 활용

dry-transaction은 비즈니스 로직을 일련의 step으로 구성하여 정의하는 간단한 방법을 제공합니다. * 각 stepdry-monadsResult 객체(Success 또는 Failure)를 반환합니다. * 한 step에서 Failure가 발생하면 전체 트랜잭션의 실행이 자동으로 중단되고 해당 Failure 결과가 반환됩니다. * 이를 통해 오류 처리 로직이 비즈니스 로직과 섞이지 않고 깔끔하게 분리됩니다.

```ruby class CreateUser include Dry::Transaction include Dry::Monads[:result]

step :validate step :persist

def validate(input) return Failure(:invalid) if input[:name].blank? Success(input) end

def persist(input) user = User.create(input) Success(user) end end ```

dry-matcher를 통한 컨트롤러의 결과 처리

dry-matcher를 사용하면 컨트롤러에서 서비스의 Success 또는 Failure 결과를 on.successon.failure 블록을 통해 깔끔하게 패턴 매칭하여 처리할 수 있습니다.

ruby MyService.call(params) do |on| on.success do |presenter| render json: presenter end on.failure do |result| render json: result, status: 400 end end

서비스 호출 및 공통 설정

  • 서비스 객체는 일반적으로 MyService.new.call 형태로 호출됩니다. dry-transaction을 사용하면 .call 메소드가 자동으로 정의됩니다.
  • include Dry::Transactioninclude Dry::Monads[:result]와 같은 반복적인 코드를 피하기 위해, 이들을 포함하는 기본 서비스 클래스를 생성하고 모든 서비스가 이를 상속받도록 할 수 있습니다.
  • dry-rails 젬은 dry-monadsdry-transaction을 포함한 여러 dry 관련 젬을 한 번에 설치하여 dry 생태계 전체를 Rails 프로젝트에 통합할 수 있도록 돕습니다. 필요한 경우 개별 젬만 설치할 수도 있습니다.

결론

최신 Rails 버전에서 코드 구조화를 위한 자체적인 도구들이 제공되지만, `dry-monads`와 `dry-transaction`은 특히 대규모 애플리케이션이나 복잡한 비즈니스 흐름을 다룰 때 그 가치를 발휘합니다. 이들 라이브러리는 흐름과 오류에 대한 명시적인 제어를 가능하게 하며, 예외(exception)를 제어 흐름으로 사용하는 것을 지양하고 실패를 명시적으로 처리하도록 강제하여 더욱 견고하고 유지보수하기 쉬운 코드를 작성하는 데 기여합니다.

댓글 0

댓글 작성

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

아직 댓글이 없습니다

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