본문 바로가기
카테고리 없음

객체와 클래스3

by 경자꿈사 2024. 7. 18.

이전 글에서 Person 클래스에 to_s 메서드를 정의하기 전에도 Person 객체에 대해 to_s 메서드를 호출할 수 있었는데 어떻게 그게 가능한지 알아보자.

아래 그림을 보면 p.methods (methods 라는 메서드 역시 Person 클래스에 정의하지 않은 것은 마찬가지다.) 를 실행하니 수많은 심볼들을 담은 배열 객체를 돌려주었다. 이것은 호출한 메서드 이름에서도 알 수 있듯이 Person 객체에 대해 호출할 수 있는 메서드들의 목록이다. 처음 몇 개는 낯익은 값들이 보인다. 우리가 직접 Person 클래스에 정의한 메서드들이다.

>> require './person'
=> true
>> p = Person.new("홍길동", "010-1111-1111", "서울 특별시 강동구")
>> p.methods
=>
[:to_s, :name, :info, :phone, :address, :phone=, :address=, :__send__, ...생략]

그렇다면 이 수 많은 메서드들은 어디에 정의되어 있고 어떻게 해서 Person 객체에서 호출할 수 있다는 것일까?

그 것을 알기 위해서는 '상속(inheritance)' 이란 개념을 알아야 한다. 컴퓨터 프로그래밍에서의 상속은 자식 클래스가 부모 클래스로 부터 속성과 메서드를 물려 받는 기능을 말한다.

상속에 대해 이해하기 쉽게 간단히 예를 들어 보겠다. 지금 우리에게 Person 클래스가 있는데 또 다른 프로그램에서 Person 클래스와 비슷하지만 '직업' 이란 속성과 '일하다' 라는 메서드가 추가로 있는 클래스(이름을 Worker라고 해보자.)가 필요하다면 어떻게 해야 할까? 그냥 처음부터 Worker 클래스를 만드는 방법도 있겠지만 이때 사용할 수 있는 방법이 바로 상속이다.

class Person
  attr_reader :name
  attr_accessor :phone, :address

  def initialize(name, phone, address)
    @name = name
    @phone = phone
    @address = address
  end

  def info
    "#{name} / #{phone} / #{address}"
  end
  
  def to_s
    info
  end
end
>> require './person'
=> true
>>
?> class Worker < Person
?>   attr_accessor :job
?>
?>   def initialize(name, job)
?>     @name = name
?>     @job = job
?>   end
?>
?>   def work
?>     "I'm working now"
?>   end
>> end
=> :work
>> w = Worker.new("홍길동", "개발자")
=> #<Worker:0x000001a64e6c92b0 @job="개발자", @name="홍길동">
>> w.name
=> "홍길동"
>> w.phone
=> nil
>> w.address
=> nil
>> w.job
=> "개발자"
>> w.work
=> "I'm working now"
>> w.methods
[:work, :job, :job=, :to_s, :name, :info, :phone, :address, :phone=, :address=, ...생략]

앞의 코드 실행 결과를 보면 기존에 만들어 놓은 Person 클래스의 코드를 irb 에서 로드한 후 Person 클래스를 상속하여 Worker 클래스를 만든 것을 알 수 있다. Worker 객체를 생성할 때는 이름과 직업만 필요하다고 간주하자. 그래서 initialize 메서드에서 두 개의 인수만 받아서 초기화 작업을 했다. 그리고 Worker 클래스에는 얘기한 대로 work 메서드도 추가로 만들었다.

이후 코드를 보면 Worker 객체에 대해 name, phone, address 메서드 모두 다 에러 없이 호출이 가능했다.

물론 연락처와 주소 정보가 없어 nil 이 보이는 것은 당연하다. 그러나 연락처와 주소 속성에 대한 setter 메서드들도 상속을 받았으므로 필요하다면 setter 를 통해 값을 설정할 수 있다.

