Ruby

[Ruby] 루비 상수참조 규칙 알아보기

강씨아저씨 2019. 1. 13. 21:58

오늘의 포스팅 내용은 Ruby에서 상수(Constants) 참조시 어떤 규칙으로 동작하는지에 대해 알아볼 예정이다.

포스팅의 내용은 사내에서 Ruby와 Rails 의 상수참조에 대한 공유자료를 만들기 위해서 관련 포스팅들을 읽고 내용을 정리한것이다.

내용이 많기때문에 Ruby의 상수참조와 Rails 의 Autoloading 에 대해서 나눠서 소개할 예정이다. 
이번에는 Ruby에서 상수참조시 어떠한 규칙으로 상수를 참조하게 되는지 알아볼 예정이다. 


왜 상수참조 원리에 대해 알아야 할까?

상수참조 규칙에 대해서 알아두면 어플리케이션 개발시 동일한 상수를 여러번 정의하거나 의도치 않은 상수참조 실패를 방지 할 수 있고 디버깅 할때 도움이 된다.  

루비에서의 상수관리
우선은 루비에서 상수를 정의했을때 우리가 정의한 상수가 어디에 어떠한 형태로 관리되는지 알아보자. 다음과 같은 예제를 보자. 

# FILE: object_constants
#
puts self.class
puts Object.constants.inspect
puts "Is MyConstants inside Object constants? #{Object.constants.include?(:MyConstants)}"
MyConstants = 1
puts Object.constants.inspect
puts "Is MyConstants inside Object constants? #{Object.constants.include?(:MyConstants)}"

이를 실행해보면 다음과 같은 결과가 나온다. 

Object
[:Object, :Module, :Class, :BasicObject, :Kernel, ... :SimpleDelegator, :RUBYGEMS_ACTIVATION_MONITOR]
Is MyConstants inside Object constants? false
[:Object, :Module, :Class, :BasicObject, :Kernel, ... :SimpleDelegator, :MyConstants, :RUBYGEMS_ACTIVATION_MONITOR]
Is MyConstants inside Object constants? true

이 예제를 통해 우리는 Top level programming scope 에서 상수를 정의했을때는 Object 의 Constants 에 정의된것을 알 수 있다. 그렇다면 Top level Programming scope 가 아닌 클래스에서 상수를 정의했다면 어떻게 될까?

# FILE: class_constants
#
class TestClassConstants
  MyClassConstants = 1
	
  def self.check
    puts self.class
	puts self.constants.inspect
	puts "Is MyClassConstants inside TestClassConstants constants? #{self.constants.include?(:MyClassConstants)}"
	puts "Is MyClassConstants inside Object constants? #{Object.constants.include?(:MyClassConstants)}"
  end
end

TestClassConstants.check

이를 실행하면 다음과 같은 결과가 나온다. 

Class
[:MyClassConstants]
Is MyClassConstants inside TestClassConstants constants? true
Is MyClassConstants inside Object constants? false

위의 두가지 예제를 통해 Ruby에서 상수정의시에는 정의시점에 self(클래스 혹은 객체)의 상수로 정의되고 정의된 상수는 Array[:Symbol] 형태로 확인 할 수 있다는 것을 알수 있다.

정의되지 않은 상수접근
정의되어 있지 않은 상수로의 접근시 다음과 같은 오류를 반환하게 된다. 

# FILE: access_undefined_constant.rb
#
UndefinedConstant

----- 실행 결과 -----
ruby access_undefined_constant.rb
access_undefined_constant.rb:3:in main : uninitialized constant UndefinedConstant (NameError)

NameError 는 어디서 발생시키는 걸까?
루비의 Top level에서 UndefinedConstant 를 접근 하려고 시도할때 Object 의 constants 에 해당 상수가 없다면  Object 의 .constant_missing() 함수를 호출하게 된다.  이를 증명하기 위해 우리는 constant_missing 함수를 재정의해보자. 

# File: redefine_object_const_missing.rb
#
class Object
  def self.const_missing(const_name)
    raise NameError, "OMG! #{const_name} is missing!"
  end
end
UndefinedConstant

