Rails의 concern은 여러 해 동안 논란의 중심에 서 있었다. 만병통치약인가, 아니면 어떤 대가를 치르더라도 피해야하나? 내 생각에 문제는 우리가 원하는대로 그걸 사용할 수 있다는 것이다. 그래서 우리 스스로 발등을 찍는다해도 놀랄 일은 아니다. 결국 concern
은 그냥 평범한 루비 mixin일 뿐이기 때문이다. 단지 보일러플레이트를 없애기 위해 조금의 문법적 설탕을 첨가했을 뿐이다.
37signals는 거대한 레일즈 코드베이스에서 여러 해 동안 concern을 사용해왔다. 그래서 이 글에서 몇 가지 설계 지침을 공유하려고 한다.
concern
을 둬야할까루비는 다중상속의 대안으로 믹스인을 제시한다. 클래스 전반에 걸쳐 코드를 재사용하는 방법으로 말이다. 물론 concern을 이런 방식으로 사용할 수도 있다. 하지만 좀 더 일반적인 경우는 하나의 모델에 대한 코드를 정리하기 위해 사용하는 경우다. 이 두 가지 경우에 대해 우리는 각기 다른 규칙을 사용한다.
app/models/concerns
app/models/<model_name>
Basecamp에서 가져온 예시로, 하나의 모델에 종속되는 경우다.
# app/models/recording.rb
class Recording < ApplicationRecord
include Completable
end
# app/models/recording/completable.rb
module Recording::Completable
extend ActiveSupport::Concern
end
이 규칙을 따르면 Concern
을 include할 때 namespace를 반복하지 않아도 된다.
컨트롤러의 경우 상황이 반전된다. controller/concerns
폴더에 대부분의 concern이 위치하게 된다. 특정 하위 시스템에 적용하는 concern은 controller/concerns/<subsystem>
으로 명명한 폴더에 배치한다. 우리가 컨트롤러를 다루는 방식은 다른 글에서 이야기하겠다.
concern에 대한 일반적인 비판은 코드가 읽기 힘들어진다는 것이다. 나는 오히려 그 반대라고 생각한다. 올바르게 사용하면 두 가지 방법으로 가독성을 개선한다.
첫째, 복잡성을 관리하는데 도움을 준다. 복잡한 시스템을 다루는 것의 본질은 복잡한 것을 좀 더 작은 조각으로 나누어 한 번에 한 가지에 집중할 수 있도록 하는 것이다. concern은 정확히 그걸 위해 사용할 수 있는 도구 중 하나다.
핵심은 각 concern이 모델의 한 가지 특성을 잘 응집한 단위여야 한다는 것이다. 다른 말로, 관련있는 것들만 담겨있어야한다. concern을 큰 모델을 더 작은 부분들로 쪼개기 위한 어떤 임의의 행동과 구조 단위로 다루어서는 안 된다. 상속이 "is a" 관계를 가지듯, "has trait"이나 "acts as"와 같은 의미론을 가져야 한다. 그렇지 않다면 득보다 실이 커지게 된다.
전에 말했던 HEY screener 시스템을 예로 들어보자. HEY의 사용자는 그에게 메일을 보내려는 다른 이의 통관 심사관 역할을 한다.
class User < ApplicationRecord
include Examiner
end
module User::Examiner
extend ActiveSupport::Concern
included do
has_many :clearances, foreign_key: "examiner_id", class_name: "Clearance", dependent: :destroy
end
def approve(contacts)
...
end
def has_approved?(contact)
...
end
def has_denied?(contact)
...
end
...
end
이 concern은 통관 심사원이라는 도메인 역할과 일치하며 이에 관련한 코드만 포함한다. 이를 통해 유지관리가 쉬워진다. 순간순간 다루어야할 개념이 적어질수록 이해하기 쉬워진다.
둘째, concern은 도메인 개념을 반영하기 위한 추가 추상화를 제공한다.
다음은 HEY의 Topic
모델이 포함하는 concern들이다. examiner
예시와 마찬가지로 이 이름들이 도메인을 얼마나 잘 담아내고 있는지 확인해보자. 도메인을 좀 더 잘 담아낼 수 있도록하고, 이는 가독성 측면에서 긍정적이다.
class Topic < ApplicationRecord
include Accessible, Breakoutable, Deletable, Entries, Incineratable,
Indexed, Involvable, Journal, Mergeable, Named, Nettable, Notifiable,
Postable, Publishable, Preapproved, Collectionable, Recycled,
Redeliverable, Replyable, Restorable, Sortable, Spam, Spanning
...
Rails Concern에 대해 흔히 하는 오해는 이것이 클래스 상속이나 구성과 같은 전통적인 객체 지향 기법의 대안이라는 것이다. 이 글의 다음 문구를 보자.
비즈니스 논리는 우려보다는 추상화(클래스)로 모델링하는 것이 좋습니다. 개체, 서비스, 리포지토리, Aggregate 또는 보다 적합한 모든 아티팩트를 사용하십시오.
아니면 이 글.
조합을 선호하라.
한 파일에 모든 걸 넣어야 한다고 말하는 것이 아니다. 그냥 평범한 클래스에 논리를 추출하여 사용해라.
나는 이게 잘못된 이분법이라고 생각한다. concern을 사용한다고 해서 시스템 설계의 중요성이 제한되거나 대체되는 것은 아니다. 적절한 책임을 가진 객체 시스템으로 만들어야지, 단순히 뚱뚱한 액티브레코드 모델을 보기좋게 정리하려는 용도로 사용해서는 안 된다. 처음에 나는 concern을 사용할 때 그런 난장판을 만들었기때문에 그게 실재하는 위험이라는 걸 안다.
37signals는 오래된 객체지향 설계, 상속과 조합, 디자인패턴에 기반하고 있고 models
폴더에 수많은 PORO
들이 자리잡고 있다. concern은 이런 접근방법과 정말 잘 어울린다. 간단한 예시와 함께 보자.
HEY는 구독을 취소한 유료 고객들의 이메일 주소도 영원히 남겨 다른 사람이 사용할 수 없도록 한다. 따라서 구독이 종료될 때 고객은 모든 데이터를 완전히 삭제(incineration
)하거나 아웃바운드 포워딩 같은 최소한의 기능만 유지하는 것(purging
) 둘 중 하나를 선택할 수 있다. 관련한 코드 조각을 보자.
class Account < ApplicationRecord
include Closable
end
module Account::Closable
def terminate
purge_or_incinerate if terminable?
end
private
def purge_or_incinerate
eligible_for_purge? ? purge : incinerate
end
def purge
Account::Closing::Purging.new(self).run
end
def incinerate
Account::Closing::Incineration.new(self).run
end
end
incineration
과 purging
은 몇 가지 코드를 공유하는 작업을 포함한다. 어떻게 해결 할 수 있을까? 공통 동작을 캡슐화하는 클래스와 이것들을 재사용할 수 있는 우리의 오랜 친구, 상속을 이용하면 된다.
나는 concern을 사용해 도메인 중심의 깔끔한 API를 제공하는 방식을 좋아한다. 이 API를 부르는 입장에서는 이 뒤에 숨겨진 복잡한 하위 시스템을 이해할 필요가 없다. 계정을 종료하고 싶다면, 단순히 다음과 같이 사용하면 된다.
account.terminate
좀 더 장황하고 덜 유창하게 표현하려면 다음과 같이 할 수 있다.
AccountTerminationService.new(account).run
계정을 종료하는 두 가지 절차를 모두 담당하는 뚱뚱한 계정 모델이 없다는 사실에 주목하자. 대신 이를 담당하는 세 개의 하위 시스템이 있고 계정 모델은 단순히 이를 사용하기 위한 문을 내어줄 뿐이다.
concern은 시스템 설계에 관련한 것들을 희생하지 않으면서도 더 간결하고 좋아보이는 API를 제공할 수 있도록 한다.
Concern은 도구다. 날이 잘 서 있는지, 너무 무딘지는 모르겠지만 잘못 사용했을 때 문제를 일으킬 수 있다. 하지만 레일즈프로그래머로서 간단한 가이드라인만 있다면 사용할 수 있는 매우 훌륭한 도구이다.
좋은 객체 지향 디자인과 concern은 매우 잘 어울리는 한 쌍이다. 당연히 concern은 좋은 소프트웨어를 설계하는 방법을 배울 필요성을 없애는 게 아니다. 코드 구성을 개선해 보다 이해하기 쉬고 유지관리가 가능하케 하는 실용적인 매커니즘이다.
혹자들은 바닐라 레일즈는 한계가 있고 작동하며 더 나아가기 위해서는 추가적인 무언가가 필요하다는 말을 한다. 그렇다면, 베이스캠프와 HEY는 전통적인 객체지향과 그 패턴들을 사용하는 바닐라 레일즈 애플리케이션일 것이고, 그 안에 상당한 concern들을 찾아볼 수 있을 것이다.
Photo by Vardan Papikyan on Unsplash
원문은 https://world.hey.com/jorge/code-i-like-iii-good-concerns-5a1b391c에서 확인할 수 있고, 원 저자분의 새로운 글을 구독할 수도 있습니다.