Ruby

모듈

본 토픽은 현재 준비중입니다.공동공부에 참여하시면 완성 되었을 때 알려드립니다.

프로그램은 작고 단순한 것에서 크고 복잡한 것으로 진화한다. 그 과정에서 코드의 재활용성을 높이고, 유지보수를 쉽게 할 수 있는 다양한 기법들이 사용된다. 그 중의 하나가 코드를 여러개의 파일로 분리하는 것이다. 이를 통해서 얻을 수 있는 효과는 아래와 같다.

  • 자주 사용되는 코드를 별도의 파일로 만들어서 필요할 때마다 재활용할 수 있다.
  • 코드를 개선하면 이를 사용하고 있는 모든 애플리케이션의 동작이 개선된다.
  • 코드 수정 시에 필요한 로직을 빠르게 찾을 수 있다.
  • 필요한 로직만을 로드해서 메모리의 낭비를 줄일 수 있다.

모듈이란

루비에서는 필요에 따라서 로드 할 수 있도록 만들어진 파일을 모듈(module)이라고 부른다. 모듈을 만들고 모듈을 로드하는 방법을 알아보자.

모듈이 없다면

우선 모듈이 없는 애플리케이션을 하나 만들어보자. 이 코드의 파일명은 main.rb 다.

def welcome()
    return 'Hello world'
end

print welcome()

위의 코드는 아무런 문제가 없는 코드다. 하지만 welcome 메소드가 자주 사용되는 것이라고 가정해보자. 이런 경우 이것이 필요한 애플리케이션마다 이 메소드를 정의해서 사용하는 것은 유지보수도 어렵고 낭비가 될 것이다. 이럴 때 모듈이 필요하다. 메소드 welcome을 모듈로 만들어보자.

모듈의 사용

새로운 파일을 만든다. 이름은 greeting.rb이다.

greeting.rb

#greeting.rb
module Greeting
    def Greeting.welcome()
		return 'Hello world'
	end
end

루비에서 모듈은 module 구문으로 시작한다. 위의 예에서 module 뒤에 따라오는 Greeting은 모듈의 이름이 된다. 모듈을 사용하는 쪽에서는 모듈의 이름을 이용해서 메소드 Greeting.welecom와 같은 모듈의 메소드를 사용할 수 있다.

main.rb의 내용을 다음과 같이 변경한다.

main.rb

require './greeting'
puts Greeting.welcome()

이전 예제와 비교했을 때 결과는 같다. 하지만 메소드 welcome을 main.rb의 외부 파일로 분리했다. 다음은 위의 코드에 대한 분석이다.

require './greeting'

require는 모듈을 로드할 때 사용하는 명령이다. greeting은 불러오려고 하는 모듈의 이름인데, 모듈의 이름은 확장자를 제외한 파일명을 사용한다. require './greeting'은 main.rb와 같은 디렉토리에 있는 greeting.rb라는 이름의 파일을 찾아서 로드한다.

Greeting.welcome

Greeting은 모듈의 이름이다. 이것은 파일의 이름이 아니다. greeting.rb 파일 내의 모듈 선언부인 module Greeting의 Greeting이 이 값이다. 즉 메소드 welcome 은 로드(require) 후에 바로 사용할 수 있는 것이 아니라 Greeting이라는 모듈의 이름을 통해서 사용할 수 있게 되는 것이다. 이러한 개념을 네임스페이스(namespace)라고 한다.

네임스페이스

네임스페이스가 무엇인가를 정의하기에 앞서서 파일을 생각해보자. 파일은 데이터를 보관하고 있는 일종의 컨테이너다. 그리고 이 컨텐이너는 파일명으로 식별이 된다. 파일의 수가 많아지면서 파일을 관리하는 것이 점점 어려워진다. 그래서 고안된 것이 바로 디렉토리다. 디렉토리를 이용하면 같은 이름의 파일이 하나의 컴퓨터에 존재할 수 있다. 파일명의 충돌을 회피 할 수 있게 된 것이다. 네임스페이스란 간단하게 디렉토리와 같은 것이라고 생각하자. 하나의 에플리케이션에는 다양한 모듈을 사용하게 된다. 그런데 모듈이 서로 다른 개발자에 의해서 만들어지기 때문에 같은 이름을 쓰는 경우가 생길 수 있다. 이런 경우 먼저 로드된 모듈은 나중에 로드된 모듈에 의해서 덮어쓰기 되기 때문에 이에 대한 대책이 필요하다. 네임스페이스가 필요해지게 되는 것이다.

