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

데코레이터 vs 서브클래스: 어떤 상황에서 무엇을 써야 할까?

지난 글에서 Rails 5.1의 훌륭한 새 기능인 delegate_missing_to를 소개했습니다. delegate_missing_to를 사용하면 한 객체에서 찾을 수 없는 메서드가 다른 객체에서 대신 호출됩니다:

class Player
  delegate_missing_to :@user

  def initialize(user)
    @user = user
  end

  def points
    Game.points_for_user(@user.id)
  end
end

Player.new(user).name # user.name을 호출합니다

하지만 댓글에서 Gavin이 지적했듯이, 이는 상속을 피하기 위한 다소 이색적인 방법처럼 보입니다. 그냥 서브클래스를 사용하면 안 될까요? 같은 효과를 얻을 수 있고, 굳이 새로운 기능을 추가할 필요도 없어 보입니다.

그럼에도 delegate_missing_to가 추가된 데에는 분명한 이유가 있을 것입니다. Rails 기능의 경우, 풀 리퀘스트를 살펴보면 그 이유를 찾기에 좋습니다. 실제로 이 풀 리퀘스트에서 DHH는 자신이 이 기능을 제안한 이유를 밝혔습니다:

"데코레이터를 만들고 싶을 때 흔히 쓰이는 패턴입니다."

꽤 좋은 출발점이 되어 줄 것 같습니다.

왜 데코레이터인가?

데코레이터를 만들면 새로운 서브클래스를 생성하지 않고도 객체의 동작 방식을 바꿀 수 있습니다. 예를 들어 앞서 본 코드에서:

class Player
  delegate_missing_to :@user

  def initialize(user)
    @user = user
  end

  def points
    Game.points_for_user(@user.id)
  end
end

"Player가 User를 데코레이트한다"고 표현할 수 있습니다. Player는 User와 거의 비슷하게 동작하면서 추가로 points라는 메서드를 가지고 있기 때문입니다. 그리고 이 모든 것이 상속 없이 이루어집니다.

그런데 왜 이런 것이 필요할까요? 이 질문에 답하기는 생각보다 쉽지 않습니다. 많은 디자인 패턴이 그렇듯, 어떤 상황에서 다른 방법 대신 이것을 써야 하는지 명확하지 않은 경우가 많기 때문입니다.

언제 데코레이터를 사용할까?

데코레이터는 그저 더 복잡한 방식의 상속에 불과할 수도 있습니다. 다음 두 줄의 코드 중 어느 쪽이 더 나아 보이시나요?

player = Player.new(User.new(name: "Justin")) # Player가 User를 데코레이트
player = Player.new(name: "Justin")           # Player가 User를 상속

당연히 두 번째가 낫다고 느껴집니다. 이 경우 Player를 서브클래스가 아닌 데코레이터로 만드는 것은 그저 코드 낭비일 뿐입니다.

하지만 때로는 객체를 생성한 시점과 먼 위치에서, 나중에 객체에 기능을 추가하고 싶을 때가 있습니다. 예를 들어 다음과 같은 코드가 있다면 어떨까요?

user = User.find(1)

... 시간이 좀 지난 후 ...

player = Player.new(user)

이제 User 객체를 원하는 방식으로, 원하는 메서드 안에서 자유롭게 생성할 수 있습니다. User 객체를 생성하는 코드는 Player 클래스가 존재하는지조차 알 필요도, 신경 쓸 필요도 없습니다. 게다가 추가 메서드가 더 이상 필요 없다면 원래의 User 객체를 그대로 사용하면 됩니다.

이는 동작(behavior)을 여러 클래스로 분리하는 데 큰 도움이 됩니다. 각 클래스는 특정 상황에서 User 객체가 어떻게 사용되는지에만 집중할 수 있습니다 — Player, Employee, Developer처럼요. 상속을 사용하면 이런 역할들이 하나의 클래스에 쉽게 뒤섞여 버립니다.

MrChris는 댓글에서 데코레이터의 또 다른 장점을 짚어주었습니다:

객체를 데코레이트하면 해당 객체의 public 메서드만 호출할 수 있습니다. 반면 서브클래스를 만들면 private 메서드까지 포함해 어떤 메서드든 호출할 수 있습니다. 이 때문에 서브클래스는 부모 클래스의 구현 세부사항에 의도치 않게 의존하게 되어 더 자주 깨질 수 있습니다. 구현 세부사항은 보통 public 메서드보다 훨씬 자주 변경되기 마련입니다.

데코레이터는 거대한 클래스를 분해할 때 특히 유용합니다. 데코레이터를 활용하면 단일 책임 원칙(Single-Responsibility Principle)을 지키기가 훨씬 수월해집니다 — 각 데코레이터는 한 가지 일에 집중해 잘 해내고, 여러 데코레이터를 조합해 더 복잡한 동작을 만들어낼 수 있습니다.

Ruby에서 동작을 공유하는 방법은 정말 다양합니다. 서브클래싱을 할 수도 있고, 모듈을 조합할 수도 있고, 심지어 한 클래스에서 메서드를 가져와 다른 클래스에 붙이는 것도 가능합니다. 하지만 데코레이터 패턴은 조금 다른 무언가를 제공합니다. 인스턴스 변수와 메서드 호출, 즉 모든 객체지향 언어의 근간이 되는 기본 빌딩 블록만을 사용합니다. 이러한 기본 요소들만으로 애플리케이션이 실행되는 동안 유연한 동작을 구현할 수 있습니다 — 코드를 불필요하게 복잡하게 만들지 않으면서 말이죠.