----- 실행 결과 -----
redefine_object_const_missing.rb:5:in \`const_missing\': OMG! UndefinedConstant is missing! (NameError)
        from redefine_object_const_missing.rb:9:in main

우리의 예상대로 UndefinedConstant 를 찾지 못한 Object 는 const_missing() 함수를 호출해서 NameError 를 반환하고 있다. 

정의된 상수를 확인하는 방법
constants 가 아닌 다른 함수로는 상수정의를 확인 할 수 없을까? 물론 아니다. defined? 함수를 사용하면 상수가 정의되어 있는지 여부를 확인 할 수 있다. 

# File: check_constant_definition_with_defined.rb
#
DefineConstant = 1
puts "Is DefineConstant defined?: #{defined?(DefineConstant) || 'no'}"
puts "Is UndefineConstant defined?: #{defined?(UndefineConstant) || 'no'}"

----- 실행 결과 -----
Is DefineConstant defined?: constant
Is UndefineConstant defined?: no

정의된 상수를 제거하는 방법
일반적으로는 정의된 상수를 제거할 수 없지만 private 함수인 Object#remove_const 를 호출한다면 이미 정의된 상수를 제거 할 수 있다. 

# File: removing_constant.rb
#
DefineConstant = 1
puts "Is const defined? #{Object.const_defined?(:DefineConstant)}"
Object.send :remove_const, :DefineConstant
puts "Is const defined? #{Object.const_defined?(:DefineConstant)}"

----- 실행 결과 -----
Is const defined? true
Is const defined? false

Module 과 Class 도 상수이다
우리는 이미 알고있지만 우리가 정의하는 Module 과 Class 또한 상수이다. 이를 다음 예제를 통해 증명해보자. 

# File: class_module_definition.rb
#
puts "Is MyClass included in Object constants? #{Object.constants.include?(:MyClass)}"
class MyClass
end
puts "Is MyClass included in Object constants? #{Object.constants.include?(:MyClass)}"

puts "Is MyClass2 included in Object constants? #{Object.constants.include?(:MyClass2)}"
MyClass2 = Class.new
puts "Is MyClass2 included in Object constants? #{Object.constants.include?(:MyClass2)}"

puts "Is MyModule included in Object constants? #{Object.constants.include?(:MyModule)}"
module MyModule
end
puts "Is MyModule included in Object constants? #{Object.constants.include?(:MyModule)}"

----- 실행 결과 -----
Is MyClass included in Object constants? false
Is MyClass included in Object constants? true
Is MyClass2 included in Object constants? false
Is MyClass2 included in Object constants? true
Is MyModule included in Object constants? false
Is MyModule included in Object constants? true

상수의 중첩
루비에서는 상수들을 중첩해서 사용할 수 있는데 이번에는 중첩되어 정의된 상수를 알아보자. 

# File: nesting.rb
#
module MyModule
  MyConstant = 'Outer Constant'

  module MyModule
    MyConstant = 'Inner Constant'
  end
end

puts "Is MyModule defined?: #{defined?(MyModule) || 'no'}"
puts "Is MyModule defined in Object?: #{Object.const_defined?(:MyModule)}"
puts "Is MyModule::MyConstant defined?: #{defined?(MyModule::MyConstant) || 'no'}"
puts "Is MyModule::MyModule defined?: #{defined?(MyModule::MyModule) || 'no'}"
puts "Is MyModule::MyModule::MyConstant defined?: #{defined?(MyModule::MyModule::MyConstant) || 'no'}"
puts "Is MyConstant defined in Object:: #{Object.const_defined?(:MyConstant)}"

puts MyModule
puts MyModule::MyConstant
puts MyModule::MyModule
puts MyModule::MyModule::MyConstant

----- 실행 결과 -----
Is MyModule defined?: constant
Is MyModule defined in Object?: true
Is MyModule::MyConstant defined?: constant
Is MyModule::MyModule defined?: constant
Is MyModule::MyModule::MyConstant defined?: constant
Is MyConstant defined in Object:: false
MyModule
Outer Constant
MyModule::MyModule
Inner Constant

위의 예제를 실행하면 루비에서는 다음과 같은 4개의 상수가 정의된다. 

  1. MyModule
  2. MyModule::MyConstant
  3. MyModule::MyModule
  4. MyModule::MyModule::MyConstant

이번에는 nesting.rb 에 다음과 같은 코드를 추가한뒤 실행해보자. 

puts "Object constants : #{Object.constants}"
puts "Is MyModule defined in Object.constants? : #{Object.const_defined?(:MyModule)}"
puts "MyModule constants: #{MyModule.constants}"
puts "Is MyConstant defined in MyModule.constants?: #{MyModule.const_defined?(:MyConstant)} "
puts "Is MyModule defined in MyModule.constants?: #{MyModule.const_defined?(:MyModule)} "
puts "MyModule::MyModule constants: #{MyModule::MyModule.constants}"
puts "Is MyConstant defined in MyModule::MyModule constants: #{MyModule::MyModule.const_defined?(:MyConstant)}"

그러면 다음과 같은 결과를 확인할수 있다. 

Object constants : [:Object, :Module, :Class, ... :Delegator, :SimpleDelegator, :MyModule, :RUBYGEMS_ACTIVATION_MONITOR]
Is MyModule defined in Object.constants? : true
MyModule constants: [:MyConstant, :MyModule]
Is MyConstant defined in MyModule.constants?: true
Is MyModule defined in MyModule.constants?: true
MyModule::MyModule constants: [:MyConstant]
Is MyConstant defined in MyModule::MyModule constants: true

이를통해 알수 있는것은 MyModule 은 Object 와 MyMobulde 에 각각 상수로 정의되어 있고 둘은 서로 다른 상수로 하나는 Object 의 상수이고 다른 하나는 MyModule 의 상수이다. 

중첩 상태 확인하기
Module.nesting 을 사용하면 중첩에 대한 상태를 확인할 수 있다. 

# File: nesting_path.rb
#
puts "Module.nesting at root level: #{Module.nesting}"

module MyModule
  puts "Module.nesting at MyModule level: #{Module.nesting}"
  MyConstant = 'Outer Constant'

  module MyModule
    puts "Module.nesting at MyModule::MyModule level: #{Module.nesting}"
    MyConstant = 'Inner Constant'
  end
end

----- 실행 결과 -----
Module.nesting at root level: []
Module.nesting at MyModule level: [MyModule]
Module.nesting at MyModule::MyModule level: [MyModule::MyModule, MyModule]

루비에서는 중첩된 상수를 어떻게 참조 할까?

상수에 대해서는 위에서 알아봤으므로 이제는 루비에서 중첩된 상수를 어떻게 처리하는지에 대해서 알아보자. 

Case1. 중첩된 상태에서의 상수참조

# File: resolving_constants_relative_reference_1.rb
#
module MyModuleA
  B = 1
  puts "MyModuleA defines B? #{MyModuleA.const_defined?(:B)}"

  module MyModuleB
    puts "MyModuleA::MyModuleB nesting: #{Module.nesting}" # => [MyModuleA::MyModuleB, MyModuleA]
    puts B
    puts "MyModuleA::MyModuleB defines B? #{MyModuleA::MyModuleB.const_defined?(:B)}"
    puts "MyModuleA defines B? #{MyModuleA.const_defined?(:B)}"
  end
end

----- 실행 결과 -----
MyModuleA defines B? true
MyModuleA::MyModuleB nesting: [MyModuleA::MyModuleB, MyModuleA]
1
MyModuleA::MyModuleB defines B? false
MyModuleA defines B? true

위의 예제에서 B를 참조할때 루비에서는 Module.nesting 에 속해있는 클래스나 모듈에 B라는 상수가 정의되어 있는지 확인하게 된다. MyModuleA::MyModuleB 에서는 상수 B를 찾지 못했지만 MyModuleA 에서 상수 B를 찾았기 때문에 NameError 없이 상수를 참조할 수 있었다.
 만약 상수 B의 위치를 ModuleB 내부로 옮기면 다음과 같은 결과를 확인할 수 있다.

# File: resolving_constants_relative_reference_2.rb
#
module MyModuleA
  puts "MyModuleA defines B? #{MyModuleA.const_defined?(:B)}"

  module MyModuleB
    puts "MyModuleA::MyModuleB nesting: #{Module.nesting}" # => [MyModuleA::MyModuleB, MyModuleA]

    B = 1

    puts B

    puts "MyModuleA::MyModuleB defines B? #{MyModuleA::MyModuleB.const_defined?(:B)}"
    puts "MyModuleA defines B? #{MyModuleA.const_defined?(:B)}"
  end
end

----- 실행 결과 -----
MyModuleA defines B? false
MyModuleA::MyModuleB nesting: [MyModuleA::MyModuleB, MyModuleA]
1
MyModuleA::MyModuleB defines B? true
MyModuleA defines B? false

Case2. Ancestors Chain 로 부터의 상수참조
Module.nesting 의 관계에서 상수를 찾지 못할경우에 루비는 Module.nesting 의 첫번째 대상의 Ancestors Chain 로부터 상수를 찾기위한 검색을 한다. 다음의 예제를 보자.( Ancestors Chain 은 https://idea-sketch.tistory.com/37 에 설명되어 있다.)

# File: resolving_constants_ancestors.rb

module MyAncestor
  B = 1

  puts "MyAncestor defines B? #{MyAncestor.const_defined?(:B)}"
end

MyAncestor::B

module MyModuleA
  module MyModuleB
    puts "MyModuleA::MyModuleB.nesting is #{Module.nesting}"

    puts "(before including MyAncestor) MyModuleA::MyModuleB defines B? #{MyModuleA::MyModuleB.const_defined?(:B)}"
    puts "(before including MyAncestor) MyModuleA::MyModuleB.constants includes B? #{MyModuleA::MyModuleB.constants.include?(:B)}"

    include MyAncestor

    puts "MyModuleA::MyModuleB Ancestors: #{MyModuleA::MyModuleB.ancestors}"

    puts "(after including MyAncestor) MyModuleA::MyModuleB defines B? #{MyModuleA::MyModuleB.const_defined?(:B)}"
    puts "(after including MyAncestor) MyModuleA::MyModuleB.constants includes B? #{MyModuleA::MyModuleB.constants.include?(:B)}"

    B
  end
end
puts "Is MyModuleA::MyModuleB::B still equal to MyAncestor::B? #{MyModuleA::MyModuleB::B == MyAncestor::B}"


----- 실행 결과 -----
MyAncestor defines B? true
MyModuleA::MyModuleB.nesting is [MyModuleA::MyModuleB, MyModuleA]
(before including MyAncestor) MyModuleA::MyModuleB defines B? false
(before including MyAncestor) MyModuleA::MyModuleB.constants includes B? false
MyModuleA::MyModuleB Ancestors: [MyModuleA::MyModuleB, MyAncestor]
(after including MyAncestor) MyModuleA::MyModuleB defines B? true
(after including MyAncestor) MyModuleA::MyModuleB.constants includes B? true
Is MyModuleA::MyModuleB::B still equal to MyAncestor::B? true

다음과 같이 MyModuleA::MyModuleB 와 MyModuleA 에서 상수 B를 찾아봤지만 상수가 없다. 그러면 루비는 다음으로 Module.nesting 의 첫번째 대상인 ModuleA::ModuleB 의 Ancestors Chain 으로 MyAncestor 에서 상수 B를 찾게 된다. 그리고 MyAcestor 에 B가 존재하기때문에 MyModuleB 는 상수 B에 접근 할 수 있게 되었다. 그렇다면 Ancestor chain 에 상수가 없고 대상의 Module.nesting 에 상수가 존재해도 찾을수 있을까?

# File: resolving_constants_ancestors2.rb

module MyAncestor
  B = 1
  module MyModuleC
   puts "MyAncestor::MyModuleC.nesting is #{Module.nesting}"
   puts "MyAncestor::MyModuleC Access B? #{B}"
   puts "MyAncestor::MyModuleC defines B? #{MyAncestor::MyModuleC.const_defined?(:B)}"
  end
  puts "MyAncestor defines B? #{MyAncestor.const_defined?(:B)}"
end

module MyModuleA
  module MyModuleB
    puts "MyModuleA::MyModuleB.nesting is #{Module.nesting}"

    puts "(before including MyAncestor) MyModuleA::MyModuleB defines B? #{MyModuleA::MyModuleB.const_defined?(:B)}"
    puts "(before including MyAncestor) MyModuleA::MyModuleB.constants includes B? #{MyModuleA::MyModuleB.constants.include?(:B)}"

    include MyAncestor::MyModuleC

    puts "MyModuleA::MyModuleB Ancestors: #{MyModuleA::MyModuleB.ancestors}"

    puts "(after including MyAncestor) MyModuleA::MyModuleB defines B? #{MyModuleA::MyModuleB.const_defined?(:B)}"
    puts "(after including MyAncestor) MyModuleA::MyModuleB.constants includes B? #{MyModuleA::MyModuleB.constants.include?(:B)}"

    B
  end
end

----- 실행 결과 -----
MyAncestor::MyModuleC.nesting is [MyAncestor::MyModuleC, MyAncestor]
MyAncestor::MyModuleC Access B? 1
MyAncestor::MyModuleC defines B? false
MyAncestor defines B? true
MyModuleA::MyModuleB.nesting is [MyModuleA::MyModuleB, MyModuleA]
(before including MyAncestor) MyModuleA::MyModuleB defines B? false
(before including MyAncestor) MyModuleA::MyModuleB.constants includes B? false
MyModuleA::MyModuleB Ancestors: [MyModuleA::MyModuleB, MyAncestor::MyModuleC]
(after including MyAncestor) MyModuleA::MyModuleB defines B? false
(after including MyAncestor) MyModuleA::MyModuleB.constants includes B? false
resolving_constants_ancestors2.rb:28:in module:mymoduleb : uninitialized constant MyModuleA::MyModuleB::B (NameError)
        from resolving_constants_ancestors2.rb:15:in `'
        from resolving_constants_ancestors2.rb:14:in main

