본문 바로가기
프로그래밍/Ruby

[Ruby/루비] 클래스 정의하기 / 믹스인 작성하기

by 별준 2020. 8. 20.

- 클래스

루비는 자바, C#, C++과 마찬가지로 클래스와 객체를 갖습니다. 객체지향에 대해 설명할 때 신이 사람을 만들면서 사람이라는 클래스를 가지고 고유한 사람 A, B, C(객체)를 만들었다고 했었습니다. 즉, 클래스는 객체의 템플릿입니다. 

루비의 클래스는 C++과는 다르게 superclass라고 불리는 오직 하나의 부모로부터만 상속할 수 있습니다. 다음을 살펴봅시다.

객체는 클래스로부터 만들어지고, 4의 클래스는 Integer입니다. Integer는 Numeric, Object, 그리고 궁극적으로는 BasicObject라는 클래스를 상속합니다.

위는 이러한 클래스들이 어떻게 관련을 맺는지 확인할 수 있습니다. 

 

위의 관계를 그림으로 그려보면 다음과 같습니다.

해당 모델은 Ruby 1.8.7 버전에서의 모델입니다. 그래서 현재 모델과는 약간의 차이가 있지만, 이해하는데에는 큰 무리가 없을 것입니다. 차이점은 Fixnum이라는 클래스는 Integer로 통합되었고, Object는 BasicObject를 상속합니다. 

 

모든 것이 궁극적으로 BasicObject를 상속합니다. 하나의 Class는 동시에 하나의 Module이며, Class의 인스턴스(instance)는 객체의 템플릿으로 기능합니다. 즉, 여기서는 Integer가 Class의 인스턴스이고, 4는 Integer의 인스턴스입니다. 이들 Class 각각은 그 자체로 하나의 객체이기도 합니다.

 

Class는 Module을 상속하고, Module은 Object를 상속하며, Object는 BasicObject를 상속합니다. 결국 최종적으로 루비에서 사용되는 모든 존재는 BasicObject라는 하나의 공통 조상을 갖습니다.

 

# tree.rb
class Tree
attr_accessor :children, :node_name

	def initialize(name, children=[])
		@children = children
		@node_name = name
	end

	def visit_all(&block)
		visit &block
		children.each {|c| c.visit_all &block}
	end

	def visit(&block)
		block.call self
	end
end

ruby_tree = Tree.new("Ruby", [Tree.new("Reia"), Tree.new("MacRuby")])

puts "Visiting a node"
ruby_tree.visit {|node| puts node.node_name}
puts

puts "visiting entire tree"
ruby_tree.visit_all {|node| puts node.node_name}

위 클래스는 간단한 트리구조를 구현합니다. 이 클래스에는 initilize, visit, visit_all이라는 3개의 메서드와 children, node_name이라는 2개의 인스턴스 변수가 담겨 있습니다. initialize는 특별한 의미가 있는데, 루비는 새로운 클래스가 객체를 생성할 때 이 메서드를 호출합니다.

 

루비에서 사용되는 규칙을 몇 가지 설명하자면, 클래스는 보통 대문자로 시작하고 각 단어의 앞글자를 대문자로 표기하는 캐멀케이스(CamelCase)를 사용합니다. 인스턴스 변수(객체마다 고유의 값을 갖는) 앞에는 반드시 @를 붙여야 하고 클래스 변수(클래스마다 고유의 값을 갖는) 앞에는 @@을 붙여야 합니다. 

인스턴스 변수와 메서드 이름은 소문자로 시작하며, 상수는 모두 대문자를 사용합니다. 

위의 코드는 Tree 클래스를 정의하고 있습니다. 각각의 트리는 @children과 @node_name이라는 두 개의 인스턴스 변수를 가지고 있습니다. 

 

attr_accessor는 인스턴스 변수(게터와 세터)를 정의합니다. 루비에서 인스턴스 변수의 접근과 설정은 다음과 같이 정의될 수 있습니다.

