Rails의 발행-구독 패턴: 구현 튜토리얼

게시 됨: 2022-03-11

게시-구독 패턴(또는 줄여서 pub/sub)은 메시지 발신자(게시자)가 특정 수신자(구독자)에게 직접 전송되도록 메시지를 프로그래밍하지 않는 Ruby on Rails 메시징 패턴입니다. 대신 프로그래머는 구독자에 대한 정보 없이 메시지(이벤트)를 "게시"합니다.

마찬가지로 구독자는 하나 이상의 이벤트에 관심을 표시하고 게시자에 대한 정보 없이 관심 있는 메시지만 받습니다.

이를 수행하기 위해 "메시지 브로커" 또는 "이벤트 버스"라고 하는 중개자가 게시된 메시지를 수신한 다음 이를 수신하도록 등록된 가입자에게 전달합니다.

즉, pub-sub는 이러한 구성 요소가 서로의 ID에 대해 알지 못하는 상태에서 서로 다른 시스템 구성 요소 간에 메시지를 전달하는 데 사용되는 패턴입니다.

이 레일스 튜토리얼에서는 발행-구독 디자인 패턴이 이 다이어그램에 배치되어 있습니다.

이 디자인 패턴은 새로운 것은 아니지만 Rails 개발자가 일반적으로 사용하지 않습니다. 다음과 같이 이 디자인 패턴을 코드 기반에 통합하는 데 도움이 되는 많은 도구가 있습니다.

  • Wisper(나는 개인적으로 선호하며 더 논의할 것임)
  • 이벤트버스
  • EventBGBus(EventBus의 포크)
  • 토끼MQ
  • 레디스

이러한 모든 도구는 기본 pub-sub 구현이 다르지만 모두 Rails 애플리케이션에 대해 동일한 주요 이점을 제공합니다.

Pub-Sub 구현의 장점

모델/컨트롤러 팽창 감소

Rails 애플리케이션에 일부 뚱뚱한 모델이나 컨트롤러를 사용하는 것은 일반적인 방법이지만 모범 사례는 아닙니다.

pub/sub 패턴은 뚱뚱한 모델이나 컨트롤러를 쉽게 분해하는 데 도움이 될 수 있습니다.

콜백 감소

모델 간에 얽힌 콜백이 많다는 것은 잘 알려진 코드 냄새이며 조금씩 모델을 밀접하게 연결하여 유지 관리하거나 확장하기 어렵게 만듭니다.

예를 들어 Post 모델은 다음과 같을 수 있습니다.

 # app/models/post.rb class Post # ... field: content, type: String # ... after_create :create_feed, :notify_followers # ... def create_feed Feed.create!(self) end def notify_followers User::NotifyFollowers.call(self) end end

Post 컨트롤러는 다음과 같이 보일 수 있습니다.

 # app/controllers/api/v1/posts_controller.rb class Api::V1::PostsController < Api::V1::ApiController # ... def create @post = current_user.posts.build(post_params) if @post.save render_created(@post) else render_unprocessable_entity(@post.errors) end end # ... end

보시다시피 Post 모델에는 모델을 Feed 모델과 User::NotifyFollowers 서비스 또는 관심사에 밀접하게 연결하는 콜백이 있습니다. pub/sub 패턴을 사용하여 이전 코드를 Wisper를 사용하는 다음과 같이 리팩토링할 수 있습니다.

 # app/models/post.rb class Post # ... field: content, type: String # ... # no callbacks in the models! end

게시자 는 필요할 수 있는 이벤트 페이로드 개체와 함께 이벤트를 게시합니다.

 # app/controllers/api/v1/posts_controller.rb # corresponds to the publisher in the previous figure class Api::V1::PostsController < Api::V1::ApiController include Wisper::Publisher # ... def create @post = current_user.posts.build(post_params) if @post.save # Publish event about post creation for any interested listeners publish(:post_create, @post) render_created(@post) else # Publish event about post error for any interested listeners publish(:post_errors, @post) render_unprocessable_entity(@post.errors) end end # ... end

