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

클래스와 모듈3

by 경자꿈사 2024. 8. 14.

오늘은 모듈의 또 다른 기능인 '네임스페이스' 에 대해 얘기하면서 '클래스와 모듈' 시리즈를 일단락 지을까 한다. 메서드 탐색 경로에 대해 더 깊이 알아보는 것은 다음으로 좀 미루려고 한다.
그렇다면 '네임스페이스'란 무엇이고 왜 필요한지부터 생각해보자. 우리가 프로그램을 작성하다보면 클래스나 모듈 그리고 메서드와 변수 등 많은 이름을 지어야 하는데 모든 이름을 겹치지 않게 짓는 것은 어려운 일이다.
더군다나 다른 누군가가 만들어 놓은 프로그램 소스를 사용해야 한다면 (실제 대부분의 프로그램이 그렇다) 이름 충돌은 불가피하다.
그런데 만약 서로 같은 이름이라 하더라도 그 이름이 속한 어떤 공간이 다르다면 실제 이름 충돌은 피할 수 있다. 식상한 예이긴 하지만 이와 딱 맞는 예가 있다.
어느 학교 같은 반에 같은 이름을 갖은 두 명의 학생이 있을 경우 그 반 안에서 누군가 그 이름을 부른다면 둘 중 누구를 부르는지 전혀 알 수 없는 상황(이름 충돌)이 되지만 만약 이름이 같은 두 아이가 서로 다른 반이라면 해당 반 안에서는 당연히 헛갈릴 일이 없고, 반을 벗어나서도 '1반 아무개' 또는 '2반 아무개' 처럼 앞에 '반 이름'을 붙여서 정확히 누구를 가리키는지 명확히 나타낼 수 있다.
여기서 '1반', '2반' 이 바로 이름이 속한 어떤 공간 즉 '네임스페이스' 라고 보면 된다.
루비에서는 클래스와 모듈 모두 '네임스페이스' 역할을 해준다. 아래 그림을 보면 같은 이름의 상수 'PI' 를 3개 정의했는데 하나는 전역 네임스페이스에, 하나는 클래스 C에, 나머지 하나는 모듈 M 에 각각 정의했다.
프로그램 코드에서 어떤 클래스나 모듈 안이 아닌 가장 바깥에서 정의한 상수는 전역 네임스페이스(기본 네임스페이스)에 속하게 되어 아래 C2 클래스에서처럼 상수 이름만으로 바로 접근이 가능하다.
그러나 C3 클래스를 보면 클래스 안에서 전역 네임스페이스에 있는 상수와 같은 이름의 상수를 정의하고 있는데 이 경우 단순히 상수 이름만을 사용하게 되면 클래스 안에서 정의한 상수를 참조하게 된다.
이는 1반 안에서 그냥 '아무개' 를 부르면 '2반의 아무개' 가 아닌 현재 같은 반 안에 있는 '아무개' 를 부르는 것과 마찬가지이다.
따라서 C3 클래스 안에서 다른 네임스페이스에 있는 같은 이름의 상수를 참조하기 위해서는 해당 네임스페이스를 나타내는 클래스 이름이나 모듈 이름을 상수 이름 앞에 명시해야 한다. 
전역 네임스페이스는 별다른 이름이 없으므로 '::상수 이름' 이렇게 하면 참조가 가능하다.

>> PI = 3.14159265359
=> 3.14159265359
>>
?> class C1
?>   PI = 3.14159
>> end
=> 3.14159
>>
?> class C2
?>   def self.print_pi
?>     puts PI
?>   end
>> end
=> :print_pi
>>
?> class C3
?>   PI = 3.14
?>   def self.print_pi
?>     puts PI
?>     puts C1::PI
?>     puts ::PI
?>   end
>> end
=> :print_pi
>>
>> C2.print_pi
3.14159265359
=> nil
>>
>> C3.print_pi
3.14
3.14159
3.14159265359
=> nil

생각해 보니 상수에 대한 설명이 없었다. 간단히 설명하면 상수는 변수와 달리 대문자로 시작해야 하고 (변수는 '_' 나 소문자로 시작) 상수를 처음 정의하면서 값을 할당한 후에는 다른 값으로 변경하면 안된다. 
물론 값을 변경하는 것이 가능하긴 하지만 상수를 쓰는 목적 자체가 변하지 않는 값을 가리키기 위한 것이므로 변경하지 않는 것이 좋다. 
또한 위의 예제 코드와 같이 상수는 정의된 클래스나 모듈 등의 네임스페이스에 속해 프로그램 전역에서 접근이 가능하지만 변수는 변수가 정의된 범위(스코프)내에서만 접근이 가능하다.
재미있게도 클래스나 모듈 이름 자체도 상수인데 해당 클래스나 모듈 객체의 참조 값을 가리킨다고 보면 된다.
아래 예를 보면 내가 정의한 MyClass 클래스를 my_class 변수에 할당하는 것처럼 보이는데 실제로는 MyClass 상수가 가리키는 객체(MyClass 클래스)의 참조 값을 변수에 할당하는 것이다.
그러면 이제 my_class 변수 역시 MyClass 클래스(객체)를 참조하게 되어 my_class 변수를 통해서도 MyClass 클래스의 인스턴스를 생성할 수 있게 된다.
그리고 루비에서 제공하는 defined? 연산자를 통해서도 MyClass 는 상수이고, my_class 는 MyClass 와 동일한 값을 가지고 있지만 상수가 아니라 (로컬)변수임을 알 수 있다.