class Person
    def initialize(name)
    	@name = name
    end
    
    def name()          # getter
    	@name = name
    end
    
    def name=(value)    # setter
    	@name = value
    end
end

하지만, attr_accessor :(변수명)으로 선언하게 되면, 자동으로 해당 변수의 Getter와 Setter가 생성됩니다. 

 

다시 Tree Class로 돌아와서, 해당 클래스는 사용자가 트리에 존재하는 모든 노드를 방문하기 위해서 블록과 재귀를 사용하고 있습니다. Tree의 visit 메서드는 안에 포함된 코드 블록을 호출합니다. visit_all 메서드는 해당 노드에 대해 visit를 호출하고, 그 노드의 모든 자신 노드에 대해 visit_all을 재귀 호출합니다.

실행결과는 다음과 같습니다.

 

- 믹스인(Mixin) 작성하기

객체지향 언어는 비슷한 객체들 사이에서 어떤 동작을 전달하기 위해서 상속이라는 개념을 활용합니다. 루비는 이러한 개념을 위해 모듈을 사용합니다. module은 여러 함수와 상수의 컬렉션이며, 클래스에 모듈을 포함하면 그 모듈의 동작과 상수가 해당 클래스의 일부가 됩니다.

임의의 클래스에 to_f라는 메서드를 더하는 모습을 예제를 통해서 살펴봅시다.

# to_file.rb
module ToFile
    def filename
        "object_#{self.object_id}.txt"
    end
    def to_f
        File.open(filename, 'w') {|f| f.write(to_s)}
    end
end

class Person
    include ToFile
    attr_accessor :name
    
    def initialize(name)
        @name = name
    end
    def to_s
        name
    end
end

모듈의 정의부터 살펴보면, 이 모듈은 두 개의 메서드를 가지고 있습니다. to_f 메서드는 to_s 메서드가 출력한 내용을 filename 메서드가 정한 이름의 파일에 저장합니다. 여기서 의문이 가는 점은 to_s 메서드가 모듈 안에서 사용되고 있지만, 아직 정의되지 않은 클래스 내부에서 구현되었다는 것이죠. 심지어 to_s 메서드를 사용해서 to_f 메서드를 정의하는 시점에는 해당 클래스가 정의되지도 않은 상태입니다. 

 

이처럼 모듈은 메서드를 포함하는 클래스와 아주 밀접한 수준으로 상호작용하고 있음을 알 수 있습니다. 자바에서는 클래스가 공식적인 인터페이스를 구현한다는 계약이 명시적으로 이루어져야하지만, 루비에서는 오리 타이핑을 사용할 수 있기 때문에 이와 같은 암묵적으로 사용될 수 있습니다.

 

Person의 구체적인 내용은 크게 중요하지 않습니다. Person은 해당 모듈을 포함하는 것으로 충분합니다. 파일에 어떤 내용을 적을 수 있는 능력을 가진 클래스가 Person인지 여부와는 무관하다는 겁니다. 우리는 파일에 어떤 내용을 적을 수 있는 능력을 추가해주는 믹스인(Mixin)방식으로 전달했습니다. 이렇게 새로운 믹스인과 하위클래스를 Person에 추가하면 하위클래스는 믹스인이 실제로 구현된 방식에 대해서 알지 못해도 믹스인의 기능을 가질 수 있습니다. 

따라서, 어느 클래스의 단일한 상속 관계를 핵심만 간추려서 간결하게 정의하고, 나머지 부가적인 기능(즉, 메서드)는 모듈을 통해서 필요할 때마다 추가하여 사용할 수 있습니다. 이런 스타일의 프로그래밍 방법을 믹스인이라고 합니다. 

믹스인 개념이 항상 모듈이라고 불리지는 않습니다. 단일한 상속 관계와 믹스인이 함께 사용되면 여러 가지 동작을 하나로 묶는 훌륭한 패키지를 구성할 수 있습니다.

 

댓글