만약 환영인사를 언어별로 다르게 하고 싶다면 어떻게 해야 할까? 3개의 파일을 만든다.

greeting_en.rb

module Greeting_en
    def welcome
		return 'Hello world'
	end
end

greeting_ko.rb

module Greeting_ko
    def welcome
		return '안녕 세계'
	end
end

main.rb

require './greeting_en'
require './greeting_ko'
puts welcome()

위의 코드는 무언가 이상하다. 메소드 welcome은 greeting_en.rb에도 있고 greeting_ko.rb에도 있다. 위의 코드에서 호출하고 있는 welcome이 누구의 welcome인지 불분명하다. 실행시켜보면 실제로 에러가 발생한다. 아래와 같이하면 welcome이 어디에 속한 메소드인지 분명해진다.

main.rb

require './greeting_en'
require './greeting_ko'
puts Greeting_en.welcome()
puts Greeting_ko.welcome()

여기서 Greeting_en, Greeting_ko가 네임스페이스다. Geeting_en과 Greeting_ko라는 네임스페이스가 있기 때문에 같은 이름인 메소드 welcome이 어떤 메소드인지를 분명하게 명시할 수 있다.

모듈 기본 디렉토리 지정

아래 내용은 중급 주제에 속하기 때문에 프로그래밍 초심자라면 모듈 기본 디렉토리의 개념이 무엇인지만 파악하고 이러한 기능이 필요할 때 이곳을 다시 찾아와서 꼼꼼하게 살펴보는 것을 권한다.

지금까지 require를 이용해서 모듈을 로드하는 방법을 알아봤다. 만약 어떤 모듈이 다양한 에플리케이션에서 공통적으로 사용된다면 어떻게 해야할까? 각각의 애플리케이션 디렉토리에 모듈을 포함시키면 된다. 하지만 이렇게 하면 모듈의 내용에 변화가 생겼을 때 모든 모듈의 내용을 업데이트해야 하는 불편함이 있다. 이러한 문제를 해결하려면 어떻게 해야할까? 모듈 기본 디렉토리를 사용하면 된다. 이것의 사용법을 알아보자.

아래의 루비의 명령을 콘솔에서 실행해보자.

puts $:

실행결과는 아래와 같다.

puts $:

$:는 특수한 배열이다. 이 배열 안에는 require나 load 메소드를 이용해서 로딩하려는 파일을 찾는 디렉토리가 저장되어 있다. 이 배열을 통해서 모듈 기본 디렉토리의 목록과 우선순위를 알 수 있다. 아래는 위의 결과를 보기 좋게 정리해본 것이다.

아래의 내용은 루비가 설치된 환경에 따라서 결과가 다를 것이다.
  1. /usr/local/lib/site_ruby/1.9.1
  2. /usr/local/lib/site_ruby/1.9.1/x86_64-linux
  3. /usr/local/lib/site_ruby
  4. /usr/lib/ruby/vendor_ruby/1.9.1
  5. /usr/lib/ruby/vendor_ruby/1.9.1/x86_64-linux
  6. /usr/lib/ruby/vendor_ruby
  7. /usr/lib/ruby/1.9.1
  8. /usr/lib/ruby/1.9.1/x86_64-linux

