티스토리 뷰

Ruby

[Ruby] View에 Decorator 패턴 적용하기

강씨아저씨 2018. 5. 7. 15:38


오늘의 주제는 Ruby On Rails 에 Decorator 패턴 적용하기 입니다. 

우선 Decorator 패턴을 적용하게 된 배경부터 설명하도록 하겠습니다.


개선과제 프로젝트 진행중 아래와 같은 구조의 코드를 발견했습니다. 

...
<% if @category == CATEGORY_A %>
    <div class="field">
        <%= f.label :title, '제목*', :class=>'v_center' %>
        <%= f.text_field :title, { required: true } %>
    </div>
<% elsif @category == CATEGORY_B %>
    <div class="field">
        <%= f.label :title, '제목', :class=>'v_center' %>
        <%= f.text_field :title %>
    </div>
<% end %>

<% if @category == CATEGORY_A || @category == CATEGORY_B %> <div class="field"> <%= f.label :name, '이름', :class=>'v_center' %> <%= f.text_field :name , placeholder: '#이름을 입력하세요'%> </div>

<% end %>

<% if @category == CATEGORY_C%> <div class="field"> <%= f.label :background_color, '배경색', :class=>'v_center' %> <%= f.text_field :background_color , placeholder: '#000000'%> </div> <% end %> <div class="field"> <%= f.label :uri, 'URI', :class=>'v_center' %>    <% if @category == CATEGORY_A || @category == CATEGORY_C %>         <%= f.text_field :image_url class: 'hidden indent_left' %>    <% elsif @category == CATEGORY_B %>         <%= f.text_field :image_url class: 'indent_left' %>    <% end %> </div> ...


위의 코드는 Type에 따라 다음과 같은 3가지 형태의 html 로 생성될 수 있습니다.


CATEGORY_A 일때
    제목(필수)
    이름
    URI(hidden)
CATEGORY_B 일때
    제목(선택)
    이름
    URI
CATEGORY_C 일때
    배경색
    URI(hidden)

코드를 보고 답답한 마음에 누가 이렇게 했나를 확인해보니 과거의 제가 이렇게 짰더군요....

그리고 그 당시에 상황을 회상해보니 '우선은 급하니까 이렇게 해놓고 나중에 리팩토링 해야지...' 라고 생각하고

그 후로 지금까지 한번도 수정을 못한 코드였습니다...


개선과제에 필요한 요구사항에 맞춰 코드를 살짝 수정하는거는 하루면 끝나는 작업이지만 

이후 제가 아닌 다른분이 이 코드를 보고 이해하려고 한다면 어떤 고생을 할까를 생각하니 그냥 둘 수 없었습니다. 

(나중에 다른분이 코드를 보고 속으로 욕할생각을 하면....)


그래서 이참에 이렇게 View 에서 경우에 따라 다른 결과를 보여줘야 할 때 유용한 방법에 대해서 찾다가

얼마전 회사에 동료분이 설명해주신 Decorator 패턴을 적용해 보면 어떨까 하고 적용을 해보게 되었습니다.


우선 Decorator Pattern 의 정의부터 확인해보겠습니다.

Decorator Pattern 이란 '주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴으로, 기능 확장이 필요할 때 서브클래싱 대신 쓸 수 있는 유연한 대안이 될 수 있다이라고 위키에 써있습니다.

https://ko.wikipedia.org/wiki/%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0_%ED%8C%A8%ED%84%B4


이를 짧게 설명하면 기존객체에 영향을 주지 않으면서 기능을 추가하고싶을때 사용되는 패턴입니다. 

그리고 제가 하고싶은 일은 기존객체(Model)에 영향을 주지 않으면서 기존객체와 연관된 View에서 경우에 따라 다른 결과를 보여주는 기능을 추가하고 싶었습니다. 


이러한 일을 하려면 아래와 같은 3가지 경우를 생각할 수 있습니다.

1. 기존처럼 html.erb 파일에 if else 에 따라 다른 동작을 하도록 로직을 추가한다. 