구독자 는 응답하려는 이벤트에만 구독합니다.

 # app/listener/feed_listener.rb class FeedListener def post_create(post) Feed.create!(post) end end
 # app/listener/user_listener.rb class UserListener def post_create(post) User::NotifyFollowers.call(self) end end

이벤트 버스 는 시스템에 다른 가입자를 등록합니다.

 # config/initializers/wisper.rb Wisper.subscribe(FeedListener.new) Wisper.subscribe(UserListener.new)

이 예에서 pub-sub 패턴은 Post 모델에서 콜백을 완전히 제거하고 모델이 서로에 대한 최소한의 지식으로 서로 독립적으로 작동하도록 하여 느슨한 결합을 보장합니다. 동작을 추가 작업으로 확장하는 것은 원하는 이벤트에 연결하기만 하면 됩니다.

단일 책임 원칙(SRP)

단일 책임 원칙은 깨끗한 코드 기반을 유지하는 데 정말 유용합니다. 그것을 고수할 때의 문제는 때때로 학급의 책임이 생각만큼 명확하지 않다는 것입니다. 이것은 MVC(예: Rails)와 관련하여 특히 일반적입니다.

모델 은 지속성, 연관 등을 처리해야 합니다.

컨트롤러 는 사용자 요청을 처리하고 비즈니스 로직(서비스 개체)을 감싸는 래퍼여야 합니다.

서비스 개체 는 비즈니스 논리의 책임 중 하나를 캡슐화하거나 외부 서비스에 대한 진입점을 제공하거나 모델 문제에 대한 대안으로 작동해야 합니다.

결합을 줄이는 능력 덕분에 pub-sub 디자인 패턴은 SRSO(단일 책임 서비스 개체)와 결합되어 비즈니스 로직을 캡슐화하고 비즈니스 로직이 모델이나 컨트롤러에 침투하는 것을 방지할 수 있습니다. 이렇게 하면 코드 기반을 깨끗하고 읽기 쉽고 유지 관리 및 확장할 수 있습니다.

다음은 pub/sub 패턴 및 서비스 개체를 사용하여 구현된 몇 가지 복잡한 비즈니스 논리의 예입니다.

발행자

 # app/service/financial/order_review.rb class Financial::OrderReview include Wisper::Publisher # ... def self.call(order) if order.approved? publish(:order_create, order) else publish(:order_decline, order) end end # ...

구독자

 # app/listener/client_listener.rb class ClientListener def order_create(order) # can implement transaction using different service objects Client::Charge.call(order) Inventory::UpdateStock.call(order) end def order_decline(order) Client::NotifyDeclinedOrder(order) end end

게시-구독 패턴을 사용하면 코드 기반이 거의 자동으로 SRSO로 구성됩니다. 또한 복잡한 워크플로를 위한 코드 구현은 가독성, 유지 관리 가능성 또는 확장성을 희생하지 않고 이벤트를 중심으로 쉽게 구성됩니다.

테스트

뚱뚱한 모델과 컨트롤러를 분해하고 많은 SRSO를 사용하면 코드 기반 테스트가 훨씬 훨씬 쉬운 프로세스가 됩니다. 통합 테스트 및 모듈 간 통신의 경우 특히 그렇습니다. 테스트는 단순히 이벤트가 올바르게 게시되고 수신되었는지 확인해야 합니다.

Wisper에는 다양한 구성 요소의 테스트를 용이하게 하기 위해 RSpec 매처를 추가하는 테스트 젬이 있습니다.

앞의 두 가지 예( Post 예 및 Order 예)에서 테스트에는 다음이 포함되어야 합니다.

