메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

Ruport: 루비를 위한 업무 레포팅(1)

한빛미디어

|

2008-05-21

|

by HANBIT

10,041

제공 : 한빛 네트워크
저자 : Gregory Brown and Michael Milner
역자 : 이대엽
원문 : Ruport: Business Reporting for Ruby

많은 이들이 루비(또는 레일즈와 함께)로 개발해 보고 싶어하는데, 그 이유는 바로 루비와 레일즈가 재미있기 때문이다. 여러 작업을 하는 데 있어 이 말은 사실이다. 그런데 여러분이 업무 레포팅에 관해 고민하고 있다면 어떤 생각이 드는가? 만약 여러분이 우리와 비슷하다면 아마도 약간의 두려움과 괴로움을 느낄 수도 있을 것이다. 대개 레포팅 작업에는 가능한 빠르게 여러 곳에서 데이터를 가져와 다양한 형식으로 가공하는 일이 따른다. 그렇지만 여러분이 열심히 일해서 만들어 낸 결과물에 대한 반응은 대개 “괜찮네요. 이 정도면 적당하긴 한데, 혹시 이 글자를 진하게 하고, 파란색으로 만들어 주실 수 있으세요? 그리고... 아, 저희는 레포트를 엑셀로 다운로드 할 수도 있어야 해요.”와 같은 식으로 나타나게 마련이다.

굳이 우리가 Ruby Reports (Ruport)로 여러분의 레포팅 작업이 즐거워질 것이라 주장하지는 않지만, Ruport로 훨씬 더 레포팅 작업이 수월하게 될 것이라 생각한다. Ruport가 여러분이 원하는 보고서를 만들어 주는 것은 아니며, 단지 Ruport는 여러분이 레포팅 작업을 시작하기에 견고한 토대를 제공해줄 뿐이다. 레포팅 애플리케이션에 대한 기반으로 Ruport를 이용하게 되면 여러분은 코드를 깔끔하고 잘 정돈된 상태로 유지할 수 있으며, 다음 번에 누군가가 브라우저에 포함된, 정교하게 제작된 보고서에 대해 인쇄 가능 버전을 요청하는 것 때문에 몹시 화내지 않아도 될 것이다.

이 기사는 여러분이 Ruport에서 해줄 수 있는 것들을 경험하게 해줄 목적으로 작성된 것이다. 우리는 서적 목록을 관리하는 Bibliophile[역주: "애서가"라는 의미이다]이라는 간단한 애플리케이션으로 시작해볼 것이다. 이 기사의 예제를 이해하고 Bibliophile의 소스 코드를 직접 돌려 보면서 여러분은 Ruport로 작업하는 것이 어떤 것인지 체감할 수 있을 것이다.

보고서 생성을 시작하기 전에 먼저 우리는 Ruport를 어떻게 구해 레일즈에서 실행해 볼 수 있는지에 관해 다룰 것이다. 이렇게 하기 위해서는 간단한 플러그인을 설치하는 것 이상의 작업이 약간 필요하긴 하지만, 상당히 쉬운 과정이기도 하다.

설치 및 설정

여러분이 가장 먼저 해야 할 일은 Ruport를 설치하는 것이다. 그리고 Ruport를 가장 쉽게 설치하는 방법은 바로 젬을 설치하는 것이다.
gem install ruport
게다가 여러분은 Ruport의 acts_as_reportable 모듈도 설치하고 싶을 텐데, acts_as_reportable 모듈을 이용하면 데이터를 수집하기 위해 액티브레코드(ActiveRecord)에 연결할 수 있기 때문이다.
gem install acts_as_reportable
이렇게 하면 Ruport를 이용하는데 필요한 것이 모두 설치된다. Ruport의 핵심적인 부분 이외의 추가적인 기능을 제공하는 다른 패키지도 있지만, Ruport를 시작하는 데에는 필요하지 않을 것이다.