그리고 아래 w.methods 실행 결과를 보면 Worker 클래스에서 직접 정의한 메서드들(:job=, :work, :job) 뿐만 아니라 Person 클래스로 부터 상속 받은 메서드들도 볼 수 있다.

require './person'

class Worker < Person
  attr_accessor :job

  def initialize(name, job)
    @name = name
    @job = job
  end
  
  def work
    "I'm working now"
  end
end
>> require './person'
=> true
>> require './worker'
=> true
>> Worker.ancestors
=> [Worker, Person, Object, Kernel, BasicObject]

우선 테스트를 편하게 하기 위해 Worker 클래스의 코드를 person.rb 파일이 있는 위치와 같은 곳에 worker.rb 라는 이름으로 파일에 저장하였다.

그리고 해당 위치에서 irb 를 실행하여 person.rb 와 worker.rb 두 파일을 require를 사용해 로드했다.

person.rb 를 먼저 로드한 후에 worker.rb 를 로드해야 한다. 그렇지 않으면 아래 그림과 같이 worker.rb 파일 안에 작성된 코드에 있는 Person 이라는 것을 인식하지 못해 NameError가 에러가 발생하게 된다.

>> require './worker'

NameError (uninitialized constant Person)

이번에는 person.rb 와 worker.rb 를 모두 정상적으로 로드한 화면을 보자.

화면을 보면 'Worker.ancestors' 라는 코드와 결과 값을 볼 수 있는데 알다시피 'ancestor' 는 조상을 뜻하는 영어 단어이고 그래서 이 메서드가 하는 일은 특정 클래스의 조상(들)의 목록을 가까운 조상부터 순서대로 배열에 담아 돌려준다.

다시 결과를 보면 Worker 의 바로 위 조상 즉 부모는 우리가 만든 Person 클래스이고 Person 클래스의 부모는 Object 클래스이다. Object 클래스 위로 Kernel 과 BasicObject 클래스가 보인다. 여기서 Kernel 은 클래스가 아니고 모듈이다. 모듈에 대한 설명은 나중에 하기로 하고 우선 우리가 궁금했던 to_s 메서드를 정의한 곳이 어디인지 찾아보자.

아래 그림을 보면 젤 위 조상인 BasicObject 클래스에 대해 instance_methods를 실행한 결과에는 우리가 찾는 to_s 메서드의 이름은 보이지 않았다. 그런데 바로 그 다음 조상인 Kernel 모듈에서 to_s 메서드를 찾을 수 있었다!

결국, to_s 메서드는 Kernel 모듈에서 처음 정의되었고 그것을 Object 클래스가 상속받고 다시 Person 클래스가 상속을 받아 Person 클래스 안에서 to_s 메서드를 만들지 않았어도 사용할 수 있었던 것이었다.

>> require './person'
=> true
>> require './worker'
=> true
>>
>> Worker.ancestors
=> [Worker, Person, Object, Kernel, BasicObject]
>> BasicObject.instance_methods
=> [:equal?, :!, :__send__, :==, :!=, :__id__, :instance_eval, :instance_exec]
>> Kernel.instance_methods
=>
[:singleton_class, :dup, :itself, :methods, :singleton_methods, :to_s, ...생략]

아래 처럼 owner 메서드를 사용하여 확인할 수 있는 방법도 있다.

?> class Person1
>> end
=> nil
>>
?> class Person2
?>   def to_s
?>     "^^"
?>   end
>> end
=> :to_s
>>
>> Person1.instance_method(:to_s).owner
=> Kernel
>> Person2.instance_method(:to_s).owner
=> Person2

Person2 클래스에서는 to_s 메서드를 Person2 클래스 내에서 다시 만들었기 때문에(이를 '오버라이딩' 했다고 한다.) owner 메서드의 결과가 Person2 가 된다.

다음 글에서는 클래스로 부터 생성한 인스턴스가 아닌 클래스 자체에 대해서도 메서드를 호출할 수 있는지 알아보겠다.

See you again~~