Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby on Rails 애플리케이션에서 모듈과 믹스인을 활용하는 방법

모듈과 믹스인은 Ruby를 매력적인 언어로 만드는 핵심 요소입니다. 한 번 작성한 코드를 다른 곳에서도 손쉽게 재사용할 수 있게 해주며, 기능별로 코드를 묶어 관심사를 분리함으로써 가독성과 유지보수성을 크게 향상시킵니다.

이 글에서는 모듈과 믹스인의 개념을 살펴보고, 모듈을 생성하고 클래스에 믹스인으로 포함시키는 방법, 그리고 Ruby on Rails 애플리케이션에서 이를 활용했을 때 얻을 수 있는 이점까지 자세히 알아보겠습니다.

모듈(Module)이란 무엇일까?

모듈은 Ruby의 가장 빛나는 기능 중 하나로, 두 가지 큰 장점을 제공합니다. 바로 네임스페이스를 만들어 이름 충돌을 방지하는 것과, 믹스인으로 활용해 애플리케이션 전반에서 코드를 공유하는 것입니다.

구조적으로 모듈은 일반 Ruby 클래스와 상당히 유사합니다. 실제로 Ruby에서는 ClassModule의 일종입니다. irb 콘솔에서 아래와 같이 확인할 수 있습니다.

> Class.is_a?(Module)
 => true

클래스처럼 모듈도 메서드와 상수를 그룹화하고 코드를 공유할 수 있습니다. 하지만 일반 클래스와는 몇 가지 중요한 차이점이 있습니다.

  • 정의 시 class 키워드 대신 module 키워드를 사용합니다.
  • 모듈은 인스턴스화할 수 없으므로 객체를 생성할 수 없습니다.
  • 모듈을 상속받을 수 없으며, 대신 믹스인으로 활용합니다.
  • 모듈은 독립적인 코드이기 때문에 모듈 간 상속 계층 구조가 없습니다.

모듈은 서비스 객체, 관심사(concerns), 상수 등 동일한 책임을 가진 코드들을 함께 묶어두기에 가장 적합한 장소입니다.

모듈은 일반적으로 다음과 같은 형태를 가집니다.

# lib/modules/invoice_creator.rb
module InvoiceCreator
  TAX_FEE = 0.5
 
  def self.generate
    puts "Don't worry! I'll generate the invoice for you at #{TAX_FEE}%"
  end
 
  def invoice_total
    puts "I'll return the invoice total"
    1000
  end
end

위 예제에서 볼 수 있듯이, 모듈은 두 가지 종류의 메서드를 제공할 수 있습니다. 바로 모듈 메서드(module method)인스턴스 메서드(instance method)입니다.

self.generate는 모듈 메서드입니다. 따라서 모듈을 다른 객체에 include하거나 extend하지 않고도 바로 호출할 수 있습니다. 서비스 객체를 만들 때 흔히 사용되는 패턴입니다.

2.5.3 :006 > InvoiceCreator.generate
Don't worry! I'll generate the invoice for you
=> nil

반면 invoice_total은 인스턴스 메서드이므로, 사용하려면 해당 모듈을 클래스에 포함(include)시켜야 합니다.

# app/models/invoice.rb
class Invoice < ApplicationRecord
  include InvoiceCreator # Invoice 클래스에 모듈을 포함시킵니다
 
  def calculate_tax
    total = invoice_total # 모듈에서 가져온 메서드
    tax = total * InvoiceCreator::TAX_FEE
    puts "This is the invoice tax: #{tax}"
  end
end

InvoiceCreator의 모든 인스턴스 메서드가 Invoice 인스턴스에서 사용 가능해지므로, calculate_tax 메서드를 아주 간단하게 호출할 수 있습니다.

2.5.3 :008 > Invoice.new.calculate_tax
"I'll return the invoice total"
"This is the invoice tax: 500.0"
 => nil

또한 calculate_tax 내부에서 모듈 안에 정의된 상수를 사용하고 있다는 점에 주목하세요. 앞서 언급했듯이 모듈은 훌륭한 상수 보관소 역할도 합니다!

이제 공급자(supplier)와 고객(customer)에게 완전히 다른 형태의 인보이스를 발행하기 위해 두 종류의 InvoiceCreator가 필요한 상황을 상상해 보세요. 같은 이름의 클래스가 충돌하게 되는데, 이 문제를 해결하려면 모듈의 또 다른 강력한 장점인 네임스페이스를 활용해야 합니다. 다음 섹션에서 자세히 살펴보겠습니다.

네임스페이스의 활용

네임스페이스란 특정 기능에 대한 로컬 컨텍스트를 만들어 코드를 체계적으로 조직화하는 방식입니다. 방금 설명한 시나리오, 즉 고객용 인보이스 생성기와 공급자용 인보이스 생성기에 각각 다른 컨텍스트가 필요한 경우에 딱 맞는 해결책입니다.

코드로 돌아가서 두 개의 서로 다른 모듈을 만들어 보겠습니다.

# lib/modules/customer/invoice_creator.rb
module Customer
  module InvoiceCreator
    def self.generate
      puts "Don't worry! I'll generate the customer invoice for you"
    end
  end
end
 
# lib/modules/supplier/invoice_creator.rb
module Supplier
  module InvoiceCreator
    def self.generate
      puts "Don't worry! I'll generate the supplier invoice for you"
    end
  end
end

이제 필요한 곳에서 Customer::InvoiceCreator 또는 Supplier::InvoiceCreator를 호출하면 됩니다.