위의 실행결과를 보면 알겠지만 MyModuleC는 Module.nesting 에 MyAncestor 가 있기때문에 C에 접근 할 수 있지만 MyModuleC를 include한 MyModuleB는 Module.nesting 이나 Ancestors Chain 의 대상이 B상수를 정의하고 있지 않기때문에 상수 B를 참조 할 수 없다!

Case3. Object 로 부터의 상수참조
위의 2개의 case를 통해서 우리는 루비가 상수를 찾기위해 Module.nesting 과 Ancestors Chain 의 대상이 상수를 정의하고 있는지를 확인한다는것을 알게되었다. 그렇다면 만약 위의 2가지 case 에 모두 해당하지 못한다면 어떻게 될까? 루비에서는 이럴경우 Object 에서 해당상수를 찾기위해 검색한다. 다음의 예제를 보자

# File: resolving_constants_searching_in_object.rb
#
B = 2

module MyAncestor
  B = 1
end

module MyAncestorB
end

module MyModuleA
  include MyAncestor
  puts "Module nesting of MyModuleA: #{Module.nesting}"
  puts "Ancestors of MyModuleA: #{MyModuleA.ancestors}"
  puts "Referencing B inside MyModuleA: #{B} (resolves to the MyAncestor constant B)"

  module MyModuleB
    include MyAncestorB
    puts "Module nesting inside MyModuleA::MyModuleB is: #{Module.nesting}"
    puts "Ancestors of MyModuleA::MyModuleB: #{MyModuleA::MyModuleB.ancestors}"

    puts "Referencing B inside MyModuleA::MyModuleB: #{B} (resolves to the Object constant :B)"
    puts "B.object_id == ::B.object_id : #{B.object_id == ::B.object_id}"
  end
