원 저자인 Jorge Manrubia 씨의 허락을 얻어 번역합니다. 원문은 https://world.hey.com/jorge/code-i-like-ii-fractal-journeys-b7688f93 에서 확인하실 수 있습니다.
프랙탈은 점진적으로 더 작은 규모로 반복되는 유사한 패턴에 관한 것이다. 나에게 좋은 코드는 프랙탈이다. 좋은 코드에서는 마치 프랙탈처럼 다른 수준의 추상화에서 반복되는 동일한 특성을 관찰할 수 있다.
그닥 놀랍지는 않다. 좋은 코드는 이해하기 쉬운 코드고, 복잡성을 다루는 가장 좋은 방법은 추상화기 때문이다. 이를 통해 우리는 복잡성을 알기 쉬운 인터페이스로 바꿀 수 있다. 하지만, 이제 우린 또 다시 우리가 밀어낸 복잡성과 싸워야 한다. 그러기 위해 아까의 동작을 다시 반복한다. 새로운 추상화를 통해 속내용을 가리고 이 복잡한 동작의 조감도를 제공한다.
나는 추상화를 모든 것을 언급하기 위해 사용하고 있다. 큰 하위 시스템에서부터 일부 내부 클래스의 마지막 Private method에 이르기까지. 하지만 어떻게 이런 추상화를 만들 수 있을까? 굉장히 중요하나 대답하기는 어렵다. 수없이 많은 책들이 이를 다루고 있음이 방증한다. 이 글에서는 이해하기 쉬운 코드를 짜는데 필수적이라고 생각하는 네 가지 자질에 초점을 맞추도록 하겠다.
글이 지나치게 추상적인 방향으로 흘러가는 것 같다. Basecamp의 코드를 보며 현실에 대해 좀 얘기를 해보자. 제품의 여러 부분에서 활동 타임라인을 제공한다. 이 타임라인은 알아서 새로고침이 된다. 사용자가 이걸 보고 있을 때 다른이가 활동을 하면 실시간으로 업데이트된다는 뜻이다.
도메인 수준에서 이야기를 해보자. 누군가 Basecamp에서 todo를 완료한다든지 문서를 만든다든지 댓글을 다는 등의 활도을 하면, 시스템은 이벤트를 만들고, 이 이벤트는 여러 목적지로 실어날라진다. 목적지는 활동 타임라인이나 webhook 등이다. 코드를 들여다보자.
가장 먼저 Event
모델이 있다. 모델은 Relaying
concern을 include 한다. (관련한 부분만 제시)
class Event < ApplicationRecord
include Relaying
end
이 concern은 relays
와 관계를 만들고 생성 후에 비동기 relay event를 만든다.
module Event::Relaying
extend ActiveSupport::Concern
included do
after_create_commit :relay_later, if: :relaying?
has_many :relays
end
def relay_later
Event::RelayJob.perform_later(self)
end
def relay_now
...
end
end
class Event::RelayJob < ApplicationJob
def perform(event)
event.relay_now
end
end
따라서 Event#relay_now
가 우리가 관심있는 메서드이다. 도메인 언어로 쓰여있다. 호출하는 관점에서 하나의 일만을 하며 이벤트 릴레잉과 관련한 모든 것을 숨기고 있다. 좀 더 파고들어가자.
module Event::Relaying
def relay_now
relay_to_or_revoke_from_timeline
relay_to_webhooks_later
relay_to_customer_tracking_later
if recording
relay_to_readers
relay_to_appearants
relay_to_recipients
relay_to_schedule
end
end
end
이 메서드는 더 낮은 추상화 수준을 가진 메서드들의 호출을 다시 부른다. 모두가 Relaying에 관한 것으로 응집성을 위배하지 않는다. 도메인에 기반하여 목적지에 대해 명확한 이름을 갖고 잇다. 역시 세부사항은 숨겨져 있고 여기서 또 다시 프랙탈의 한 조각을 발견할 수 있다. 이 메서드가 무엇을 하는지 알기 위해 추상화 수준을 뛰어넘을 이유는 없다.
#relay_to_or_revoke_from_timeline
메서드가 우리가 찾는 것으로 보인다.
module Event::Relaying
private
def relay_to_or_revoke_from_timeline
if bucket.timelined?
::Timeline::Relayer.new(self).relay
::Timeline::Revoker.new(self).revoke
end
end
end
도메인에 뿌리를 둔 좋은 이름들을 다시 볼 수 있다. bucket이 timelined
인지 확인한 후, Timeline::Relayed
객체를 만들어 타임라인에 이벤트를 전달한다. 대칭성에 주목하라. 이벤트를 취소하는, 대칭쌍을 이루는 클래스가 있다. relay
와 timeline
에 대해 초점을 맞추고 있고 세부적인 구현은 숨겨져 있다. 이 클래스를 좀 더 파보도록 하자.
class Timeline::Relayer
def initialize(event)
@event = event
end
def relay
if relaying?
record
broadcast
end
end
private
attr_reader :event
delegate :bucket, to: :event
def record
bucket.record Relay.new(event: event), parent: timeline_recording, visible_to_clients: visible_to_clients?
end
def broadcast
TimelineChannel.broadcast_event(event, to: recipients)
end
end
이번에는 메서드가 아니고 순수한 루비 클래스를 이용해 추상화를 했지만, 같은 특질들을 볼 수 있다. 밖으로는 메서드 #relay
만을 노출하며 구현의 세부사항들을 숨기고 있다. 안을 들여다보면, 두 개의 연산을 확인할 수 있다. relay
를 데이터베이스에 저장하고 Action Cable을 통해 broadcast하는 것이다. (이 코드는 Hotwire 이전에 작성한 것이다.) 다시 대칭성에 줌족하라. 두 작업 모두 한 줄짜리 코드로 표현할 수 있지만, 더 높은 수준의 메서드로 추출했다.
마침내 저수준의 구현까지 도달했다. #record
메서드는 relay
를 DB에 저장한다. relay는 recordable
이고, Rails의 delegated type을 사용한 예시이다. #broadcast
는 event를 시작할 때 관심을 가졌던 수신자에게 broadcast하는 방식이다.
이 예에서는 이벤트가 생성되는 순간부터 액션 케이블 채널을 통해 푸시될 때까지 릴레이 논리를 쉽게 이해할 수 있었다. 각 단계에서 관심사가 하나뿐이었기 때문이다. 하나의 책임과 단일한 수준의 추상화, 그리고 우리의 도메인을 반영하는 이름들. 물론 좋은 코드를 구성하는 것은 주관적이고 더 많은 개념을 포함한다. 하지만 사소하지 않은 규모의 시스템에서 이런 여행을 할 수 있는 능력은 내가 좋아하는 코드의 첫번쨰 자질이다.
이 시리즈의 다른 글:
수년 전에 작성한 composed method implementation pattern에서도 이러한 취지를 확인할 수 있다. 가장 좋은 점은 저기 참조된 두 권의 책인데, 이 주제에 관심이 있다면 꼭 읽어보길 권한다.
Photo by Martin Rancourt on Unsplash
많은 도움 되었습니다. 감사합니다.