2. Rails 의 Helper 클래스를 활용한다. 

3. Decorator 클래스를 생성해서 해당 클래스를 사용한다. 


우선 1번의 경우 가장 단순하지만 가독성이 좋지않고 view 코드 내부에서 너무 많은 Ruby 로직이 들어가면서 복잡도가 증가하는 단점이 있습니다. 

그리고 2번은 Ruby 로직을 Helper로 옮겨서 복잡도를 줄인다는 장점이 있지만 Helper는 모든 View에서 접근 가능하므로 책임이 모호하고 인스턴스화 할 수 없다는 단점이 있습니다.

(https://stackoverflow.com/questions/45468184/decorators-vs-helpers-in-rails)

그래서 나온 대안이 3번인 Decorator 패턴입니다. 

Decorator는 기본적으로 Model을 Wrapping 하고 View의 Display를 위한 목적으로 생성됩니다. 그래서 원본 데이터의 값을 수정하지 않고 데이터를 정렬하거나 쉼표를 추가하거나 가격에 통화를 붙이는등의 객체의 데이터를 꾸미는 일을 합니다.

그리고 Model에서 Display를 위한 로직을 제거함으로써 Model이 원래의 목적과 맞지 않는 로직들로부터 오염되지 않도록 도와줍니다. 




(View와 Model이 직접관계를 갖고 있을때)



(View와 Model 사이에 Decorator가 추가되었을때)



Rails 에서 Decorator 패턴을 적용하는 방법은 여러가지가 있지만 저는 기존에 Rails 개발자들이 많이 추천하는 Draper 라는 Gem을 사용하였습니다. 

(https://github.com/drapergem/draper)


직접구현하지 않고 Gem을 사용한 이유는 Gem 이 지원하지 않는 특별한 기능이 필요하지도 않았고 바퀴의 재발명을 할 필요가 없다고 생각해서 입니다.

(https://zetawiki.com/wiki/%EB%B0%94%ED%80%B4%EC%9D%98_%EC%9E%AC%EB%B0%9C%EB%AA%85)


Decorator 패턴을 적용한 결과는 다음과 같습니다. 

...
<% f.object.category_a_or_b? %>
    <div class="field">
        <%= f.label :title, f.object.title_text, :class=>'v_center' %>
        <%= f.text_field :title. f.object.title_option %>
    </div>
<% end %>
		
<% f.object.category_c? %>
    <div class="field">
        <%= f.label :background_color, '배경색', :class=>'v_center' %>
        <%= f.text_field :background_color , placeholder: '#000000'%>
    </div>
<% end %>
		
<% f.object.category_a_or_b? %>
    <div class="field">
        <%= f.label :name, '이름', :class=>'v_center' %>
        <%= f.text_field :name , placeholder: '#이름을 입력하세요'%>
    </div>
<% end %>
		
<div class="field">
    <%= f.label :uri, 'URI', :class=>'v_center' %>
    <%= f.text_field :image_url, f.object.uri_option %>
</div>
...

Decorator 내부에서 다음과 같은 로직은 다음과 같이 변경되었습니다. 

class MyDecorator < Draper::Decorator
def category_a_or_b?
  [CATEGORY_A, CATEGORY_B].include?(self.category)
end

def category_c?
  [CATEGORY_C].include?(self.category)
end

def title_text
  if self.category == CATEGORY_A
    제목*
  elsif
    제목
  end
end

def title_option
  if self.category == CATEGORY_A
    { required: true }
  elsif
    { }
  end
end

def uri_option 
  option = 'indent_left'
  if [CATEGORY_A, CATEGORY_C].include?(self.category)
    option << ' hidden'
  end
end

....

end


Decorator를 사용함으로써 View내에 불필요한 로직들을 제거했고 Model과 View 사이에 Decorator를 넣음으로써 둘간의 관계를 느슨하게 한다는 이점을 얻었습니다. 


이상 Decorator를 사용하면서 경험한 내용들을 끄적였습니다..

누군가에게는 도움이되었기를 바라면서 오늘의 포스팅 끝~




댓글