출판사, 원작자의 저작권의 문제 소지가 발생하는 경우 본 게시물은 삭제될 수 있습니다.
코드를 구성하는데는 크게 두 가지 측면을 고려해야 한다. 첫 번째는 서로 다른 코드가 같은 이름을 가지고 있을 때 어떻게 충돌을 방지할 것인가 하는 문제, 두 번째는 다수의 소스 파일을 어떻게 프로젝트로 구성할 것인가 하는 문제다.
이름 공간
어떤 클래스 내부에 정의된 메서드나 상수의 이름은 그 클래스의 맥락에서만 사용 가능하다.
class Triangle
SIDES = 3
def area
# ...
end
end
class Square
SIDES = 4
def initialize(side_length)
@side_length = side_length
end
def area
@side_length * @side_length
end
end
puts "A triangle has #{Triangle::SIDES} sides"
sq = Square.new(3)
puts "Area of square = #{sq.area}"
실행결과
A triangle has 3 sides
Area of squre = 9
두 클래스에 모두 SIDES라는 상수와 area라는 인스턴스 메소드가 정의 되어 있지만 혼동되는 일은 없다. 인스턴스 메서드에 접근하려면 정의된 클래스를 통해 생성된 객체를 사용해야 하며, 상수에 접근하려면 클래스 이름에 콜론을 붙이고 상수 이름을 지정해야만 한다. 이중 콜론(::)은 루비의 이름공간 충돌을 해결해 주는 연산자다. 이중 콜론의 좌측에는 클래스 이름이나 모듈이름이 와야 하며 우측에는 클래스나 모듈에 정의된 상수를 지정해야 한다.
모듈 또는 클래스 안에 코드를 작성하는 것은 서로 다른 코드를 분리하는 괜찮은 방법이다.
Math::E # => 2.718281828459045
Math.sin(Math::PI/6.0) # => 0.499999999999999999994
루비는 클래스와 모듈의 이름은 단순히 상수에 불과하다. 따라서 클래스나 모듈 내부에 또 다른 클래스나 모듈을 정의하면 내부 클래스 이름은 다른 상수와 같은 이름 공간 규칙을 따르게 된다.
module Formatters
class Html
# ...
end
class Pdf
# ...
end
end
html_writer = Formatters::html.new
클래스와 모듈은 다른 클래스나 모듈 안에 정의될 수 있다. 이러한 중첩에 특별한 제한은 없다.
소스 코드 조직하기
소스코드를 다수의 파일로 분할하는 방법과 분할한 파일을 파일 시스템의 어디에 배치할지에 대해 알아보자.
자바와 같은 언어는 이 문제를 쉽게 해결한다. 이러한 언어에서는 외부 레벨의 클래스는 자신의 파일에 정의되어 있어야 하며, 그 파일은 클래스 이름을 따라서 이름이 붙이도록 되어 있다. 루비를 비롯한 다른 언어들에는 소스코드와 파일에 대한 특별한 제약이 존재하지 않는다.
그렇기 때문에 일관적인 규칙이 없다면 많은 어려움이 발생한다. 루비 커뮤니티에는 실질적ㅇ니 표준이 자리잡아 가고 있다. 예시들을 살펴보자.
작은 프로그램
작고 자기 완결적인 스크립트는 하나의 파일에 저장된다. 하지만 이렇게 하면 프로그램 자동 테스트를 작성하는게 어려워진다. 테스트 코드가 테스트하고자 하는 대상 소스를 로드하려면 프로그램 자체를 실행해야만 하기 때문이다. 따라서 작은 프로그램을 작성하면서 테스트자동화를 위해서는 프로그램을 외부 인터페이스를 제공하는 작은 드라이버와 그 부분을 포함하는 하나 또는 그 이상의 파일로 나눠야 한다. 그래야만 프로그램을 실행하지 않고도 분리되어 있는 각 파일을 실행할 수 있다
예제로 아나그램(어떤 단어의 알파벳 순서를 바꿔도 의미를 가지는 단어)을 검색하는 간단한 프로그램을 구상해보자. 프로그렘에 하나 이상의 단어를 넘겨주면 각 단어의 아나그램을 반환한다.
$ ruby anagram.rb teaching code
Anagrams of teaching; cheating, teaching
Anagrams of code : code, coed
재사용을 고려하지 않고 프로그램을 작성한다면 아래의 코드와 같이 하나의 파일에 작성해도 무방하다.
packaging/anagram.rb
#!/usr/bin/env ruby
require 'optparse'
dictionary = "/user/share/dict/words"
OptionParser.new do | opts |
opts.banner = "Ussage: anagram [ options ] word .."
opts.on("-d", "--dict path", String, "Path to dictionary") do | dict |
dictionary = dict
end
opts.on("-h", "--help", "Show this message") do
puts opts
exit
end
begin
ARGV << "-h" if ARGV.empty?
opts.parse!(ARGV)
rescue OptionParser::ParseError => e
STDERR.puts e.message, "\n", opts
exit(-1)
end
end
# "wombat"을 "abmotw"로 변환한다. 모든 아나그램은 특징을 공유한다.
def signature_of(word)
word.unpack("c*").sort.pack("c*")
end
signatures = Hash.new
File.foreach(dictionary) do | line |
word = line.chomp
signature = signature_of(word)
(signatures[signature] | | = [ ] ) << word
end
ARGV.each do | word |
signature = signature_of(word)
if signatures[signature]
puts "Anagrams of #{word}: #{signatures[signature].join(, ")}"
else
puts "No amagrams of #{word} in #{dictionary}"
end
end
다른사람이 위 코드를 사용하고 싶다면 당황스러울 것이다. 이 코드는 테스트도 없고 패키지화도 되어 있지 않다.
코드를 자세히 살펴보면 세 부분으로 나뉘어 있다. 1) 옵션분석, 2) 사전을 읽어 변환, 3) 명령행 인자에 지정된 단어를 찾아 검색을 수행하고 결과반환
- 옵션 분석기(parser)
- 아나그램 검색 테이블이 저장된 클래스
- 명령행에 지정된 단어를 가져오는 클래스
- 간단한 명령행 인터페이스
앞선 세 부분은 실질적으로 라이브러리화가 가능하며, 마지막 인터페이스 부분을 통해 사용된다.
이러한 파일을 어디에 저장하면 좋을까? 여기에 대해 오래된 루비 커뮤니티에 오래된 관습이 있다. 이 관습은 아오키 미네로의 setup.rb에서 시작되었으며, 이후 루비젬 시스템에서 공식적으로 사용하고 있다. 여기서는 프로젝트 디렉터리를 생성하고 세 개의 서브 디렉터리를 생성한다.
anagram/ <- 프로젝트 디렉터리
bin/ <- 명령행 인터페이스는 여기에 들어간다.
lib/ <- 라이브러리 파일은 여기에 들어간다.
test/ <- 테스트 파일을 여기에 들어간다.
클래스들은 준비된 명령행 인터페이스에서만 사용되지만 다른 개발자가 이 라이브러리를 자신의 코드에서 사용하고 싶어 할 수도 있다는 것을 고려할 필요가 있다. 따라서 사용자를 배려한 코드를 작성할 필요가 있으며 루비의 최상위 이름공간을 직접 작성한 클래스로 오염(?) 시켜서는 안된다. 단 하나의 최상위 레벨 모듈 Anagram만을 생성하며 이 모듈 아래 모든 클래스를 정의한다. 여기서 작성하고자 하는 옵션 분석기 클래스의 전체 이름은 Anagram::Options가 될 것이다.
Options 클래스는 Anagram 모듈 아래에 존재하기 때문에 options.rb 파일은 lib/ 디렉터리 아래의 anagram 디렉터리에 저장되는 것이 적절하다. 이를 통해 코드의 위치를 유추할 수 있다. A::B::C라는 이름을 만나게 되면, c.rb라는 파일이 라이브러리 디렉터리 아래의 a/b/ 아래에 있다고 생각할 수 있기 때문이다. 따라서 저장되는 디렉터리의 구조와 여기에 저장되는 파일의 일부가 결정된다.
anagram/
bin/
anagram <- 명령행 인터페이스
lib/
anagram/
finder.rb
options.rb
runner.rb
test/
... 테스트 파일들
옵션 분석기부터 다뤄보자. 옵션 분석기는 명령행에서 옵션을 배열로 읽어 들여 디렉터리 파일의 경로와 아나그램을 찾고자 하는 단어 목록을 반환하는 일이다.
packaging/anagram/lib/options.rb
require 'optparse'
module Anagram
class Options
DEFAULT_DICTIONARY = "/usr/share/dict/words"
attr_reader :dictionary, :words_to_find
def initialize(argv)
@dictionary = DEFAULT_DICTIONARY
parse(argv)
@words_to_find = argv
end
private
OptionsParse.new do | opts |
opts.banner = "Usage: anagram [ options ] word... "
opts.on("-d", "--dict path", String, "Path to dictionary") do | dict
@dictionary = dict
end
opts.on("-h", "--help", "Show this message") do
puts opts
exit
end
begin
argv = ["-h"] if argv.empty?
opts.parse!(argv)
rescue OptionParser::ParseError => e
STDERR.puts e.message, "\n", opts
exit(-1)
end
end
end
end
단위 테스트 코드는 비교적 간단하다. options.rb 파일은 자기 완결적인 소스 코드이기 때문이다. 테스트 코드를 test/test_options.rb 파일에 작성한다.
packaging/anagram/test/test_options.rb
require 'test/unit'
require 'shoulda'
require_relative '../lib/anagram/options'
class TestOptions < Test::Unit::TestCase
context "specifying no dictionary" do
should "return default" do
opts = Anagram::Options.new(["someword"])
assert_equal Anagram::Options::DEFAULT_DICTIONARY, opts.dictionary
end
end
context "specifying a dictionary" do
should "return it" do
opts = Anagram::Options.new(["-d", "mydict", "someword"])
assert_equal "mydict", opts.dictionary
end
end
context "specifying words and no dictionary" do
should "return the words" do
opts = Anagram::Options.new(["word1", "word2"])
assert_equal ["word1", "word2"], opts.words_to_find
end
end
context "specifying words and a dictionary" do
should "return the words" do
opts = Anagram::Options.new(["-d", "mydict", "word1", "word2"])
assert_equal ["word1", "word2"], opts.words_to_find
end
end
end
$ ruby test/test_options.rb
Run options:
# Runniong tests:
....
Finished tests in 0.00~~
4 tests, 4 assertions, 0 failures, 0 errors, 0 skips
검색 코드(lib/anagram/finder.rb)는 원래 버전을 조금 수정했다. 테스트를 쉽게 만들기 위해 기본 생성자가 파일 이름이 아닌 단어들을 받도록 변경되었다. 다음으로 팩터리 메서드 from_file을 추가했다. 이 메서드는 파일 이름을 받아 이 파일을 사용하는 새로운 Finder를 생성한다.
packaging/anagram/lib/anagram/finder.rb
module Anagram
class Finder
def self.from_file(file_name)
new(File.readlines(file_name))
end
def initialize(dictionary_words)
@signatures = Hash.new
dictionary_words.each do | line |
word = line.chomp
signature = Finder.signature_of(word)
(@signatures[signature] | | = [ ] ) << word
end
end
def lookup(word)
signature = Finder.signature_of(word)
@signatures[signature]
end
def self.signature_of(word)
word.unpack("c*").sort.pack("c*")
end
end
end
이 파일 역시 Finder 클래스는 최상위 레벨 Anagram 모듈 안에 정의되었다. 또한 이 코드도 자기완결적 코드이므로 단위테스트를 작성할 수 있다.
packaging/anagram/test/test_finder.rb
require 'test/unit'
require 'shoulda'
require_relative '../lib/anagram/finder'
class TestFinder < Test::Unit::TestCase
context "signature" do
{ "cat" => "act", "act" => "act", "wombat" = > "abmotw" }.each do
| word, signature |
should "be #{signature} for #{word}" do
assert_equal signature, Anagram::Finder.signature_of(word)
end
end
end
context "lookup" do
setup do
@finder = Anagaram::Finder.new(["cat", "wombat"])
end
should "return word if word given" do
assert_equal ["cat"], @finder.lookup("cat")
end
should "return word if anagram given" do
assert_equal ["cat"], @finder.lookup("act")
assert_equal ["cat"], @finder.lookup("tca")
end
should "return nil if no word matches anagram" do
assert_nil @finder.lookup("wibble")
end
end
end
이 코드를 test/test_finder.rb에 저장하자
$ ruby test/test_finder.rb
Run options:
# Running tests:
...
Finnished tests in 0.003306s, blabla
6 tests, 7 assertions, 0 failures, 0 errors, 0 skips
이 모든 코드가 적절한 위치에 배치 되었다. 사용자가 사용하게 되는 명령한 인터페이스는 가능한 가볍게 만들자. 파일명은 bin/ 디렉터리 아래에 anagram이다(rb확장자는 실행파일에 어울리지 않으므로 생략)
packaging/anagram/bin/anagram
#! /usr/local/rubybook/bin/ruby
require 'anagram/runner'
runner = Anagram::Runner.new(ARGV)
runner.run
이 스크립트는 lib/anagram/runner.rb 파일을 통해 다른 라이브러리들도 함께 읽어들인다.
packaging/anagram/lbi/anagramrunner.rb
require_relative 'finder'
require_relative 'options'
module Anagram
class Runner
def initialize(argv)
@options = Options.new(argv)
end
def run
finder = Finder.from_file(@options.dictionary)
@options.words_to_find.each do | word |
anagrams = finder.lookup(word)
if anagrams
puts "Anagrams of #{word}: #{anagrams.join(', ' )}"
else
puts "No anagrams of #{word} in #{@options.dictionary}"
end
end
end
end
end
이 예제에서는 finder와 options 라이브러리가 runner와 같은 디렉터리 아래에 존재하기 대문에 require_relative에 의해 문제없이 로드된다. 모든 파일이 적절한 위치에 배치되었으며 명령행에서 프로그램을 실행할 수 있다
$ ruby - I lib bin/anagram teaching code
Anagram of teaching: cheating, teaching
Anagram of code: code, coed
끄읕.