예를들면 greeting.rb라는 모듈을 로드 할 때 루비는 위의 목록 중 제일 위에 있는 /usr/local/lib/site_ruby/1.9.1/greeting.rb 파일을 찾는다. 이 파일이 존재하지 않는다면 이번엔 /usr/local/lib/site_ruby/1.9.1/x86_64-linux/greeting.rb 파일을 찾는다. 이 파일이 존재한다면 로드하고, 존재하지 않는다면 순차적으로 파일의 존재 여부를 확인하게 되는 것이다. 위에서 열거된 디렉토리에 루비의 모듈을 위치시키면 모든 루비 애플리케이션에서 사용할 수 있게 되는 것이다.

위의 목록에서 site_ruby 디렉토리는 주로 자신이 만든 모듈을 위치시킨다. vender_ruby는 루비의 배포판(MRI, jruby)의 필요에 따른 모듈이 위치한다. 루비 버전에 종속적인 모듈은 해당 버전(여기선 1.9.1)의 디렉토리에 위치시키고, 운영체제에 종속적인 모듈(여기선 x86_64-linux)은 해당 운영체제에 알맞는 디렉토리에 위치시킨다.

모듈 로드의 우선순위

모듈을 1번 /usr/local/lib/site_ruby/1.9.1과 2번 /usr/local/lib/site_ruby/1.9.1/x86_64-linux 에 위치시켜보자.

필자와 똑같이 하지 말고 puts $:를 통해서 출력된 내용을 보고 따라해야 한다.

/usr/local/lib/site_ruby/1.9.1/greeting.rb

module Greeting
    def Greeting.welcome()
		return 'hello'
	end
end

/usr/local/lib/site_ruby/1.9.1/x86_64-linux/greeting.rb

module Greeting
    def Greeting.welcome()
		return 'world'
	end
end

main.rb

require 'greeting'
Greeting.welcome()

main.rb를 실행한 결과는 hello가 출력된다. 첫 번째 파일인 /usr/local/lib/site_ruby/1.9.1/greeting.rb 파일을 삭제 한 후에 다시 시도해보자. world가 출력될 것이다. 첫 번째 파일이 사라졌기 때문에 루비는 두 번째 파일인 /usr/local/lib/site_ruby/1.9.1/x86_64-linux/greeting.rb 모듈을 사용한 것이다.

  • 봤어요 0명

댓글