필요한 것이 모두 설치되면 레일즈 프로젝트에서 Ruport를 사용하기 시작할 수 있다. 우리는 책 모음에 관한 정보를 저장할 수 있게 해줄 Bibliophile라는 이름의 레일즈 프로젝트를 만들 것이다. 표준 rails bibliophile 명령어를 이용하여 애플리케이션의 구조를 만든 후, 여러분은 environment.rb 파일에 Ruport를 불러들여 사용 가능한 상태로 만들 필요가 있을 것이다. 그렇게 하는 가장 믿을만하고, Ruport에서 적절한 시간에 로딩할 것을 보장하는 방법은 Ruport를 필요로 하는 코드를 config.after_initialize 블럭에 추가하는 것이다. 설정 파일에서 이것과 관련된 부분이 아래에 나타나 있다.
  Rails::Initializer.run do |config|
    config.after_initialize do 
      require "ruport" 
    end
  end
이제 Ruport가 로딩되면 레일즈 프로젝트에서 Ruport를 사용할 수 있을 것이다. 한 가지 알아둘 것은 우리가 acts_as_reportable 모듈을 사용하고자 하더라도 Ruport에서 자동적으로 acts_as_reportable 역시 로딩하려 할 것이므로 직접 모듈을 require할 필요가 없다는 점이다. 우리는 보고서 생성을 시작할 때 보고서의 코드를 담을 파일의 위치를 결정해야 할 것이다. 물론 레일즈가 레일즈에서 사용하는 모든 디렉터리 구조와 파일 위치에 대해 관례(convention)을 정하긴 하지만, Ruport에서 필요로 하는 파일에 대해서는 정하지 않는다. 결국 개인의 선호 문제이긴 하지만 Ruport 커뮤니티에서는 대개 app/reports 디렉터리를 사용하여 모든 보고서 코드를 담는다. 그러므로 여러분이 그러한 방식을 따를 것이라면 나중에 해당 디렉터리를 사용할 것이므로 지금 그 디렉터리를 생성해 두도록 한다.

