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

Ruby 상속 완벽 가이드 – 오늘 바로 OOP 마스터하기!

클래스 상속(Inheritance)은 객체 지향 프로그래밍(OOP)의 핵심 기능으로, 기존 클래스를 더 구체적이고 특화된 버전으로 확장할 수 있게 해주는 강력한 도구입니다.

간단한 예시를 볼까요?

Food -> Fruit -> Orange

이 클래스들 사이에는 분명한 관계가 있습니다. 오렌지는 과일이고, 과일은 음식이기 때문입니다.

부모 클래스(슈퍼 클래스 또는 베이스 클래스라고도 불립니다)는 항상 하위 클래스보다 더 일반적인 개념입니다. 즉, Fruit(더 일반적)는 Orange(더 구체적)의 부모 클래스가 되는 것이죠.

Ruby에서는 다음과 같이 표현합니다:

class Food
end

class Fruit < Food
end

class Orange < Fruit
end

Ruby에서 상속이 주는 핵심 의미 중 하나는, Food에 정의된 모든 메서드와 상수가 Fruit뿐만 아니라 Orange에서도 그대로 사용할 수 있다는 점입니다. 메서드는 기반 클래스에서 하위 클래스로 전달되며, 그 반대 방향으로는 전달되지 않습니다.

상속의 목적은 무엇일까요?

객체 계층 구조를 만드는 것이 단순히 코드를 보기 좋게 정리하는 정도에 그친다고 생각할 수 있지만, 실제로는 훨씬 더 실용적인 이유가 있습니다.

상속은 특정 목적에 맞춰 부모 클래스의 새로운 버전을 만들기 위해 사용됩니다. 부모 클래스에는 어울리지 않는 추가 기능이나 메서드가 필요할 때 유용하죠.

예를 들어, 모든 과일은 색상, 무게, 이름이라는 공통 속성을 가집니다. 하지만 어떤 과일은 다른 과일과 공유하지 않는 고유한 특성이 있을 수 있습니다. 이럴 때 모든 과일의 공통 특성(색상, 무게 등)을 상속받는 새 클래스를 만들고, 거기에 특별한 특성만 추가하면 됩니다. 이것이 바로 특수화(Specialization)입니다.

또 다른 예시로, 데이터베이스에 쓰기를 수행하는 클래스가 있는데, 디버깅 목적으로 모든 DB 작업을 로그로 남기는 변형 버전이 필요하다고 가정해 봅시다. 이런 경우에는 데코레이터 패턴이 적합합니다. 기본 개념은 상속을 활용해 기존 클래스를 감싸고(wrap) 새로운 기능을 추가하는 것인데, 원본 클래스는 전혀 수정하지 않고 이를实现할 수 있다는 점이 핵심입니다.

실무 속의 상속

Ruby 개발자라면 사실 매일 상속을 사용하고 있다는 사실을 알고 계셨나요? Ruby 자체도 상속을 활용해 puts, class, super 같은 메서드들을 제공합니다.

그 이유는 모든 Ruby 객체가 기본적으로 Object 클래스를 상속받기 때문입니다.

다음과 같이 클래스를 만들면:

class Apple
end

부모 클래스는 자동으로 Object가 됩니다:

Apple.superclass
# => Object

그렇기 때문에 앞서 언급한 메서드들을 자유롭게 사용할 수 있는 것입니다.

예를 들어 puts를 호출하면 Ruby는 다음 순서로 메서드를 탐색합니다:

  • 먼저 해당 클래스 내부에서 메서드를 찾습니다
  • 찾지 못하면 부모 클래스들을 따라 올라가며 탐색합니다
  • 그래도 없으면 다시 Object부터 시작해 method_missing을 찾습니다
  • 최종적으로 찾지 못하면 NoMethodError가 발생하며, 명시적인 객체 없이 호출한 경우(a.size vs size)에는 NameError가 발생합니다

Rails에서도 상속의 좋은 예시를 찾을 수 있습니다.

애플리케이션 컨트롤러:

class ApplicationController < ActionController::Base
end

개별 컨트롤러:

class SessionsController < ApplicationController
end

모델:

class Comment < ApplicationRecord
  belongs_to :article
end

이처럼 상속은 Ruby 곳곳에 존재하지만, 때로는 상속이 최선의 해결책이 아닐 수 있습니다.

컴포지션(합성): 상속의 강력한 대안

상속에는 분명한 한계가 있습니다.

예를 들어, 부품들을 조립해 컴퓨터를 만든다고 생각해 봅시다. 우리는 "컴퓨터는 부품을 가진다"고 말하지만, 개별 부품 자체가 컴퓨터는 아닙니다. 부품을 분리하면 각각의 기능을 제대로 수행할 수 없죠.

이럴 때 필요한 것이 바로 컴포지션(Composition)입니다!

컴포지션은 서로 다른 부품들이 모여 하나의 기능을 수행하도록 클래스를 구성하는 방식입니다. 마치 컴퓨터처럼요.

실제 컴포지션의 예시입니다:

class Computer
  def initialize(memory, disk, cpu)
    @memory = memory
    @disk   = disk
    @cpu    = cpu
  end
end

컴퓨터는 동작에 필요한 부품들을 외부에서 전달받습니다. 이것이 바로 컴포지션입니다.

리스코프 치환 원칙(LSP)

상속은 올바른 상황에서 사용하면 매우 강력하지만, 모든 도구가 그렇듯 남용될 수 있습니다. 실제로 객체 지향 설계의 고전인 『Design Patterns』에는 이런 유명한 문구가 있습니다:

"상속보다 컴포지션을 선호하라."
Design Patterns: Elements of Reusable Object-Oriented Software

상속을 올바르게 사용하고 있는지 확인하려면 SOLID 원칙 중 L, 즉 리스코프 치환 원칙(Liskov Substitution Principle)을 따르면 됩니다. 이 원칙은 하위 클래스가 기반 클래스를 대신해 사용될 수 있어야 한다고 말합니다.

다시 말해, Fruit를 상속받았는데 color가 문자열을 반환한다면, 하위 클래스에서 color가 심볼을 반환하도록 바꾸면 안 됩니다.

예시: Fruit의 사용자들은 color가 문자열을 반환한다는 사실에 의존하고 있습니다.

class Fruit
  def color
    "orange"
  end
end

다음 코드는 LSP를 위반합니다:

class Orange < Fruit
  def color
    :orange
  end
end

color가 심볼을 반환하도록 변경하면, Fruit 객체를 Orange로 안전하게 대체할 수 없게 됩니다. 왜냐하면 심볼에는 split 같은 문자열 전용 메서드가 없어서 호출 시 에러가 발생하기 때문입니다.

또 하나 주목해야 할 경고 신호는, 하위 클래스가 부모 클래스의 진정한 특수화가 아니라 단순히 유틸리티 메서드를 공유하기 위한 목적으로만 상속을 사용하는 경우입니다.

마치며

지금까지 Ruby의 상속컴포지션에 대해 알아보았습니다. 상속은 부모 클래스의 특수화된 버전을 만들 때, 컴포지션은 여러 구성 요소를 하나의 전체로 조립할 때 사용하세요. 그리고 코드가 엉망이 되지 않으려면 LSP 원칙을 꼭 기억하시기 바랍니다.

이제 여러분도 더 나은 객체 지향 코드를 작성할 준비가 되었습니다 🙂
읽어주셔서 감사합니다!