IBM®
메인 컨텐츠로 가기
    Korea [국가변경]    이용약관
 
 
   
        제품    서비스 & 솔루션    고객지원 & 다운로드    회원 서비스    
한국 developerWorks   >  Developer CoD  > developerworks

루비 메타프로그래밍, Part 3: 실전 메타프로그래밍





난이도 : 중급
2008년 6월 3일


연재순서
1회(2008년 3월): 프로그램을 작성하는 프로그램
2회(2008년 4월): eval 메서드
3회(2008년 6월): 실전 메타프로그래밍


Part 2에서는 루비의 eval 메서드를 이용하여 런타임에 동적으로 메서드를 추가하는 방법을 살펴보았다. 이번 연재에서는 이러한 메타프로그래밍 기법을 활용해 간단한 ORM(Object/Relational Mapping) 프레임워크를 구현해 보기로 한다. 우선 MySQL 클라이언트를 사용하여 우리가 쓸 데이터베이스를 하나 만들어 두자.


C:\>mysql -uroot -p
Enter password:
mysql> create database example_db default charset utf8;
Query OK, 1 row affected (0.44 sec)


위에서는 example_db라는 이름의 데이터베이스를 생성했다("default charset utf8" 부분은 문자열 데이터 인코딩에 유니코드 인코딩인 UTF-8 인코딩을 사용하라는 의미다). 이제 루비젬 유틸리티를 사용하여 mysql 드라이버를 설치하자.


C:\>gem install mysql
Bulk updating Gem source index for: http://gems.rubyforge.org
Successfully installed mysql-2.7.3-mswin32
Installing ri documentation for mysql-2.7.3-mswin32...
Installing RDoc documentation for mysql-2.7.3-mswin32...
While generating documentation for mysql-2.7.3-mswin32


irb에서 MySQL 데이터베이스에 직접 연결해 보자.


C:\>irb --simple-prompt
>> require "mysql"
=> true
>> connection = Mysql.init
=> #<Mysql:0x2e85c7c>
>> connection.real_connect("localhost", "root", "", "example_db")
=> #<Mysql:0x2e85c7c>


위에서 real_connect 메서드는 실제로 MySQL 데이터베이스에 접속할 때 사용하는 메서드다. "localhost", "root", "", "example_db"는 각각 MySQL 서버 호스트네임, 사용자 ID, 비밀번호, 데이터베이스 이름을 가리킨다. 이제 MySQL 클라이언트로 돌아가서 example_db에 users 테이블을 정의하자.


mysql> use example_db;
Database changed
mysql> create table users (
    ->   id int not null auto_increment primary key,
    ->   name varchar(8) not null,
    ->   email varchar(60) not null,
    ->   created_at datetime not null
    -> );
Query OK, 0 rows affected (0.10 sec)


users 테이블에 레코드를 몇개 추가해 보자.


mysql> insert into users (name, email, created_at) values ("Chul-Soo Kim",
"chulsoo@hanmail.net", "2008-05-02 10:30:00"), ("Young-hui Lee",
"younghui@naver.com", "2008-05-10 14:25:00");


앞서 나온 connection 객체를 통해 users 테이블에 SQL 쿼리를 던질 수 있다.

 

>> result = connection.query("select * from users")
=> #<Mysql::Result:0x110df50>
>> result.fetch_row
=> ["1", "Chul-Soo Kim", "chulsoo@hanmail.net", "2008-05-02 10:30:00"]
>> result.fetch_row
=> ["2", "Young-hui Lee", "younghui@naver.com", "2008-05-10 14:25:00"]


SQL 쿼리를 사용하지 않고도, 데이터베이스로부터 데이터를 불러올 수 있도록 액티브레코드 스타일의 User 모델 클래스를 정의해 보자.

Listing 1. user.rb

class User < Base
end