마지막 설정 단계는 새로 만들어진 app/reports 디렉터리를 레일즈 프로젝트에서 보이게 하는 것이다. 그렇게 하려면 environment.rb 파일의 설정 영역에 지시자(directive)를 추가하여 로딩 경로에 대한 새 디렉터리를 추가한다.
  config.load_paths += %W( #{RAILS_ROOT}/app/reports )
Ruport를 설치하고 레일즈 프로젝트와 Ruport를 연동하는 것은 이게 끝이다. 다음으로는 Ruport에서 보고서에서 보여질 데이터를 어떻게 가져오는지 알아볼 것이다.

acts_as_reportable을 이용한 데이터 수집

완전한 보고서를 생성하기 전에 먼저 Ruport를 이용한 데이터 수집에 관해 알아보도록 하자. 보고서에 아무 텍스트나 추가하는 데에는 어떠한 데이터 수집 메커니즘을 필요로 하지 않고도 가능하지만, 대부분의 경우 여러분은 완성된 보고서의 일부로서 표 형식의 데이터를 보여주고 싶을 것이다. 이러한 필요에 따라 Ruport는 표 형식의 데이터를 쉽고 효율적으로 처리할 수 있게 해주는 일련의 자료 구조를 제공하고 있다.

이 중 가장 핵심적인 자료 구조는 Ruport::Data::Table이다. 가장 간단한 경우, 여러분은 테이블(Table)을 보고서에서 보여주고자 하는 데이터로 채울 필요가 있을 것이다. acts_as_reportable 모듈의 목적은 액티브레코드 모델(ActiveRecord Model)에 의해 표현되는 데이터로부터 Ruport Table을 채우는 가장 손쉬운 방법을 제공하는 것이다.

지금은 레일즈 콘솔을 사용하여 acts_as_reportable이 데이터 모음을 어떻게 사용하는지를 보여줄 것이므로 Ruport에서 제공되는 특별한 내장 메커니즘을 사용하여 텍스트 형식으로 데이터를 출력할 것이다. 차후에 Ruport의 형식화 시스템(formatting system)을 사용하여 출력결과를 직접 정의하는 방법을 보여줄 것이다.

가장 먼저 모델이 필요할 텐데, 애플리케이션의 명칭이 Bibliophile이므로 분명 책과 저자에 대한 모델이 필요할 것이다. 다음은 책과 저자 테이블을 생성하는 마이그레이션(migration)이다.
  class CreateBooks < ActiveRecord::Migration
    def self.up
      create_table :books do |t|
        t.string :name
        t.string :description
        t.string :isbn
        t.string :status
        t.integer :author_id
        t.integer :pages
        t.integer :genre_id
      end
    end

    def self.down
      drop_table :books
    end
  end

  class CreateAuthors < ActiveRecord::Migration
    def self.up
      create_table :authors do |t|
        t.string :name
        t.timestamps
      end
    end

    def self.down
      drop_table :authors
    end
  end
모델과 Ruport를 연결하기 위해서는 각 모델 정의에 acts_as_reportable 라인을 추가하기만 하면 된다.
  class Book < ActiveRecord::Base
    acts_as_reportable
    belongs_to :author  
  end

  class Author < ActiveRecord::Base
    acts_as_reportable
    has_many :books
  end
이렇게 하면 모델로부터 Ruport Table을 직접 생성하는데 사용할 수 있는 report_table이라는 이름의 클래스 메서드가 각 모델에 제공될 것이다. 레일즈 콘솔에서 데이터를 조금 만들어 보면 앞서 설명한 내용이 어떻게 동작하는지 확인해 볼 수 있을 것이다.
  >> Author.create(:name => "Umberto Eco")
  >> Author.create(:name => "William Gaddis")
  >> Author.create(:name => "Thomas Hardy")
  >> Author.create(:name => "Ben Okri")
이제 몇 명의 저자가 만들어졌으므로 Ruport 테이블을 만드는 것이 얼마나 쉬운지 알 수 있을 것이다. 여러분이 단순히 report_table 메서드를 호출하기만 해도 테이블이 생성될 것이다. 결과로 나타나는 테이블의 구조를 확인해 보기 위해서는 Ruport에서 제공되는 기본 텍스트 출력결과를 사용할 것이다.
>> puts Author.report_table
+----------------------------------------------------------------------------->>
|      name      |           updated_at           | id |           created_at >>

+----------------------------------------------------------------------------->>
| Umberto Eco    | Mon Mar 31 23:05:39 -0400 2008 |  1 | Mon Mar 31 23:05:39 ->>
| William Gaddis | Mon Mar 31 23:05:58 -0400 2008 |  2 | Mon Mar 31 23:05:58 ->>
| Thomas Hardy   | Mon Mar 31 23:06:07 -0400 2008 |  3 | Mon Mar 31 23:06:07 ->>
| Ben Okri       | Mon Mar 31 23:06:22 -0400 2008 |  4 | Mon Mar 31 23:06:22 ->>
+----------------------------------------------------------------------------->>
이렇게 하는 것은 매우 쉽지만 실제 프로젝트에서는 모든 컬럼을 보여주고 싶지는 않을 것이다. 사실 여러분은 저자 테이블에서 저자의 이름에만 관심이 있을 수도 있다. 여러분은 report_table에 다양한 옵션을 주어 출력결과를 직접 정의할 수가 있다. 여러분은 이것을 acts_as_reportable에서 사용되는 몇 가지 추가 옵션이 포함된 AactiveRecord.find 메서드로 생각하면 될 것이다. 옵션을 포함할 경우, 여러분은 첫 번째 매개변수로 :all이나 :first(매개변수를 지정하지 않으면 :all이 기본값이다)을 지정해야 한다.

여러분은 특정 옵션을 지정하여 결과 테이블에 포함할 컬럼을 지정할 수도 있다. :only 옵션은 지정된 컬럼만이 포함되어야 함을 나타내며, :except 옵션은 열거된 컬럼을 제외한 컬럼이 포함되어야 함을 나타낸다. 두 경우 모두 여러분은 컬럼명이나 컬럼명의 배열을 줄 수 있다.
>> puts Author.report_table(:all, :only => "name")
+----------------+
|      name      |
+----------------+
| Umberto Eco    |
| William Gaddis |
| Thomas Hardy   |
| Ben Okri       |
+----------------+
또 한 가지 알아둘 것은 만약 여러분이 :only 옵션에 컬럼명의 배열을 주게 되면 컬럼은 배열내의 순서에 따라 정렬될 것이라는 점이다.
>> puts Author.report_table(:all, :only => ["id","name"])
+---------------------+
| id |      name      |
+---------------------+
|  1 | Umberto Eco    |
|  2 | William Gaddis |
|  3 | Thomas Hardy   |
|  4 | Ben Okri       |
+---------------------+
여러분은 일반 ActiveRecord.find에서 사용할 수 있는 것도 모두 사용할 수가 있다.
>> puts Author.report_table(:all, :only => ["id","name"], :order => "authors.name")
+---------------------+
| id |      name      |
+---------------------+
|  4 | Ben Okri       |
|  3 | Thomas Hardy   |
|  1 | Umberto Eco    |
|  2 | William Gaddis |
+---------------------+
만약 여러분이 여러 개의 연관된 모델로부터 출력결과를 결합하고자 한다면 :include 옵션을 사용할 수가 있다. 또한 여러분은 해시(hash)를 이용하여 포함된 모델에 옵션을 중첩할 수도 있는데, 이를 통해 테이블내의 모든 데이터를 직접 정의할 수가 있다. 먼저 우리는 책을 만들 필요가 있을 것이므로 다음과 같이 코드를 작성하여 관련 모델을 어떻게 한 테이블에 결합하는지 볼 수 있을 것이다.
>> Book.create(:name => "Baudolino", :author_id => 1, :pages => 521)
>> Book.create(:name => "The Famished Road", :author_id => 4, :pages => 500)

>> Book.create(:name => "The Recognitions", :author_id => 2, :pages => 956)
>> Book.create(:name => "The Return of the Native", :author_id => 3, :pages => 418)
가장 간단한 :include 옵션의 사용법은 단순히 포함될 모델의 이름을 지정하는 것이다.
>> puts Book.report_table(:all, :only => "name", :include => :author)
+----------------------------------------------------------------------------->>
|           name           | author.id |       author.created_at        |     >>
+----------------------------------------------------------------------------->>

| Baudolino                |         1 | Mon Mar 31 23:05:39 -0400 2008 | Mon >>
| The Famished Road        |         4 | Mon Mar 31 23:06:22 -0400 2008 | Mon >>
| The Recognitions         |         2 | Mon Mar 31 23:05:58 -0400 2008 | Mon >>
| The Return of the Native |         3 | Mon Mar 31 23:06:07 -0400 2008 | Mon >>
+----------------------------------------------------------------------------->>
출력결과를 전부 직접 정의하기 위해서는 포함된 모델에도 옵션을 주어야 할 수도 있다. 한 가지 알아둘 점은 포함된 모델에서 반환되는 컬럼명에는 전체 경로가 지정된(qualified) 모델명이 포함된다는 것이다.
>> puts Book.report_table(:all, :only => "name", :include => { :author => { :only => "name" } })
+-------------------------------------------+
|           name           |  author.name   |
+-------------------------------------------+
| Baudolino                | Umberto Eco    |
| The Famished Road        | Ben Okri       |
| The Recognitions         | William Gaddis |
| The Return of the Native | Thomas Hardy   |
+-------------------------------------------+
이제 여러분은 액티브레코드 모델(또는 여러 모델)에서 Ruport 테이블을 생성하는 방법에 대해 알게 되었을 것이다. 그 밖에 acts_as_reportable이 이해하는 고급 명령도 있는데, 이는 여러분 스스로 찾아볼 수 있을 것이다. 하지만 지금은 여러분이 여태까지 수집한 데이터를 이용하여 형식이 지정된 보고서를 어떻게 만드는지에 관한 내용으로 넘어갈 것이다.

보고서 형식 지정하기

Ruport는 보고서의 형식을 지정하는데 있어 매우 체계적인 접근법을 취하고 있다. 우리는 형식에 종속된 코드를 포함하는 데이터 조작 코드를 혼합하기 보다는 레일즈의 MVC 패턴과 상당히 유사하게 명확한 분리를 유지할 것이다.

레일즈처럼 Ruport에도 데이터와 그 데이터를 최종적으로 형식화할 코드를 중재하는 역할을 수행하는 컨트롤러가 있다. 컨트롤러의 수행방식을 확인하는 가장 좋은 방법은 예제를 통해 알아보는 것이므로 가장 마지막 부분에서 알아보았던 내용을 토대로 간단한 Book 리스트를 만들어 보도록 하자. 차후에 다른 형식을 추가할 것이며, 지금은 HTML로 시작해볼 수 있다. 다음은 Bibliophile 애플리케이션의 모든 책에 대한 제목, 저자, 페이지수를 보여주는 간단한 보고서를 구현하는 코드이다.
app/reports/book_report.rb
class BookReport < Ruport::Controller

  stage :list

  def setup
    self.data = Book.report_table(:all, :include => { :author => { :only => ["name"] } },
                                         :only => ["name", "author.name", "pages"],
                                         :order => "books.name")
    data.rename_columns("name" => "Title", "author.name" => "Author")
  end

  formatter :html do
    build :list do
      output << textile("h3. Book List")
      output << data.to_html
    end
  end

end
script/console을 이용하여 실제로 보고서를 실행해볼 수 있다:
>> puts BookReport.render_html

Book List

Title Author pages
Baudolino Umberto Eco 521
The Famished Road Ben Okri 500
The Recognitions William Gaddis 956
The Return of the Native Thomas Hardy 418
결과로 만들어지는 HTML은 평범하고 특별한 건 없어 보인다. 조금만 더 있으면 어떻게 레일즈 컨트롤러와 뷰에 연결하여 이 보고서를 표시할 수 있는지 보여주겠지만, 지금은 Ruport에서 이 HTML에 어떤 일을 했는지에 대해서만 좀 더 자세히 살펴보기로 하자.

Ruport 컨트롤러는 단계적으로 보고서를 처리하여 작업을 수행하는데, 이러한 단계는 여러분의 형식자(formatter)에 정의되어 있다. 이러한 매우 단순한 보고서에는 형식자가 빌드할 수 있는 단 하나의 단계인 list만이 존재한다. 아래는 이러한 list를 정의하는 코드이다:
  stage :list
이 사실을 염두에 두면 형식자 코드가 아마도 좀 더 명확하게 느껴질 것이다:
  formatter :html do
    build :list do
      output << textile("h3. Book List")
      output << data.to_html
    end
  end
BookReport.render_html이 로딩될 때 HTML 형식자가 build 블럭 안의 코드를 실행하는 것이 확실하긴 하지만, output과 textile 메서드가 어디에서 오는지 알아내는 것이 다소 어려울 수도 있을 것이다.

약간 단순화하자면 우리는 Ruport의 문법설탕(syntactic sugar)의 비밀을 풀어헤쳐 이것이 어떻게 평범한 기존 루비 객체와 연결되는지 살펴볼 수가 있다. 보고서는 다음과 같이 손쉽게 재작성될 수 있을 것이다:
class BookReport < Ruport::Controller

  stage :list

  def setup
    self.data = Book.report_table(:all, :include => { :author => { :only => ["name"] } },
                                         :only => ["name", "author.name", "pages"],
                                         :order => "books.name")
    data.rename_columns("name" => "Title", "author.name" => "Author")
  end

  class HTML < Ruport::Formatter::HTML

    renders :html, :for => BookReport

    def build_list
      output << textile("h3. Book List")
      output << data.to_html
    end
  end

end
이같이 작성할 경우 Ruport의 Formatter 객체가 필요한 세부사항만을 공유하면서 실질적으로 컨트롤러의 엔터티를 분리한다는 것이 분명해 진다. 구체적으로 말하면 Controller와 Formatter 사이에서만 data와 options 속성이 공유된다는 것이다.

이렇게 만들어 주는 코드가 아래에 나타나 있다:
  renders :html, :for => BookReport
이 라인은 BookReport 컨트롤러로 하여금 Ruport의 HTML 형식자의 하위 클래스에서 HTML 출력결과를 처리하게 한다. 이는 컨트롤러에게 BookReport.render_html가 요청될 때 이 객체가 바로 해당 단계를 수행할 것이라는 것을 알려준다.

그리고 이 코드에서 우리는 이전의 블럭에서 사용한 build 메서드가 평범한 루비 메서드를 만들어내는 문법 설탕일 뿐이라는 사실을 알게 된다.
  def build_list
    output << textile("h3. Book List")
    output << data.to_html
  end
이 체인의 가장 아래에서 우리는 일종의 마법이 있다는 것을 알게 되므로 이 보고서의 컨텍스트에서 Ruport의 형식화 시스템이 동작하는 방식을 머릿속으로 잘 정리할 수가 있다.

BookReport.render_html이 요청될 때 다음의 단계가 차례대로 수행된다:
  1. BookReport에서 :html로 등록된 Formatter를 찾는다.
  2. setup 메서드가 실행되어 data와 options가 설정된다.
  3. 각 단계가 정의된 순서대로 수행된다.
  4. 형식자의 출력결과가 반환된다.
형식화 시스템을 고급스럽게 사용하는데 있어 이 과정이 다소 복잡하긴 하지만 이러한 단계가 바로 Ruport에서 보고서를 렌더링할 때 일어나는 과정의 핵심적인 부분을 이룬다.

이제 시스템의 실제 동작 방식을 다루었으므로 보고서에서 CSV와 PDF 지원을 추가하는 법에 관해 간략하게 알아보기로 하자.
  formatter :pdf do
    build :list do
      pad(10) { add_text "Book List" }
      draw_table data
    end
  end

  formatter :csv do
    build :list do
      output << data.to_csv
    end
  end
여러분도 알고 있듯이 CSV 포맷은 일반화된 형식이므로 Ruport 테이블에는 그러한 CSV에 대한 형식화 지원이 내장되어 있으며, CSV의 경우에는 단지 단순 데이터 뭉치만이 필요할 뿐이다.

script/console을 살펴보면 여러분은 정확히 어떤 결과를 얻게 될지 알게 될 것이다.
>> puts BookReport.render_csv 
Title,Author,pages
Baudolino,Umberto Eco,521
The Famished Road,Ben Okri,500
The Recognitions,William Gaddis,956
The Return of the Native,Thomas Hardy,418
PDF 출력은 약간 더 재미있다. 첫 번째 라인에서는 위와 아래에 여백을 지정하면서 문서에 텍스트를 조금 추가한다.
  pad(10) { add_text "Book List" }
두 번째 라인을 보고 약간 놀라워했을지도 모르는데, 여러분이 output << data.to_pdf와 같은 것을 기대했을 수도 있기 때문이다. PDF는 HTML이나 텍스트, 또는 CSV와 같은 스트리밍 데이터 형식이 아니므로 PDF 캔버스에 테이블을 그리는 특수한 도우미가 필요하다.
  draw_table data
이것 말고는 코드는 특별한 게 없다. Ruport의 컨트롤러는 파일로 렌더링하는 것을 지원하므로 이렇게 하여 PDF를 받을 수가 있다.
>> BookReport.render_pdf(:file => "books.pdf")
출력결과는 다음과 같다:



출력결과는 꽤 일반적인 모습인데, 머리글 행을 가운데로 정렬하고 글꼴을 좀 키우면 더 나아 보일 것이다. 이를 위해 저수준 PDF 형식자 도우미를 이용할 수도 있겠지만, 대신 형식자 템플릿(formatter template)으로 알려진 Ruport의 고수준 시스템을 살펴볼 것이다.
TAG :
댓글 입력
자료실