댓글 본문
  1. 홍즈
    greeting.rb 파일을 main.rb와 같은 폴더에 배치하고
    require './greeting'
    을 통해 Greeting.welcome()을 호출하면

    C:/Ruby200-x64/lib/ruby/2.0.0/rubygems/core_ext/kernel_require.rb:53:in `require[Decode error - output not utf-8]

    이런 오류메세지가 뜹니다.

    반면에 알려주신것과 같이 puts $:을 통해 디렉토리를 알고
    (저의경우) C:\Ruby200-x64\lib\ruby\site_ruby\2.0.0\x64-msvcrt\
    폴더에 greeting.rb 파일을 옮긴다음에
    require 'greeting' 으로 하고 Greeting.welcom()하면 정상작동하구요.

    즉 질문의 요지는 실행코드와 같은 폴더에 모듈코드를 넣으면 에러가발생하는데 왜그럴까요?
  2. egoing
    어이쿠 고맙다뇨. 꼼꼼하게 챙겨주시는 분이 있어서 든든할 따름이죠. 일단 말씀하신 바는 제가 생각을 좀 해보고 다시 피드백을 드릴께요.
    지금 만들고 있는 커리큘럼은 시리즈로 기획하고 있는데요. 제가 할 수 있는 범위에서 모든 프로그래밍 언어에 대한 커리큘럼을 만들어 보고 싶습니다.
    사실 언어라는 것이 공통되는 부분과 공통되지 않는 부분이 있잖아요? 그래서 공통되는 부분에 대해서는 똑같은 설명 똑같은 동작 방식의 예제를 사용하고, 공통되지 않는 부분 (문법이나, 그 언어의 기능 등)에 대한 변주를 추가하는 방식으로 컨텐츠를 만드는 실험을 하고 있습니다. 말하자면 프로그래밍 언어 커리큘럼의 프래임웍을 만들어보고 싶은거구요. 이것을 통해서 얻을 수 있는 효과는 생산자는 빠르게 만들 수 있고, 학습자도 빠르게 배울 수 있다는 점을 기대하고 있어요.
    문제는 공통된 프래임을 강조하다보니까 각 언어의 특성을 충분히 반영하지 못할 가능성이 있다는 점에서 고민이 사실 됩니다.
    특히 앞선 수업에서 조언 주셨던 반복문 보다는 반복자를 실제로는 더 많이 사용한다는 부분이나, 지금 조언 주신 모듈은 클래스의 확장이나 클래스의 네임스페이스의 용도로 사용된다는 부분은 어찌보면 루비라는 언어를 잘 설명하는 접근 방법이지만, 자바스크립트 같은 언어에서는 또 지금과 같은 순서가 오히려 좋을수도 있을 것 같거든요.
    좀 다른 이야기지만 객체지향에 대한 부분이 제일 뒤에 있는 이유는, 제 경험칙에 의하면 초심자들이 가장 많이 이탈하는 부분 중의 하나가 객체지향이었던 것 같습니다. 하지만 객체지향의 개념을 모르고도 프로그래밍을 할 수는 있기 때문에 가급적 부담스러운 내용을 뒤로 빼고 싶다는 욕심에서 객체지향이 뒤에 있게 된 것인데 역시 고민스럽습니다.
    이렇게 함께 고민해주시는 분이 계셔서 얼마나 힘이 되는지 몰라요. 방금 제가 말씀 드린 바에 대해서 피드백 주시면 너무 좋겠구요. 조언 주신 부분에 대해서는 계속 생각해볼께요. 어차피 동영상으로 만들기 전까지는 계속 내용이나 구조는 바꾸고 있어요.
    맛있는 점심 되세요 :)
    대화보기
    • Tw Shim
      음.. 제 의견입니다만.."모듈명.메서드명" 으로 잘 콜하고 있다가 갑자기 "메서드명"만으로 콜하려고 하는부분에서 부터 개연성이 부족해지는것 같아요.
      모듈은 클래스를 설명하신 다음에 include, extend와 함께 다루시고 require만 설명하는편이 좋지 않았을까요?
      실제쓸때도 모듈은 클래스를 확장하기 위한 인터페이스나 네임스페이스 용도 로 사용되지 저런식으로 유틸리티 묶음으로 사용되는 경우가 드물기도 하구요.
      그건 그렇고 저야말로 피드백 들어주셔서 감사합니다. 헤헤.
      대화보기
      • egoing
        예 말씀하신 바가 더욱 정확한 설명인 것 같습니다만, 제가 설명하고 있는 맥락은 네임스페이스라는 것을 강조하기 위해서이기 때문에 기존의 설명을 조금 다듬어봤습니다. 즉 에러 자체의 원인을 설명하려고 했던 것은 아니고, 네임스페이스가 필요한 이유를 설명하기 위해서 부러 네임스페이스를 없는 코드를 만들어봤습니다.
        아래는 변경한 내용입니다.
        "위의 코드는 무언가 이상하다. 메소드 welcome은 greeting_en.rb에도 있고 greeting_ko.rb에도 있다. 위의 코드에서 호출하고 있는 welcome이 누구의 welcome인지 불분명하다. 실행시켜보면 실제로 에러가 발생한다. 아래와 같이하면 welcome이 어디에 속한 메소드인지 분명해진다."
        혹시 제가 알려주신 맥락을 잘못 파악하고 있다면 알려주세요. 그리고 피드백 주셔서 너무나 감사합니다!
        대화보기
        • Tw Shim
          http://codepad.org/9C5ulwmq
          >> main.rb를 실행해보면 에러가 발생한다. 이유는 메소드 welcome이 greeting_ko에 속한 것인지, greeting_en에 속한 것인지 불분명하기 때문이다. 아래와 같이 하면 분명해진다.
          루비는 오픈클래스라서 동일한 선언이 두개있으면 뒤의 선언이 우선됩니다.
          저경우에 에러가 나는 이유는 'main의 스코프에 welcome이 없어서' 일것 같아요.