end

puts "Root level B is: #{B}"
puts "MyAncestor::B is: #{MyAncestor::B}"
puts "MyModuleA::B is: #{MyModuleA::B}"

----- 실행 결과 -----
Module nesting of MyModuleA: [MyModuleA]
Ancestors of MyModuleA: [MyModuleA, MyAncestor]
Referencing B inside MyModuleA: 1 (resolves to the MyAncestor constant B)
Module nesting inside MyModuleA::MyModuleB is: [MyModuleA::MyModuleB, MyModuleA]
Ancestors of MyModuleA::MyModuleB: [MyModuleA::MyModuleB, MyAncestorB]
Referencing B inside MyModuleA::MyModuleB: 2 (resolves to the Object constant :B)
B.object_id == ::B.object_id : true
Root level B is: 2
MyAncestor::B is: 1
MyModuleA::B is: 1

위의예제에서 보는것과 같이 MyModuleB 에 상수 B에 접근하기위해 MyModuleA::MyModuleB 와 MyModuleA 에서 상수 B를 찾기위해 시도하고 그 후 MyAcestorB 에서 상수 B를 찾기를 시도한다 하지만 상수 B를 찾을 수 없기 때문에 마지막으로 Object 의 상수 B를 찾기 시도하고 Top level에 있는 상수 B(2) 를 찾게 되었다. 여기서 주의할점은 MyAncestor 가 상수 검색에 대상이 아니어서 MyAncestor 에 정의되어 있는 상수 B(1) 를 찾지 않고 지나간다는 것이다. 