2.5.3 :014 > Customer::InvoiceCreator.generate
Don't worry! I'll generate the customer invoice for you
 => nil
2.5.3 :015 > Supplier::InvoiceCreator.generate
Don't worry! I'll generate the supplier invoice for you
 => nil
2.5.3 :016 >

이렇게 하면 각각의 고유한 코드가 자신만의 모듈 안에 캡슐화되어 관심사 분리 원칙을 자연스럽게 지킬 수 있습니다. 더불어 네임스페이스를 적극적으로 활용하면 코드베이스를 깔끔하게 정리하는 데도 큰 도움이 됩니다.

이제 Ruby 모듈의 또 다른 강력한 기능, 믹스인을 활용하는 방법을 알아보겠습니다.

믹스인(Mixin)의 마법

이미 알고 계시겠지만, Ruby는 단일 상속(single inheritance) 메커니즘을 채택하고 있습니다. 즉, 하나의 클래스는 오직 하나의 클래스로부터만 상속받을 수 있습니다. 하지만 실제 개발에서는 여러 클래스로부터 기능을 물려받아야 하는 경우가 자주 발생합니다. Ruby에서는 이러한 니즈를 '상속보다 조합(composition over inheritance)' 패턴으로 해결합니다.

그 해결책이 바로 믹스인입니다. Ruby 클래스에 코드 조각을 믹스인으로 포함시키면, 상속 없이도 해당 클래스에 새로운 동작(behavior)을 추가할 수 있습니다. 놀랍지 않나요? 즉, Ruby에서 믹스인이란 필요한 클래스에 포함시키는 모듈을 의미합니다. 이를 통해 코드를 깔끔하게 유지하고 책임을 분리된 상태로 관리할 수 있습니다.

예를 들어, InvoiceCreator 모듈이 인보이스 생성 프로세스를 완수하기 위해 InvoiceCalculator, InvoiceRenderer, InvoiceSender 같은 여러 모듈의 기능이 필요하다고 가정해 봅시다.

여러 모듈을 믹스인으로 연결해서 포함시키면, 아래 예제처럼 해당 메서드들을 직접 호출할 수 있습니다.

# lib/modules/invoice_calculator.rb
module InvoiceCalculator
  def calculate_items_total
    puts "imagine some math here"
  end
end
 
# lib/modules/invoice_renderer.rb
module InvoiceRenderer
  def generate_invoice_pdf
    puts "imagine that we are using some PDF generation magic here"
  end
end
 
# lib/modules/invoice_sender.rb
module InvoiceSender
  def send_invoice
    puts "imagine your favorite mail service being used here"
  end
end

InvoiceCreator가 나머지 세 모듈을 모두 상속받을 수는 없기 때문에, 대신 include 방식으로 포함시킵니다. 이렇게 하면 InvoiceCreator는 다른 인보이스 관련 모듈들의 모든 메서드를 포함하게 되며, InvoiceCreator 모듈이 포함된 어떤 클래스나 모듈에서도 이 메서드들을 호출할 수 있습니다. 단, 주의할 점이 있습니다! 여러 모듈에 같은 이름의 메서드가 존재하면 서로 덮어쓰게 되므로 조심해야 합니다.

# lib/modules/customer/invoice_creator.rb
module Customer
  module InvoiceCreator
    include InvoiceCalculator
    include InvoiceRenderer
    include InvoiceSender
 
    def generate_invoice
      calculate_items_total # InvoiceCalculator에서 가져옴
      generate_invoice_pdf # InvoiceRenderer에서 가져옴
      send_invoice # InvoiceSender에서 가져옴
    end
  end
end

이제 서비스를 포함시킨 어디에서든 다음과 같이 메서드를 호출할 수 있습니다.

# app/models/invoice.rb
class Invoice < ApplicationRecord
  include Customer::InvoiceCreator
 
  def send_invoice_to_customer
    puts "Don't worry! I'll generate the customer invoice for you"
    generate_invoice
  end
end

InvoiceCreator 모듈을 통해 포함된 인보이스 모듈들의 메서드를 호출하면 다음과 같은 결과를 얻습니다.

2.5.3 :051 > Invoice.new.generate_invoice
Don't worry! I'll generate the supplier invoice for you
imagine some math here
imagine that we are using some PDF generation magic here
imagine your favorite mail service being used here
 => nil

바로 이것이 '상속보다 조합' 원칙을 실천하는 방식입니다. 이 원칙은 가능한 한 상속 대신 조합을 사용하라고 권장하며, 조합에서는 특정 기능을 다른 클래스에 제공하는 역할을 담당하는 클래스를 만들게 됩니다. 우리가 방금 한 작업이 정확히 그것입니다.

마치며

Ruby 개발자라면 코드를 설계하고, 작성하고, 유지보수하고, 리팩토링할 때 언어가 제공하는 편의 기능과 문법적 설탕(syntax sugar)을 사랑하게 됩니다. 이 글을 통해 모듈이 코드 가독성을 높이고, 단일 책임 원칙을 지키며, 코드베이스를 깔끔하고 유지보수하기 쉽게 만들어 주는 훌륭한 도구라는 점을 느끼셨기를 바랍니다. 또한 여러 소스로부터 기능을 물려받아야 하는 상황에서 믹스인의 마법으로 모듈을 활용할 수 있다는 것도 기억해 두세요.

앞으로도 Ruby와 Rails의 강력한 기능들을 계속 소개할 예정이니 많은 기대 부탁드립니다!