Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

루비(Ruby)에서 정크 드로어 클래스 피하는 법: 데이터와 로직의 분리

루비(Ruby)는 객체지향 언어이기 때문에 우리는 흔히 세상을 객체들의 집합으로 모델링하곤 합니다. 예컨대 두 개의 정수 xy를 묶어 Point라고 부르고, Line은 그런 Point 두 개로 이루어져 있다고 정의하는 식입니다.

이 접근 방식은 분명 유용하지만, 한 가지 큰 문제를 안고 있습니다. 바로 데이터에 대한 하나의 해석을 다른 모든 해석보다 우선시한다는 점입니다. x와 y는 언제나 Point일 것이라고 가정하며, 이들이 Cell이나 Vector로 동작해야 할 일은 애초에 없을 거라 전제하는 것이죠.

그렇다면 정말 Cell이 필요해지면 어떻게 될까요? 데이터의 소유권은 Point에 있으므로, 어쩔 수 없이 Cell 관련 메서드를 Point에 추가하게 됩니다. 시간이 지나면 결국 서로 느슨하게 연관된 메서드들을 마구 담아 둔, 일종의 '서랍장' 같은 Point 클래스가 탄생합니다.

이런 정크 드로어(junk drawer) 클래스는 너무 흔해서 우리는 그것을 불가피한 것으로 받아들이고, 개발 과정에 '리팩토링' 단계를 덧붙여 완화하곤 합니다. 하지만 처음부터 이 문제를 예방할 수 있다면 어떨까요?

웹 개발의 첫 번째 규칙: User 클래스에 대해 이야기하지 않는다

실전에서 오랜 기간 운영된 Rails 애플리케이션의 심장부에는 언제나 User라는 이름의 거대한 괴물 같은 클래스가 자리하고 있습니다.

모든 것은 무해하게 시작됩니다. 사용자 로그인 기능이 필요하고, username과 password를 저장해야 하니 클래스를 하나 만들면 됩니다.

class User
  attr_accessor :username, :password, :email, :address

  def authenticate!(password)
    ...
  end
end

그런데 이게 워낙 잘 동작하다 보니, 어느 순간 사람들이 당신에게 돈을 내고 싶어 합니다. "좋아," 우리는 말하죠. "User는 사실 Subscriber랑 거의 같은 거니까 속성 몇 개와 메서드 몇 개만 추가하자."

class User
    ...

    attr_accessor :payment_processor_token, :subscription_plan_id, ...etc

    def charge_cc
      ...
    end
end

훌륭합니다! 이제 진짜 돈이 굴러 들어옵니다! 그러던 중 CEO가 영업팀이 SalesForce에 쉽게 가져갈 수 있도록 사용자 연락처 정보를 VCard로 내보내는 기능을 원한다고 결정했습니다. vim을 열 시간입니다.

class User
    ...

    def export_vcard
      ...
    end
end

우리는 대체 무엇을 한 걸까?

처음에는 오직 인증만을 담당하던 User 클래스였습니다. 그런데 여기에 메서드와 속성을 계속 추가한 결과, 이제 그것은 User/Subscriber/Contact가 뒤섞인 프랑켄슈타인 같은 하이브리드가 되어 버렸습니다.

왜 우리는 이런 짓을 하는 걸까요? 개발자로서 스스로를 존중하지 않는 걸까요? 아니면 애초에 이 데이터가 정말로 하나의 사물, 즉 객체라고 믿으면서 스스로 실패의 길을 열어 준 걸까요?

username, password, email 주소의 조합을 왜 User라고 부르는 걸까요? Subscriber라고 부르면 안 되는 걸까요? Contact는요? SessionHolder는요?

데이터의 본질은 어디까지나 데이터

데이터는 그저 데이터일 뿐입니다. 오늘은 그것을 남자 친구처럼 다뤄야 할 수 있고, 내일은 전 남자 친구처럼 다뤄야 할 수도 있습니다.

이것이 Elixir 같은 함수형 언어가 취하는 접근 방식입니다. 데이터는 루비의 해시, 배열, 문자열과 비슷한 단순한 구조로 저장됩니다. 데이터로 무언가를 하고 싶을 때는 그 데이터를 함수에 전달하고, 결과값은 함수가 반환합니다.

단순해 보이지만, 이 방식 덕분에 관심사를 서로 다른 모듈로 아주 쉽게 분리할 수 있습니다.

Elixir에서 User 시스템을 구성한다면 대략 이런 모습일 겁니다:

my_user = %{username: "foo", password: ..., phone: ..., payment_token: ...}
my_user = Authentication.authenticate(my_user)
my_user = Subscription.charge(my_user)
my_user = Contact.export_vcard(my_user)

데이터가 코드와 분리되어 있기 때문에, 어떤 모듈도 그 데이터에 대한 특권적인 해석권을 갖지 않습니다.

다시 루비로 돌아오면

이 접근 방식이 Elixir에서 이토록 잘 통하는데, 루비에도 도입하면 안 될 이유가 있을까요? User를 단순한 데이터 래퍼(wrapper)로 만들고, 모든 비즈니스 로직을 모듈로 빼내는 데는 아무런 장애물이 없습니다.

class User
  attr_accessor :username, :password, :email, :address
end

module Authentication
  def self.authenticate!(user)
    ..
  end
end

module Subscription
  def self.charge(user)
    ..
  end
end

module Contact
  def self.export_vcard(user)
    ..
  end
end

원한다면 User 클래스를 Struct로 만들거나, (어느 정도) 불변(immutable) 객체가 되도록 코드를 추가할 수도 있습니다.

그래서, 무슨 의미가 있는가?

너무 사소한 변화처럼 보이나요? 대체 요점이 뭘까요?

앞서 살펴본 것처럼, 전형적인 OO 방식은 시간이 지날수록 문제를 드러냅니다. 특정 클래스가 데이터를 소유한다고 선언하는 순간, 같은 데이터를 다른 방식으로 활용해야 할 때 곤란해집니다.

반면 모듈 방식에서는 새로운 동작을 추가하는 것이 전혀 문제가 되지 않습니다. 필요한 방식대로 데이터를 해석하는 새 모듈을 만들면 그뿐입니다. 데이터와 기능이 완전히 분리되어 있기 때문에 가능한 일입니다.

다른 접근 방식들

정크 드로어 문제를 막는 방법은 이것만 있는 건 아닙니다. 전통적으로 객체지향 프로그래밍에서는 상속과 신중한 리팩토링으로 이를 해결하려 했습니다. 2012년에는 Sandi Metz가 『Practical Object Oriented Design in Ruby』를 출간해 많은 개발자가 의존성 주입(dependency injection)으로 객체를 조합하기 시작했고, 더 최근에는 함수형 프로그래밍의 유행에 힘입어 루비스트들이 불변 '데이터 객체(data object)'를 실험하고 있습니다.

이 방법들은 모두 깔끔하고 아름다운 코드를 만드는 데 기여할 수 있습니다. 하지만 클래스가 일반적으로 데이터를 소유한다는 사실 자체는 변하지 않기 때문에, 클래스가 무엇인가와 클래스가 데이터로 무엇을 하느냐 사이의 긴장은 언제나 존재할 수밖에 없습니다.

저는 이런 접근 방식들이 정크 드로어 문제 회피에서 어느 정도 성공을 거두는 이유 역시, 결국 데이터를 코드로부터 분리(decoupling)하기 때문이라고 생각합니다.