발행인

 # spec/service/financial/order_review.rb describe Financial::OrderReview do it 'publishes :order_create' do @order = Fabricate(:order, approved: true) expect { Financial::OrderReview.call(@order) }.to broadcast(:order_create) end it 'publishes :order_decline' do @order = Fabricate(:order, approved: false) expect { Financial::OrderReview.call(@order) }.to broadcast(:order_decline) end end

구독자

 # spec/listeners/feed_listener_spec.rb describe FeedListener do it 'receives :post_create event on PostController#create' do expect(FeedListner).to receive(:post_create).with(Post.last) post '/post', { content: 'Some post content' }, request_headers end end

그러나 게시자가 컨트롤러인 경우 게시된 이벤트를 테스트하는 데 몇 가지 제한 사항이 있습니다.

더 나아가고 싶다면 페이로드도 테스트하면 더 나은 코드 기반을 유지하는 데 도움이 될 것입니다.

보시다시피 pub-sub 디자인 패턴 테스트는 간단합니다. 다양한 이벤트가 올바르게 게시되고 수신되는지 확인하는 문제일 뿐입니다.

성능

이것은 더 많은 가능한 이점입니다. 게시-구독 디자인 패턴 자체는 코드 성능에 큰 영향을 미치지 않습니다. 그러나 코드에서 사용하는 모든 도구와 마찬가지로 게시/구독 구현을 위한 도구는 성능에 큰 영향을 미칠 수 있습니다. 때로는 나쁜 영향이 될 수도 있지만 때로는 매우 좋을 수도 있습니다.

첫째, 나쁜 영향의 예: Redis는 "고급 키-값 캐시 및 저장소입니다. 흔히 데이터 구조 서버라고 합니다.” 이 인기 있는 도구는 pub/sub 패턴을 지원하며 매우 안정적입니다. 그러나 원격 서버(Rails 애플리케이션이 배포된 동일한 서버가 아님)에서 사용하는 경우 네트워크 오버헤드로 인해 성능이 크게 저하됩니다.

반면 Wisper는 wisper-celluloid, wisper-sidekiq 및 wisper-activejob과 같은 비동기식 이벤트 처리를 위한 다양한 어댑터를 제공합니다. 이러한 도구는 비동기 이벤트 및 스레드 실행을 지원합니다. 적절하게 적용하면 애플리케이션의 성능을 크게 높일 수 있습니다.

결론

성능 면에서 추가 마일을 목표로 하는 경우 pub/sub 패턴이 도달하는 데 도움이 될 수 있습니다. 그러나 이 Rails 디자인 패턴으로 성능 향상을 찾지 못하더라도 코드를 정리하고 유지 관리하기 쉽게 만드는 데 여전히 도움이 됩니다. 결국, 유지될 수 없거나 애초에 작동하지 않는 코드의 성능에 대해 누가 걱정할 수 있겠습니까?

Pub-Sub 구현의 단점

모든 것과 마찬가지로 pub-sub 패턴에도 몇 가지 가능한 단점이 있습니다.

느슨한 결합(유연하지 않은 의미론적 결합)

pub/sub 패턴의 가장 큰 장점은 가장 큰 단점이기도 합니다. 게시된 데이터의 구조(이벤트 페이로드)는 잘 정의되어야 하며 빠르게 유연성이 없어집니다. 게시된 페이로드의 데이터 구조를 수정하려면 모든 구독자에 대해 알아야 하고 또한 수정하거나 수정 사항이 이전 버전과 호환되는지 확인해야 합니다. 이로 인해 게시자 코드의 리팩토링이 훨씬 더 어려워집니다.

이것을 피하려면 게시자의 페이로드를 정의할 때 각별히 주의해야 합니다. 물론 앞서 언급한 대로 페이로드를 테스트하는 훌륭한 테스트 제품군이 있다면 게시자의 페이로드 또는 이벤트 이름을 변경한 후 시스템이 다운되는 것에 대해 크게 걱정할 필요가 없습니다.