Case4. 상수를 찾지 못할경우
Case1,2,3 을 통해서도 상수를 찾지 못할경우 NameError 를 반환한다. 그리고 이에대해서는 우리가 이미 위에서 확인해봤다.

# File: resolving_constant_missing.rb
#
module MyAncestor
  B = 1
end

module MyAncestorB
end

module MyModuleA
  include MyAncestor

  module MyModuleB
    include MyAncestorB

    B
  end
end

----- 실행 결과 -----
resolving_constant_missing.rb:16:in module:MyModuleB: uninitialized constant MyModuleA::MyModuleB::B (NameError)
Did you mean?  MyModuleA::B
        from resolving_constant_missing.rb:13:in module:MyModuleA
        from resolving_constant_missing.rb:10:in main

제한된 상수참조 
제한된 상수참조란 다른 클래스나 모듈에 정의된 상수에 대해 참조 할 수 있게 하는 방법이다. 

# File: qualified_constant_references_found_in_parent.rb
#
module MyModuleA
  module MyModuleB
    B = 1
    module MyModuleC
      puts MyModuleB::B
    end
  end
 end

----- 실행 결과 -----
1

다음과 같이 MyModuleC 에서 MyModuleB 의 정의된 상수 B를 참조하게 할 수 있다. 그리고 아래와 같이 MyModuleB의 Ancestors Chain 에 상수가 존재하더라도 동일하게 참조 할 수 있다. 