?> class MyClass
>> end
=> nil
>>
>> puts MyClass.object_id
260
=> nil
>>
>> my_class = MyClass
=> MyClass
>>
>> puts my_class.object_id
260
=> nil
>>
>> my_class.new
=> #<MyClass:0x000002186d25e068>
>>
>> defined? MyClass
=> "constant"
>>
>> defined? my_class
=> "local-variable"

클래스 사용의 주 목적은 네임스페이스가 아니라 객체를 표현하는 틀을 제공하고 실제 인스턴스(객체)를 생성하여 메서드를 실행하기 위함이므로 단순히 네임스페이스가 필요한 경우라면 클래스가 아니라 모듈을 사용하는 것이 좋다.
모듈을 네임스페이스로 사용하는 간단한 예를 보자. 
내가 현재 만들고 있는 프로그램에서 문자열 안에 포함된 단어의 수를 세는 기능을 갖는 클래스를 만들어야 하고 그 클래스 이름을 WordCounter 라고 짓고 싶다고 해보자.
그런데 같은 프로그램 안에서 기존에 누군가 만들어 놓은 이름은 같지만 기능이 다른 WordCounter 클래스(또는 모듈)를 사용해야 한다면 어떻게 해야 될까?
아래 그림을 보면 class_and_module 이라는 폴더 밑에 세 개의 파일과 third_party 폴더 하나가 있고 다시 third_party 폴더 안에는 word_counter.rb 파일 하나가 있다.

각각의 파일 내용도 그림 아래 순서대로 옮겨 놓았다. 젤 아래 코드가 third_party/word_counter.rb 파일의 내용으로 다른 누군가가 만들어 놓은 코드라고 생각하면 된다.

 

require './third_party/word_counter'
require './word_counter'

pp WordCounter.new.count("./test.txt")

class WordCounter
  def count(str)
    h = Hash.new(0)
    str.split.each { |w| h[w] += 1 }
    h
  end
end

 

class WordCounter
  def count(file)
    h = Hash.new(0)
    File.foreach(file) do |str|
      str.split.each { |w| h[w] += 1 }
    end
    h
  end
end

실제 이런식으로 프로그램을 작성하진 않겠지만 예를 위한 코드로서 이름 충돌로 어떤 문제가 발생하는지 보여주기에는 적당할 것 같아 작성해 본 것이다.

만약 이대로 my_program.rb 를 실행한다면 결과가 어떻게 나올 것 같은가?

다행히(?) 에러가 발생하진 않지만 결과는 다음처럼 test.txt 파일 안에 있는 문장에서 단어의 수를 세지 않았다.

이유는 my_program.rb 소스 안에서 require 로 두 개의 루비 소스 파일을 로드한 순서 때문이다. third_party 안에 있는 word_counter.rb 파일에 정의된 WordCounter 클래스는 분명히 파일 안의 내용에서 단어 수를 세도록 count 메서드를 작성했지만, 뒤이어 로드한 내가 작성한 word_counter.rb 파일 안에 있는 같은 이름의 WordCounter 클래스에 있는 역시 같은 이름의 count 메서드가 그 기능을 재정의해 버린 것이다.

내가 만든 count 메서드는 파일이 아니라 단순히 인수를 문자열로 생각하여 공백을 기준으로 단어 수를 세기 때문에 파일명을 그냥 단어 하나로 인식한 것이다.

그래서 실제 my_program.rb 소스에서 require 순서를 아래처럼 바꾸면 원하는(?) 결과가 나오게 된다.

require './word_counter'
require './third_party/word_counter'

pp WordCounter.new.count("./test.txt")

이제 내가 작성한 WordCounter 클래스가 전역 네임스페이스가 아닌 모듈을 사용하여 특정 네임스페이스에 속하도록 프로그램을 수정해 보자. 아래 코드를 보면 WordCounter 클래스를 Utils 모듈 안에서 정의함으로써 내가 작성한 WordCounter 클래스를 Utils::WordCounter 로 명확히 참조할 수 있게 되었다.

이제 require 순서와 상관 없이 내가 원하는 'count' 메서드를 올바르게 사용할 수 있게 되었다.

module Utils
  class WordCounter
    def count(str)
      h = Hash.new(0)
      str.split.each { |w| h[w] += 1 }
      h
    end
  end
end
require './third_party/word_counter'
require './word_counter'

pp WordCounter.new.count("./test.txt")
pp Utils::WordCounter.new.count("I love you! you love me?")

끝으로 아래와 같이 모듈을 include 하게 되면 해당 모듈의 네임스페이스에 정의된 상수에 대해 따로 네임스페이스를 지정하지 않고도 바로 참조가 가능하다. 

>> require './word_counter'
=> true
>> include Utils
=> Object
>> Object.ancestors
=> [Object, Utils, Kernel, BasicObject]
>> WordCounter.new.count("I love you! you love me?")
=> {"I"=>1, "love"=>2, "you!"=>1, "you"=>1, "me?"=>1}

 

See you again~~