이제 "User.find(2)" 또는 "u = User.new", "u.save" 등의 코드를 통해 users 테이블로부터 데이터를 읽어들이거나 새로운 레코드를 추가할 수 있어야 한다. 위의 코드에서 User 클래스는 Base 클래스로부터 상속되고 있다. User 클래스의 실질적인 ORM 기능은 Base 클래스에서 구현될 것이다. 다음은 Base 클래스를 구성하는 코드다.

Listing 2. base.rb

require 'mysql'

class Base
  attr_accessor :new_record

  class <<self
    attr_accessor :table_name, :attributes
  end

  def self.inherited(subclass)
    subclass.table_name = underscore(subclass.name) + "s"
    subclass.attributes = []

    result = query("desc #{subclass.table_name}")
    while (row = result.fetch_row)
      subclass.attributes << row[0]
      subclass.class_eval <<-EOS
        def #{row[0]}
          @#{row[0]}
        end
        def #{row[0]}=(value)
          @#{row[0]} = value
        end
      EOS
    end
  end

  def self.query(str)
    unless @connection
      @connection = Mysql.init
      @connection.options(Mysql::SET_CHARSET_NAME, "utf8")
      @connection.real_connect("localhost", "root", "", "example_db")
    end
    
    @connection.query(str)
  end

  def self.insert_id
    @connection.insert_id
  end

  def self.underscore(camel_cased_word)
    camel_cased_word.to_s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
  end

  def self.find(id)
    record = self.new
    record.new_record = false
  
    result = query("select * from #{@table_name} where id=#{id}")
    if (row = result.fetch_row)
      self.attributes.each_with_index do |a, i|
        record.send("#{a}=", row[i])
      end
    end
    record
  end

  def initialize(attributes={})
    self.new_record = true

    self.class.attributes.each do |a|
      self.send("#{a}=", nil)
    end
  end
  
  def save
    if self.new_record
      self.class.query("insert into #{self.class.table_name} " +
      "(#{self.class.attributes[1..-1].join(',')}) values " +
      "(#{self.class.attributes[1..-1].map {|a| self.send(a).inspect}.join(',')})")
      self.id = self.class.insert_id
    else
      self.class.query("update #{self.class.table_name} set " +
      "#{self.class.attributes[1..-1].map {|a| "#{a} = #{self.send(a).inspect}"}.join(',')} " +
      "where id=#{self.id}")
    end
    true
  end
end


코드에 대한 설명은 조금 뒤로 미루고 우선 User 클래스를 직접 이용해 보자.


>> load "base.rb"
=> true
>> load "user.rb"
=> true
>> u = User.find(2)
=> #<User:0x110457c @created_at="2008-05-10 14:25:00", @name="Young-Hui Lee", @new_record=false, @email="younghui@naver.com", @id="2">
>> u.name
=> "Young-Hui Lee"
>> u.email
=> "younghui@naver.com"


위에서는 User 클래스를 통해 id 값이 2인 사용자 레코드를 불러오고 있다. 이번에는 users 테이블에 새로운 레코드를 추가해 보자.


>> u = User.new
=> #<User:0x1106fac @created_at=nil, @name=nil, @new_record=true, @email=nil, @id=nil>
>> u.name = "Gildong"
=> "Gildong"
>> u.email = "gildong@gmail.com"
=> "gildong@gmail.com"
>> u.created_at = "2008-05-26 20:30:00"
=> "2008-05-26 20:30:00"
>> u.save
=> true


이처럼 Base 클래스는 기본적인 ORM 프레임워크의 기능을 제공한다. 이제 Base 클래스 코드의 주요 부분을 살펴보자.

Base 클래스 코드 앞 부분의 inherited 메서드는 모델 클래스가 Base 클래스를 상속할 때 자동으로 호출되는 클래스 메서드다. 이곳에서는 모델 클래스의 이름으로부터 매핑될 데이터베이스 테이블의 이름으로 자동으로 유추하고("User" => "users"), 동시에 데이터베이스로부터 해당 테이블의 스키마 정보를 읽어와서 각 레코드 필드에 해당하는 접근자 메서드를 클래스에 추가하고 있다.