메시징 버스 안정성

게시자는 구독자의 상태를 알지 못하며 그 반대의 경우도 마찬가지입니다. 간단한 게시/구독 도구를 사용하면 메시징 버스 자체의 안정성을 보장하고 게시된 모든 메시지가 올바르게 큐에 대기되고 전달되는지 확인하는 것이 불가능할 수 있습니다.

교환되는 메시지의 수가 증가하면 간단한 도구를 사용할 때 시스템이 불안정해지며 좀 더 정교한 프로토콜 없이는 모든 가입자에게 전달을 보장하는 것이 불가능할 수 있습니다. 교환되는 메시지 수와 달성하려는 성능 매개변수에 따라 RabbitMQ, PubNub, Pusher, CloudAMQP, IronMQ 또는 기타 여러 대안과 같은 서비스 사용을 고려할 수 있습니다. 이러한 대안은 추가 기능을 제공하며 더 복잡한 시스템의 경우 Wisper보다 안정적입니다. 그러나 구현하려면 몇 가지 추가 작업이 필요합니다. 여기에서 메시지 브로커의 작동 방식에 대해 자세히 알아볼 수 있습니다.

무한 이벤트 루프

시스템이 완전히 이벤트에 의해 구동되는 경우 이벤트 루프가 발생하지 않도록 각별히 주의해야 합니다. 이러한 루프는 코드에서 발생할 수 있는 무한 루프와 같습니다. 그러나 미리 감지하기가 더 어려우며 시스템을 정지시킬 수 있습니다. 시스템 전반에 걸쳐 많은 이벤트가 게시되고 구독되는 경우 통지 없이 존재할 수 있습니다.

Rails 튜토리얼 결론

게시-구독 패턴은 모든 Rails 문제와 코드 냄새에 대한 은총알은 아니지만 서로 다른 시스템 구성 요소를 분리하고 유지 관리, 읽기 및 확장성을 높이는 데 도움이 되는 정말 좋은 디자인 패턴입니다.

SRSO(단일 책임 서비스 개체)와 결합되면 pub-sub는 비즈니스 논리를 캡슐화하고 다양한 비즈니스 문제가 모델이나 컨트롤러에 침투하는 것을 방지하는 데 실제로 도움이 될 수 있습니다.

이 패턴을 사용한 후의 성능 향상은 주로 사용 중인 기본 도구에 따라 다르지만 성능 향상은 경우에 따라 크게 향상될 수 있으며 대부분의 경우 확실히 성능을 저하시키지 않습니다.

그러나 pub-sub 패턴의 사용은 신중하게 연구하고 계획해야 합니다. 느슨한 결합의 강력한 힘에는 느슨하게 결합된 구성 요소를 유지 관리하고 리팩토링하는 큰 책임이 따르기 때문입니다.

이벤트가 통제를 벗어나기 쉽기 때문에 단순한 pub/sub 라이브러리는 메시지 브로커의 안정성을 보장하지 못할 수 있습니다.

마지막으로 너무 늦을 때까지 눈에 띄지 않는 무한 이벤트 루프를 도입할 위험이 있습니다.


저는 이 패턴을 거의 1년 동안 사용해 왔으며, 이 패턴 없이 코드를 작성하는 것은 상상하기 어렵습니다. 저에게 백그라운드 작업, 서비스 개체, 관심사, 컨트롤러 및 모델이 모두 서로 깨끗하게 통신하고 매력처럼 함께 작동하게 하는 것은 접착제입니다.

이 코드를 검토하면서 내가 배운 것만큼 많이 배웠으면 하고 Publish-Subscribe 패턴에 Rails 애플리케이션을 훌륭하게 만들 수 있는 기회를 제공하는 데 영감을 받았으면 합니다.

마지막으로 Wisper를 구현한 그의 멋진 작업에 대해 @krisleech에게 큰 감사를 드립니다.