# File: qualified_constant_references_found_in_ancestors_of_parent.rb
#
module MyAncestor
  B = 1
end

module MyModuleA
  module MyModuleB
    include MyAncestor

    module MyModuleC
      puts MyModuleB::B
    end
  end
end

----- 실행 결과 -----
1

다만 기존의 상수참조와의 차이점은 Object 에서의 상수참조를 하지 않는다는 것이다. 다음의 예제를 보자 

# File: qualified_constant_references_not_found.rb
#
B = 1

module MyAncestor
end

module MyModuleA
  module MyModuleB
    include MyAncestor
    puts "Module nesting of MyModuleA::MyModuleB: #{Module.nesting}"
    puts "Ancestors of MyModuleA::MyModuleB: #{MyModuleA::MyModuleB.ancestors}"
   
    module MyModuleC
      puts MyModuleB::B
    end
  end
end

----- 실행 결과 -----
Module nesting of MyModuleA::MyModuleB: [MyModuleA::MyModuleB, MyModuleA]
Ancestors of MyModuleA::MyModuleB: [MyModuleA::MyModuleB, MyAncestor]
qualified_constant_references_not_found.rb:15:in module:MyModuleC : uninitialized constant MyModuleA::MyModuleB::B (NameError)
Did you mean?  B
        from qualified_constant_references_not_found.rb:14:in module:MyModuleB
        from qualified_constant_references_not_found.rb:9:in module:MyModuleA
        from qualified_constant_references_not_found.rb:8:in main

다음과 같이 Module.nestingAncestors Chain 에서 상수를 찾지 못했지만 제한된 상수참조를 사용하면 Object 에서 상수 참조를 시도하지 않는다. 

오늘은 여기까지 다음에는 Ruby On Rails 의 상수참조에 대해서 알아보고 Ruby의 상수참조와 어떻게 다른지 확인해보겠다.

누군가에게 도움이 되었길 바라면서 오늘의 포스팅 끝~