기존 ORM 프레임워크에서는 데이터베이스 테이블의 스키마 정보에 맞춰 모델 클래스 속성(attribute)에 대한 접근자 메서드를 별도로 정의해 주어야 했다. Base 클래스는 루비의 메타프로그래밍 기법을 통해 이와 같이 반복적인 코딩 작업을 자동화한다(이는 레일스의 액티브레코드가 사용하는 방식이기도 하다).

query 메서드는 SQL 쿼리를 입력받아 데이터베이스에 질의를 한 후 결과값(Result Set)을 리턴해주는 클래스 메서드다. 그 외의 주요 메서드로는 find와 save 등이 있다. find 메서드는 레코드의 id 값을 인자로 받아 그에 해당하는 레코드를 찾아 리턴해준다. save 메서드는 레코드가 새 레코드인 경우에는 데이터베이스 테이블에 레코드를 추가하고, 이미 존재하는 레코드인 경우에는 그 값을 업데이트한다.

Base 클래스는 겨우 70여줄의 루비 코드로 작성되어 있지만 모델 클래스의 이름으로부터 데이터베이스 테이블의 이름을 유추하고 모델 클래스 속성에 대한 접근자 메서드의 정의를 자동화하는 등 액티브레코드의 핵심 기능을 모두 구현하고 있다. 이처럼 루비의 동적인 특성과 메타프로그래밍을 적절히 활용하면 프레임워크 사용시 불필요한 설정이나 반복적인 코딩 작업을 최소화할 수 있다.

지금까지 3회에 걸쳐 루비 메타프로그래밍에 관해 다뤄보았다. 메타프로그래밍은 그 사용법이 어려워 그 동안 주류 프로그래밍 언어에서 널리 사용되지는 못했다. 루비는 메타프로그래밍에 대한 직관적인 인터페이스를 제공하고 있으며, 이에 따라 루비 프로그래머들 사이에서는 점차 메타프로그래밍 사용이 늘어나는 추세다. 직접 프레임워크 등의 라이브러리를 개발하고자 하는 프로그래머에게 메타프로그래밍은 강력한 도구가 될 것이다.



위로




필자 소개

황대산황대산 me@daesan.com

미국 예일대학교에서 수학을 전공했고, 현재 국내에서 프리랜서 개발자로 활동중이다. 루비/레일스 에반젤리스트를 자임하며 틈틈이 잡지 등에 관련 글을 기고하는 등 활발히 활동하고 있다. ‘웹 개발 2.0 루비 온 레일스’라는 책을 집필했다




이 문서 북마킹 하기

mar.gar.in mar.gar.in naver naver eolin eolin del.icio.us del.icio.us




developerWorks에 소개되었으면 하고 바라던 주제가 있으시거나, 관심 있는 기술에 대한 전문가의 지식과 견해가 궁금하시다면 Developer CoD 코너에 원하는 주제를 신청해주세요. 해당 분야 전문가를 필자로 섭외해, 여러분이 원하는 주제에 대한 맞춤형 컨텐츠를 제작해드립니다.
developer CoD 신청양식 다운로드   MS워드 아이콘   아래아한글 아이콘



[지난 Developer CoD 보기]


위로


사이트 여행

dW 커뮤니티
포럼 | 블로그 | Spaces
dW Student Community

로컬 컨텐츠

행사 및 세미나

기획 기사

개발자 입문

튜토리얼 및 교육

TOP 10 인기자료

SW 다운로드

RSS 피드

뉴스레터
 
  
자바스크립트가 작동이 중지되었습니다. 이 기능을 수행하시려면 브라우저에서 자바스크립스트를 작동시켜 주시거나 이곳을 클릭해주세요.

Special offers
Screencast
IBM SOA Sandbox 시험판
dW Student Community
로보코드
코드 트레이닝


    IBM 소개 개인정보 보호정책 문의