decent exposure
디자인 패턴에 관심이 있는 분들은 MVC(Model-View-Controller)와 MVP(Model-View-Presenter)에 대해 들어보셨을 겁니다. 그리고 이보다 조금 더 관심을 가지고 계신 분들이 들어보셨을 법 한 패턴이 하나 더 있습니다. 바로 MVVM(Model-View-ViewModel)이죠.
MVVM은 마틴 파울러가 주장한 설계 패턴 중 하나인데, 레일즈 개발자가 이해하기 쉽도록 축약하자면 "웹 서버에서 받는 요청들의 대부분은 컨트롤러를 거칠 필요가 없다." 라는 이론에 기인합니다. 그래서 컨트롤러를 대신해서 모델을 뷰에 맞게 2차 가공한 일명 뷰모델(ViewModel)이라는 게 존재하고, 이 뷰모델이 컨트롤러를 대신하는 원리이죠.
오늘 소개할 decent_exposure는 이 MVVM의 개념을 레일즈에 적용한 젬 중 하나입니다. 다음의 예를 살펴보죠.
class ProductsController < ApplicationController expose(:product) # ... end
위 코드는 decent_exposure의 expose라는 메소드를 호출한 상품(Product) 컨트롤러입니다.
언뜻 보면 별 내용이 없지만, 코드를 이해하기 쉽도록 풀어보겠습니다.
class ProductsController < ApplicationController def product @product ||= params[:id] ? Product.find(params[:id]) : Product.new(params[:product]) end helper_method :product hide_action :product # ... end
product라는 특이한 메소드가 하나 있고, 이 메소드는 뷰를 위한 헬퍼로 등록되어 있습니다.
언뜻 쓸모 없어보이는 이 메소드는, 그러나 굉장히 유용하며 다양한 기능을 포함하고 있는 마법 상자와도 같습니다. 이제 컨트롤러 코드를 고쳐볼까요?
# Before class ProductsController < ApplicationController def show @product = Product.find(params[:id]) end def new @product = Product.new end def edit @product = Product.find(params[:id]) end def create @product = Product.new(params[:product]) if @product.save redirect_to @product, notice: 'Product was successfully created.' else render action: "new" end end def update @product = Product.find(params[:id]) if @product.update_attributes(params[:product]) redirect_to @product, notice: 'Product was successfully updated.' else render action: "edit" end end def destroy @product = Product.find(params[:id]) @product.destroy redirect_to products_url end end
이제 expose를 이용해 보겠습니다. 코드는 아래와 같이 바뀝니다.
# After class ProductsController < ApplicationController expose :product def create if product.save redirect_to products_path, :notice => "Successfully created product." else render :new end end def update if product.save redirect_to products_path, :notice => "Successfully updated product." else render :edit end end def destroy product.destroy redirect_to products_url, :notice => "Successfully destroyed product." end end
코드가 줄어들었죠? 이해하기 어렵다면 맨 위에 있는 expose의 해석본과 함께 살펴보세요. 뷰에서는 @product대신 product를 이용하면 이전 코드와 동일한 역할을 수행합니다.
이처럼 우리 작성하는 레일즈 액션 중에서 컨트롤러를 거치지 않고 바로 뷰로 넘겨도 문제없는 액선들이 다수 존재합니다. decent_exposure는 이러한 코드들의 중복 요소를 제거하는데 매우 유용하죠.
다음은 decent_exposure의 위키에 있는 또 다른 예제입니다.
# Before class PeopleController < ApplicationController respond_to :html, :xml, :json before_filter :get_company def index @people = @company.people.where(params.except(:action, :controller, :format)) end def show @person = @company.people.find(params[:id]) end def new @person = @company.people.build end def edit @person = @company.people.find(params[:id]) end def create @person = @company.people.build(params[:person]) @person.save respond_with(@person) end def update @person = @company.people.find(params[:id]) @person.update_attributes(params[:person]) respond_with(@person) end def destroy @person = @company.people.find(params[:id]) @person.destroy respond_with(@person) end private def get_company @company = Company.find(params[:company_id]) end end
# After class PeopleController < ApplicationController respond_to :html, :xml, :json expose(:company) { Company.find(params[:company_id]) } expose(:people) do company.people.where(params.except(:action, :controller, :format)) end expose(:person) def create person.save respond_with(person) end def update person.save respond_with(person) end def destroy person.destroy respond_with(person) end end
매우 쉽고 간편하죠? 레일즈 코드를 RESTful하게 작성했다면 지금 당장이라도 decent_exposure를 이용해 프로젝트의 코드 규모를 줄일 수 있습니다.
하지만 decent_exposure는 레일즈 커뮤니티에서도 많은 논란이 있는 젬입니다. 일단 MVVM의 본질적인 부분을 다루지 못했다는 의견이 첫번째, 그리고 레일즈의 내부 구현상 MVC 외의 패턴(이는 MVP와 MVVM 모두를 가리킵니다)을 완벽하게 적용하기 어렵다는 의견이 두번째입니다.
그렇기에 decent_exposure를 사용할 때 이를 유념하고 이용해 주세요.
-
- 출처 & 관련자료 -
GitHub : voxdolo/decent_exposure
Wikipedia : Model View ViewModel
RailsCasts : Decent Exposure











