Passenger (a.k.a. mod_rails)

[Development]

원문(http://inocrazy.com/docs/8)에서 좀 더 깔끔하게 보실 수 있습니다.

Phusion Passenger (a.k.a. mod_rails)

Phusion Passenger는 네덜란드의 Phusion사에서 개발된 Apache web server의 module이다.(mod_rails로도 알려져있다.) PHP Application을 배포하는 것처럼, Rails Application에서 upload-and-go(업로드 후, 바로 실행)를 가능하게 해준다. 이 문서에서는 기존 Rails Deployment의 문제점을 살펴본 후, Passenger의 설치와 설정 방법에 대해서 알아볼 것이다.



Deployment : Rails vs PHP

Rails를 실전에 도입하는데 있어서 큰 장벽 중의 하나는 배포 환경의 문제이다. 최적화된 성능을 위해서 여러가지 선택을 해야만 한다. 우선 어떤 웹서버를 사용할 것인가(Apache 2.2 + mod_proxy_balancer or Nginx)를 결정해야 하고, 어플리케이션의 규모에 적절한 Mongrel 클라이언트의 개수를 결정해야 한다. 또한 이 Mongrel을 감시하기 위한 모니터링 데몬인 Monit이나 God 등도 설정해 주어야 한다. Mongrel의 경우, 하나의 클라이언트가 수십 메가씩 메모리를 점유하기 때문에 단순히 호스팅을 받아서는 구성할 수도 없다. 이 과정을 조금이라도 편하게 하기위해서 Capistrano를 도입해서 배포를 자동화할 수 있지만, 그를 위해서 또 다른 설정을 해야만 한다. 문제는 이러한 복잡한 설정을 통해서도 만족할만한 성능을 얻기가 어렵다는 것이다.


PHP의 경우, 개발한 어플리케이션을 호스팅 서버에 올리기만 하면 모든 배포가 끝나게 된다. 서버가 여러 대인 경우에도 rsync 한방이면 깔끔하게 정리된다. 가장 중요한 것은 이렇게 간단한 배포 방식에도 불구하고 꽤 괜찮은 성능을 보여준다는 것이다. 그에 반해서 Rails의 개발자는 너무도 많은 설정과 고민을 해야만 하고, 그에 반해 성능은 만족스럽지 못하다. Rails의 이러한 고민들이 어느날 혜성처럼 등장한 Phusion Passenger에 의해 해결되었다.



설치하기

Passenger는 두 가지 형태의 설치방법을 제공한다. gem 형태로 설치한 후에 빌드할 수도 있고, 소스코드를 다운받아서 빌드할 수도 있다. 설치방법은 두 방식 모두 비슷하므로, 여기서는 좀 더 쉬운 방식인 gem을 이용한 설치방법을 살펴보자. (소스로 설치하는 경우, 설치가 완료된 후에도 설치된 mod_passenger.so 모듈이나 passenger-spawn-server 실행파일에 접근해야 하기 때문에, 소스 디렉토리를 삭제하면 안된다.)


  1. passenger gem을 설치한다.

    1. $ gem install passenger

  2. passenger module을 빌드한다.

    1. $ passenger-install-apache2-module

Passenger에서는 사용하기 쉬운 passenger installer를 제공한다. shell에서 passenger-install-apache2-module 명령어를 실행시키면 아래 화면과 같이 installer의 시작 화면이 표시된다. 설치는 스텝별로 설명과 함께 진행되기 때문에 크게 어려운 점은 없을 것이다. Passenger 설치 시에 주의해야할 사항 몇 가지를 정리해보자.


passenger01.png

passenger 설치 시작 화면


Apache bin 디렉토리를 PATH에 설정하기

passenger installer의 첫번째 단계는 설치에 필요한 소프트웨어를 찾는 것이다. 이때 Apache2 development header나 APR development header가 필요한데, Apache의 bin 디렉토리가 PATH에 설정되어있지 않을 경우, 아래와 같이 not found 메시지를 보여주게 된다. 또한 친절하게도 해당 문제에 대한 해결책까지 함께 보여준다. 하지만 Apache의 bin 디렉토리를 PATH에 추가함으로써 간단하게 해결할 수 있다.


passenger03.png

Apache 관련 header를 찾을 수 없다는 메시지


  1. $ export PATH=/usr/local/apache2/bin:$PATH

boost/non_type.hpp 파일 문제

이 문제는 내 환경에서만 발생하는 것인지 모르지만, 여러 대의 서버에서 테스트한 결과 동일한 현상이 발생하여 정리해둔다. 위의 Apache 설정을 한 후에 passenger installer를 다시 실행시키면 필요한 소프트웨어가 모두 발견되고, Apache module을 컴파일하고 설치하는 스텝으로 넘어가게 된다. 그런데 컴파일 중 아래와 같은 오류 메시지가 발생하였다.


passenger04.png


소스코드를 살펴보면 Boost C++ Library의 non_type.hpp 라는 파일을 찾지 못 해서 발생하는 에러인데, 해당 파일이 있어야할 ext/boost 디렉토리에 존재하지 않았다. 결국 이 문제를 해결하기 위해서 Boost C++ Library를 다운받아서 non_type.hpp 파일을 ext/boost 디렉토리에 복사한 후, 설치를 완료할 수 있었다.


설치가 완료된 후 아래와 같이 Apache의 httpd.conf 파일을 설정하는 방법에 대한 설명이 나오게 된다.


passenger05.png



설정하기

httpd.conf 파일을 열어서 Passenger module의 설정을 추가한다.

  1. LoadModule passenger_module /usr/local/lib/ruby/gems/1.8/gems/passenger-1.0.1/ext/apache2/mod_passenger.so
    RailsSpawnServer /usr/local/lib/ruby/gems/1.8/gems/passenger-1.0.1/bin/passenger-spawn-server
    RailsRuby /usr/local/bin/ruby

VirtualHost의 Root로 설정하기

기본적으로 PHP Application을 설정하는 것과 동일하다. 여기서는 inocrazy.com 이라는 Rails Application을 설정하는 것을 가정해보자. Document Root는 /home/inocrazy/www/current/public 이며, inocrazy.com을 도메인으로 설정할 것이다. httpd.conf 파일에 VirtualHost 설정을 추가한다.

  1. <VirtualHost *:80>
       ServerName inocrazy.com
       DocumentRoot /home/inocrazy/www/current/public
    </VirtualHost>

Sub 디렉토리로 설정하기

기존의 superkdk.com 사이트의 inocrazy라는 디렉토리에 inocrazy.com Rails Application을 설정하는 경우를 생각해보자. superkdk.com 사이트의 Document Root는 /home/superkdk/www이다.  우선적으로 할 일은 superkdk.com의 Document Root에 inocrazy라는 symlink를 만드는 것이다.

  1. $ ln -s /home/inocrazy/www/current/public /home/superkdk/www/inocrazy

그 후에 RailsBaseURI 옵션을 VirtualHost 설정에 추가해주면 된다.

  1. <VirtualHost *:80>
       ServerName superkdk.com
       DocumentRoot /home/superkdk/www
       RailsBaseURI /inocrazy
    </VirtualHost>


설정 옵션들

Passenger에서 사용할 수 있는 옵션들에 대해서 살펴보자.


RailsSpawnServer <filename>

Passenger spawn server의 경로를 설정한다. 위에서 살펴본 것처럼 설치 시에 정확한 경로를 알려주므로 해당 경로로 설정해두면 된다. 만약 이 옵션이 설정되지 않을 경우, $PATH 경로에서 passenger-spawn-server 라는 실행파일을 찾게된다.


RailsBaseURI <uri>

위에서 언급한 것처럼 Sub 디렉토리에 Rails Application을 설정하는 경우 설정한다. 각 VirtualHost마다 설정할 수 있다.


RailsAutoDetect <on|off>

Virtual Host의 Document Root가 Rails Application인지를 자동으로 찾아낼지 여부를 설정한다. 기본값은 on이다.


RailsAllowModRewrite <on|off>

Passenger가 mod_rewrite 규칙들을 override할지 여부를 설정한다. 기본값을 off이다.


RailsRuby <filename>

사용할 ruby 인터프리터의 경로를 설정한다.


RailsEnv <string>

기본적인 RAILS_ENV 변수값을 명시한다. 주의해야할 것은 이 옵션이 Global 옵션이라는 것이다. 따라서 각 Virtual Host마다 RAILS_ENV를 설정하기 위해서는, 각 Rails Application의 config/environment.rb에서 RAILS_ENV= 로 설정하는 방법 밖에 없다. (개인적으로 이 옵션은 VirtualHost 옵션으로 변경되어야 한다고 생각된다.)


RailsMaxPoolSize <integer>

동시에 실행될 Rails Application의 인스턴스 수를 설정한다. 이 값을 높게 설정할 경우, 메모리 사용량은 그만큼 높아지지만, 동시접속 처리능력은 (당연히) 증가하게 된다. Global 옵션으로서 기본값은 20이다.

(PhusionRuby Enterprise Edition을 사용하면 메모리 사용량을 33% 정도 감소시킬 수 있다고 한다.)


RailsPoolIdelTime <integer>

Rails Application의 인스턴스의 유휴시간을 설정한다. 해당 유휴 시간 동안 아무 일도 하지않은 인스턴스는 메모리 보존을 위해서 shutdown되게 된다. 매뉴얼에서는 하나의 Rails 웹페이지에서 머무는 평균 시간의 2배 정도를 설정하는 것을 추천하고 있다. Global 옵션으로서 기본값은 120이다.


RailsUserSwitching <on|off>

사용자 전환을 실행할지 여부를 설정한다. 기본값은 on이다.


RailsDefaultUser <username>

사용자 전환이 실행될 경우, 전환한 사용자 계정을 설정한다. 기본값은 nobody이다.



Rails Application을 재시작하기

설정을 변경하거나 새롭게 배포한 후에는 Rails Application을 재시작해야한다. Mongrel 기반에서는 Mongrel 클라이언트를 모두 재시작하는 방법을 사용하였다. 그러면서 정확하게 죽지않는 개새끼(mongrel) 문제도 발생하곤 했다. Passenger에서는 두 가지 재시작 방법을 제공한다.


  1. Apache 재시작하기
  2. tmp/restart.txt 시간 갱신하기

첫 번째 방법은 (당연하지만) Apache를 재시작하는 것이다. 하지만 여러 개의 VirtualHost가 존재하는 서버에서 하나의 Rails Application을 재시작하기 위해서 매번 Apache를 재시작시키는 것은 부담되는 일이다.  이를 위해 Passenger에서는 특정 Rails Application만 재시작할 수 있는 방법을 제공한다. tmp 디렉토리에 restart.txt 파일을 생성하거나 갱신할 경우, 해당 Rails Application을 재시작한다. shell에서 touch 명령어를 통해서 간단하게 restart.txt를 생성하거나 갱신할 수 있다.


  1. $ touch tmp/restart.txt


결론

지금까지 Passenger의 기본적인 설치방법과 설정방법에 대해서 살펴보았다. Passenger의 등장으로 인해서 그동안 Rails Deployment에 있어서 장애로 여겨졌던 많은 부분들이 해소될 수 있을 것 같다. PHP만큼 쉽게 배포될 수 있는 환경이 구축됨에 따라서 Rails 개발자들도 좀 더 확산될 수 있을 것 같다. 또한 기존의 Rails Deployment 방식도 Passenger 기반으로 재설정되어야 할 것이다.


참조



이 글은 스프링노트에서 작성되었습니다.

2008/04/17 03:29 2008/04/17 03:29

Rails 2.1 : Dirty Objects

[Development]

원문(http://inocrazy.com/docs/7)에서 좀 더 깔끔하게 보실 수 있습니다.

속성의 변화를 추적하기 - ActiveRecord::Dirty

다음과 같은 경우를 생각해보자. Book이라는 모델이 있고, Book은 category를 나타내는 category_id라는 컬럼을 가지고 있다. Book의 category가 변경될 때마다 (즉, category_id 값이 변할 때마다) 특정 counter 테이블을 변경시켜줘야 한다면 어떻게 구현할 수 있을까? Book이 update되는 경우에 category_id가 변경되었는지 여부를 확인해서 counter 테이블을 갱신해야 할 것이다. 막상 구현하려고 하면, Book의 before_update callback을 이용해야하는 것은 바로 감이 오지만, category_id의 변경여부를 모니터링하는 방법은 바로 해결책을 내놓기가 어렵다.


즉, 위의 예처럼 모델의 특정 속성의 변경 여부를 모니터링할 필요가 있을 때, 쉽게 사용할 수 있는 방법이 Changeset 9127에서 추가되었다. 물론 기존에도 동일한 기능을 하는 acts_as_modified 라는 플러그인이 존재하였으나, Rails에 기능으로서 포함됨으로써 보다 편하게 사용할 수 있게 되었다. ActiveRecord::Dirty 모듈은 ActiveRecord::Base 클래스에 mix-in 되면서, '_changed?', '_change', '_will_change!', '_was' 접미사를 갖는 suffixed attribute method(접미사가 붙은 속성메소드)를 추가한다. suffixed attribute method는 ActiveRecord::AttributeMethodsattribute_method_suffix 클래스 메소드에 의해서 쉽게 추가할 수 있는데, 추후 다른 문서에서 ActiveRecord::AttributeMethods에 대해서 구체적으로 살펴볼 것이다.


ActiveRecord::Dirty에 의해 추가되는 메소드는 다음과 같다.

  • obj.<attr>_changed?
  • obj.<attr>_was
  • obj.<attr>_change
  • obj.<attr>_will_change!
  • obj.changed?
  • obj.changed
  • obj.changes

사용법 파악하기

위에서 열거한 메소드들을 하나씩 살펴보면서 사용법을 파악해보자. Person이라는 모델이 name이라는 속성을 갖는다고 가정하자. (이 단락의 소스코드들은 하나의 코드를 편의상 나누어둔 것이다.)


  1. person = Person.find_by_name('Dongkyu Kim')
  2. person.name          # => 'Dongkyu Kim'
  3. person.name_changed? # => false

'Dongkyu Kim'이라는 name 속성을 갖는 레코드를 가져온다. <attr>_changed? 메소드는 해당 속성의 변경 여부를 알려준다. 단, 이 변경 여부는 저장되기 전에만 확인할 수 있다. 저장이 되면서, 내부에서 변경정보를 유지하고 있는 @changed_attributes 속성이 리셋되기 때문이다.


  1. person.name = 'Kim Dongkyu'
  2. person.name_changed? # => true
  3. person.name_was      # => 'Dongkyu Kim'
  4. person.name_change   # => ['Dongkyu Kim', 'Kim Dongkyu']

name이 변경되었기 때문에 name_changed?는 true를 리턴한다. <attr>_was 메소드는 변경 이전의 값을 알려준다. 또한 <attr>_change 메소드를 통해서 변경 이전 값과 이후의 값을 Array로 받을 수 있다.


  1. person.changed?  # => true
  2. person.changed   # => ['name']
  3. person.changes   # => { 'name' => ['Donkyu Kim', 'Kim Dongkyu'] }

obj.changed? 메소드는 하나의 속성이라도 변경되었을 경우, true를 리턴한다. obj.changed 메소드는 변경된 속성명을 Array로 알려준다. 또한 obj.changes 메소드는 변경된 속성명과 함께 해당 속성의 변경 이력을 Hash 값으로 전달해준다.


  1. person.save
  2. person.changed?      # => false
  3. person.name_changed? # => false

위에서 언급하였듯이 일단 저장하게 되면, 저장 이전의 변경 정보는 알 수 없게 된다.


몇 가지 주의사항들

위에서 설명한 메소드들은 전혀 어려울 것 없이 코드에서 사용할 수 있다. 이 단락에서는 ActiveRecord::Dirty의 메소드를 사용하는데 있어서 주의해야할 몇 가지를 언급하면서 문서를 마치고자 한다.


<attr>= writer로 속성을 변경하지 않는 경우

ActiveRecord::Dirty의 메소드들은 기본적으로 <attr>= writer로 값을 변경하는 경우에, 변경 정보를 모니터링할 수 있다. 따라서 다른 메소드로 속성 값을 변경하는 경우에는, <attr>_will_change! 메소드로 해당 속성이 변경될 것임을 미리 알려주어야만 속성의 변경 여부를 파악할 수 있다.


  1. person = Person.find_by_name('Dongkyu Kim')
  2. person.name_will_change!  # => name 속성이 다른 메소드로 변경될 것임을 알림.
  3. person.name.upcase!       # => <attr>= writer로 변경하지 않음.
  4. person.name_changed?      # => true

같은 값으로 변경하는 경우에는?

만약 속성의 값을 같은 값으로 설정하는 경우, 또는 A에서 B로 변경했다가 다시 A로 변경하는 경우의 changed?의 결과는 어떻게 될까?


  1. person = Person.find_by_name('Dongkyu Kim')
  2. person.name                     # => 'Dongkyu Kim'
  3. person.name_changed?            # => false
  4. person.name = 'Dongkyu Kim'     # 같은 값으로 설정한다.
  5. person.name_changed?            # => false
  6. person.name = 'Kim Dongkyu'
  7. person.name_changed?            # => true
  8. person.name_change              # => ['Dongkyu Kim', 'Kim Dongkyu']
  9. person.name = 'Dongkyu Kim'
  10. person.name_changed?            # => true
  11. person.name_change              # => ['Dongkyu Kim', 'Dongkyu Kim']
  12. person.changed?                 # => true

위의 예제에서 보는 것과 같이 같은 값으로 변경하는 경우에는 변경된 것으로 인식하지 않는다. 하지만 한번이라도 다른 값으로 변경된 속성은 changed?를 true로 리턴하게 된다.


참조

이 글은 스프링노트에서 작성되었습니다.

2008/04/16 03:47 2008/04/16 03:47

제5회 루비 세미나

[Development]
그 전날 밤을 샜음에도 불구하고, 올해의 마지막 루비 세미나에 참석하였다. 1, 2회 세미나에는 참석했지만, 3, 4회에는 시간이 맞질 않아서 아쉽게 참석을 못 했었다. 그래서 (집으로 돌아와서 넉다운될 정도로 피곤했음에도 불구하고) 무리를 해서라도 참석했다. 결론부터 이야기하자면 너무도 좋은 시간이었다. 매일 루비 & 레일스 프로그래밍을 하고 있지만 무언가 조금씩 정체되어가는듯한 느낌이 들었었는데, 이번 세미나는 내가 다시 뛸 수 있게 불을 붙여준 촉매제가 되었다.

자세한 세션별 후기는 다른 분들이 작성하셨으므로, 여기서는

생략하겠다.


이번 세미나에서 느낀 점을 몇가지 정리해보자. 우선 루비 커뮤니티의 확장이다. 올해 초 2회 세미나에서 웰리에 대한 소개를 할 때만 해도, 국내에는 레일스에 대한 레퍼런스가 없던 상황이었다. 하지만 이제는 여러 훌륭한 레퍼런스들이 많이 소개되고, 현재도 개발 중에 있다. (나중에 시간을 내서, 레일스 사이트들에 대한 레퍼런스를 정리해봐야겠다. ^^) 또한 참여하는 인원도 많아지고, 다양한 분야의 분들이 모이시는 것 같다.

이에 따라서 세미나를 세분화하는 것도 좋은 방법일 것 같다. 앞으로의 세미나 주제들은 (지금까지의 추세를 봤을 때) 좀 더 다양해지고, 내용의 깊이도 깊어질 것이다. 이에 따라 주제별로 세미나를 진행하게 되면, 좀 더 다양한 형태의 세미나를 진행할 수 있을 것이고, 처음 루비를 접하시는 분들에게도 적합한 세미나를 진행할 수 있을 것이다.
우선은 루비와 레일스 세미나를 양분하는 것도 하나의 방법일 것이다.

내년에는 올해보다 좀 더 다양하고 재미있는 주제의 세미나를 기대해본다. 또한 루비 기반의 서비스 중에서 소위 말하는 대박나는 사이트들도 많이 나왔으면 좋겠다. 어제 세미나에서 발표하신 분들과 참석하신 분들 모두 수고 하셨습니다. ^^
2007/12/03 01:23 2007/12/03 01:23

Ruby기반의 경량 웹프레임워크, Wuby

[Development]


Ruby InsideWuby라는 Ruby기반의 경량 웹프레임워크를 소개하는 글이 올라왔다. 글을 읽고난 뒤, 바로 다운 받아서 소스코드를 읽어보았다. Wuby는 Ruby 기반의 다른 프레임워크 (Camping, Sinatra 등)처럼 경량의 웹프레임워크로서 다음과 같은 특징을 갖고 있다.


특징

  • 주석포함 570 정도되는 경량의 프레임워크.
  • Wuby 파일 (wuby.rb) 자체가 프레임워크인 동시에 httpd 역할을 한다. 따라서 third party Application이나 gem 설치할 필요가 없다.
  • Camping처럼 MVC 구조가 아니라, PHP, ASP 같은 페이지 단위로 구현된다. 내부에서 사용되는 메소드로 PHP스럽다. ^^;;
  • 데이터베이스로는 SDBM 사용하며, wrequest라는 함수로 CRUD 처리한다.

코드는 변수 선언부와 4개의 클래스로 구성되어져 있다.

  • 프레임워크 구동과 관련된 변수 선언부
  • class MimeMap : 확장자별 mimetype 선언한 class
  • class Log : Rotation되는 Log 파일을 처리하는 class
  • class Request : socket으로부터 들어오는 request parsing한다.
  • class Wuby : TCP server 기능을 하는 class. html 파일에 대한 template 처리도 한다.

아래의 코드는 Wuby의 실제 예제 코드이다.
<%= wrender('header.inc') %>

<% @id = @params["id"] %>


<h2><%= wrequest("r", "blog", "title", @id) %></h2>

<h3><%= wrequest("r", "blog", "time", @id) %></h3>

<p><%= wrequest("r", "blog", "body", @id) %> </p>

<p>

      <a href="edit.rhtml?id=<%=@id%>">edit post</a> |

      <a href="delete.rhtml?id=<%=@id%>">delete post</a>

</p>


<a href="index.rhtml">home</a>


<%= wrender('footer.inc') %>


위의 특징에서 언급했듯이, Wuby는 PHP나 ASP처럼 페이지단위로 Request가 처리된다. 위의 예제 코드는 blog.dbm 이라는 sdbm 파일에서 parameter로 넘어온 id에 해당되는 title, time, body 값을 가져와서 보여주는 간단한 블로그의 view 페이지이다. 예제코드만 보면 ruby와 관련된 소스코드인지 알 수 없을 정도로 ruby 스럽지 않다.

2007/11/20 03:36 2007/11/20 03:36

Capistrano 설치시 Zlib:BufError

[Development]
저번 주에 개발 PC의 하드디스크가 살짝 맛이 가버리는 바람에 생쑈를 한번 했다.
결국 하드디스크 교체하고 개발환경을 새로 구축해야만 했다. ㅠ ㅠ

Rails 개발환경을 구축하기 위해서 Gem을 설치하던 중, Capistrano 설치에서 문제가 발생하였다.
Capistrano는 Rails Application을 손쉽게 배포하고 관리하기 위해서 없어서는 안될 gem이다.

[code type=bash]$ gem install capistrano[/code]

위의 명령어로 설치하게 되면 net-ssh를 설치하는 중, Zlib:BufError가 발생하게 된다.
(윈도우 개발환경에서만 발생하는 것 같다.) 이를 해결하기 위해서는 RubyGems 를 update 시켜준다.

[code type=bash]$ gem update --system[/code]

설치 중 오류가 발생한다면, 다시 한번 위의 명령어를 실행시켜서 설치한다.
이렇게 update 후에 Capistrano를 설치하면 깔끔하게 설치될 것이다.
2007/05/31 17:11 2007/05/31 17:11

Ruby에서 String#<< 사용시 주의할 점.

[Development]
[code type=ruby][/code]개발 중인 Project에서 ActiveRecord::Base#find 문이 조금 복잡하게 구성이 되야 한다.
그래서 find 문에서 hash 값으로 넘어가는 options 부분을 분리하여, 처리한뒤 find문에 전달한다.
예를 들면
[code type=ruby]
find_options = {}
find_options[:select] = Post::COLUMNS    # Constant
find_options[:conditions] = ...

if query
  find_options[:conditions][0] << [' AND title LIKE ?', '%'+query+'%']
  find_options[:conditions] << query
end
[/code]

위와 같이 코드를 구성하면, 복잡한 조건에 대해서도 깔끔하게 코드를 정리할 수 있다.
그런데 어떤 경우에 hash의 값으로 엉뚱한 값이 들어가 있는 버그가 발견되었다.
디버깅 결과, Constant를 할당하는 부분에서 발생되는 것이었다. 결론부터 말하면
hash의 값으로 Constant를 전달하고, String#<< (Append method)로 값을 추가하면
Constant의 값이 바뀐다. 이는 변수에 전달한 경우도 마찬가지다.

[code type=ruby]
> CONST = '1, 2, 3'
> hsh = {}
> hsh[:test] = CONST
> hsh[:test] << ', 4, 5'  # => 'warning도 없이 깔끔하게(^^;) 추가됨.
> puts CONST
'1, 2, 3, 4, 5'
> hsh[:test] += ', 6'
warning: already initialized constant CONST
[/code]

따라서 위와 같은 경우를 방지하기 위해서, Constant를 할당한 경우는 += 로 String을 추가해야 한다.
2007/02/27 19:09 2007/02/27 19:09
TAG. ,

Rails Application의 컨트롤러를 모듈화시킬 때의 주의사항

[Development]
Rails Appliation이 커지다보면 컨트롤러를 모듈화해서 분리시켜야할 필요가 있다. 예를 들어 100개의 컨트롤러가 있다고 할 때, 이 파일들이 $RAILS_APP/app/controller 디렉토리에 들어있다고 하면 관리의 측면에서 매우 비효율적이 되기 때문이다. 이 글에서는 컨트롤러를 모듈화시

1. 컨트롤러의 생성.
Rails의 script를 통해서 컨트롤러를 생성할 경우, 컨트롤러명 앞에 모듈 이름을 붙여주면 된다.
예를 들어 Bar::Foo::TestController 라는 컨트롤러를 만들려면,
[code]
ruby script/generate controller bar/foo/test
[/code]
라고 하면 된다. 이렇게 생성된 컨트롤러 파일은 $RAILS_APP/app/controller/bar/foo/test_controller.rb 가 되고, 소스는 다음과 같다.
[code type=ruby]
class Bar::Foo::TestController < ApplicationController
end
[/code]

2. 컨트롤러의 활용.
위와 같이 모듈화 시킨 컨트롤러는 단순히 관리에만 편리한 것은 아니다. 모듈화시킬 때, 얻을 수 있는 이점 중의 하나는 컨트롤러를 기능별로 묶을 수 있다는 점이다. application.rb 파일에 선언한 method나 상수는 전체 컨트롤러에서 사용될 수 있다. 이것은 컨트롤러를 생성할 때, application.rb에서 선언된 ApplicationController 를 상속해서 선언하기 때문이다. 이와 마찬가지로 기능별로 base 컨트롤러를 만든 후, 그 컨트롤러를 상속받아서 하위 컨트롤러를 만들 경우 중복되는 소스 코드없이 깔끔한 컨트롤러를 만들 수 있다.

예를 들어 관리자 페이지를 만든다고 가정하자. 관리자 페이지에는 여러가지 기능들이 있겠지만, 우선적으로 관리자 페이지에 대한 접근 권한을 체크해야할 것이다. 만약 컨트롤러를 모두 분리해서 사용한다면, ApplicationController에 validation 코드를 작성하고, 그것을 각 admin 관련 코드에서 before_filter로 불러줘야 한다. 이를 소스 코드로 풀어보면,
[code type=ruby]
# app/controller/application.rb
class ApplicationController < ActionController::Base
  def validates_admin
    # admin validatation code
  end
end

# app/controller/admin_user_controller.rb
class AdminUserControlller < ApplicationController
  before_filter :validates_admin
end

# app/controller/admin_log_controller.rb
class AdminLogController < ApplicationController
  before_filter :validates_admin
end
[/code]

하지만, 만약 admin이라는 모듈로 admin 관련 기능을 묶는 다면 (물론 하위로 더 세분화시킬 수도 있다.)
[code type=ruby]
# app/controller/admin/base_controller.rb
class Admin::BaseController < ApplicationController
  before_filter :validates_admin

private
  def validates_admin
   # admin validatation code
  end
end

# app/controller/admin/user_controller.rb
module Admin
  class UserController < BaseController
    # Nothing to do
  end
end
[/code]

위와 같이 상속받는 클래스들에서는 코드를 추가할 필요가 없게된다. 여기서는 관리자 인증이라는 간단한 예를 들었지만, 기능이 복잡해지고 추가될 수록 이렇게 관리한 컨트롤러는 나중에 빛을 발할 것이다. ^^;

3. 주의할 점
위와 같이 모듈화 시켜서 사용할 경우에 주의할 점은 모델명과의 충돌이다. 예를 들어서 우리가 Admin이라는 모델 클래스를 갖고 있다면 어떻게 될까? 관리자 코드의 안에서는 module Admin이 Admin 모델 클래스를 감추게 되기 때문에, 접근할 수가 없게 된다. 즉, 다음과 같은 코드는 find method가 없다는 에러를 발생시킨다.

[code type=ruby]
# app/controller/admin/user_controller.rb
module Admin
  class UserController < BaseController
   admins = Admin.find(:all)  # => module Admin에는 find method가 없으므로 에러를 발생시킴.
  end
end
[/code]

따라서 모듈화시켜서 사용할 경우에는 모델명과의 충돌을 피하는 것이 좋다. 이 점만 유의한다면, Rails Application이 커지더라도, 유연하게 컨트롤러를 관리할 수 있을 것이다.





2007/02/08 18:40 2007/02/08 18:40

Ruby에서 iconv 사용시 주의사항

[Development]
한국어 (EUC-KR)로 되어있는 Content를 UTF-8로 변환할 경우, iconv를 이용해서 쉽게 변환할 수 있다.

[code type=ruby]
conv = Iconv.new('UTF-8', 'EUC-KR')
converted = conv.iconv(content)  # => UTF-8로 변환된 문자열
[/code]

문제가 되는 경우는 Content 내에 확장 완성형 문자 (ex. 아햏햏)가 있는 경우에 발생한다.
변환을 시도할 경우, Iconv::IllegalSequence 에러가 발생하게 된다.
이럴 경우, Iconv 객체를 생성할 때, //IGNORE 옵션을 붙여서 처리하지 못하는 문자열을 무시하도록
설정할 수 있다.

[code type=ruby]
conv = Iconv.new('UTF-8//IGNORE', 'EUC-KR')
[/code]


2007/01/19 12:22 2007/01/19 12:22
TAG. , ,

Ruby에서 한글 자소 분리 (초성, 중성, 종성)

[Development]
Ruby에서 UTF-8 형식의 한글을 자소를 분리할 일이 생겨서,
기존에 Java에서 사용하던 함수를 ruby로 converting하였다.

Ruby에서는 Built-in Library까지 확장할 수 있으므로, (이런 확장성이 내가 Ruby를 좋아하는 이유다. ^^)
String class를 확장하여서 구현하면 된다. 단 주의할 점은 UTF-8 기준에서 처리되므로
초, 중, 종성 값이 UTF-8 값이어야 하고 (즉, 코드를 저장하는 파일이 UTF-8 형식이어함)
처리할 string도 UTF-8로 인코딩 되어있어야 한다.

이 조건이 되지 않는 경우는, Iconv를 이용해서 Wrapper method를 구현하면 될 것이다.
  1. class String
  2.   # 한글 초성, 중성, 종성. (UTF-8 이어야 함)
  3.   @@chosung = ['ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ']
  4.   @@jungsung = ['ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ']
  5.   @@jongsung = ['', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ']
  6.   # UTF-8 형식의 문자열을 분해한다. 
  7.   def separate
  8.     separated = []
  9.    
  10.     self.unpack('U*').each do c
  11.       n = (c & 0xFFFF).to_i
  12.      
  13.       # 유니코드 2.0 한글의 범위 : AC00(가) ~ D7A3(힣)
  14.       if n >= 0xAC00 && n <= 0xD7A3
  15.         n = n - 0xAC00
  16.         n1 = n / (21 * 28)  # 초성 : '가' ~ '깋' -> 'ㄱ'
  17.         n = n % (21 * 28)  # '가' ~ '깋'에서의 순서
  18.         n2 = n / 28;    # 중성
  19.         n3 = n % 28;    # 종성
  20.        
  21.         separated << @@chosung[n1] << @@jungsung[n2] << @@jongsung[n3]
  22.       else
  23.         separated << c.to_a.pack('U')
  24.       end
  25.     end 
  26.    
  27.     separated
  28.   end
  29. end
 
2006/11/23 19:43 2006/11/23 19:43

Ruby에서 Ruby-Feedparser의 사용시 인코딩 문제

[Development]

Ruby에서 Ruby-Feedparser를 사용해서 RSS1.0, RSS2.0, ATOM 형식의 feed들을 손쉽게
parsing할 수 있다. 그런데 Ruby-FeedParser의 내부에서는 모든 string을 UTF-8로 변환해서
리턴하게 된다.

textconverters.rb를 보면

  1. # Convert a text in inputenc to a text in UTF8
  2. # must take care of wrong input locales
  3. def toUTF8(inputenc)
  4.    if inputenc.downcase != 'utf-8'
  5.      # it is said it is not UTF-8. Ensure it is REALLY not UTF-8
  6.      begin
  7.        if self.unpack('U*').pack('U*') == self
  8.          return self
  9.        end
  10.      rescue
  11.        # do nothing
  12.      end
  13.      begin
  14.        return self.unpack('C*').pack('U*')
  15.      rescue
  16.        return self #failsafe solution. but a dirty one :-)
  17.      end
  18.    else
  19.      return self
  20.    end
  21.   end

이 부분에서 보면 iconv를 사용하지 않고 unpack, pack으로 인코딩을 처리하는데, 이게 한국어나
일본어에서는 깨져서 볼 수가 없게 된다. (iconv가 error-proof하지 않기 때문에 저렇게 처리했다고 한다.)
따라서 Ruby-Feedparser를 사용하는 경우에는 toUTF8 함수를 override해서 다음과 같이 변경해야,
인코딩을 제대로 처리할 수 있다.

  1. require 'feedparser'
  2. require 'iconv'
  3. class String
  4.   def toUTF8(inputenc)
  5.   if inputenc.downcase != 'utf-8'
  6.      begin
  7.        iconv = Iconv.new('UTF-8', inputenc)  # iconv를 사용해서 UTF-8로 encoding 한다.
  8.        return iconv.iconv(self)
  9.      rescue
  10.        return self #failsafe solution. but a dirty one :-)
  11.      end
  12.   else
  13.      return self
  14.   end
  15.   end
  16. end
 
2006/11/23 10:01 2006/11/23 10:01