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

한국 developerWorks  >  웹 개발  >

Real world Rails, Part 3: ActiveRecord 최적화 하기 (한글)

일반적인 성능 문제 해결하기

developerWorks
문서 옵션

JavaScript가 필요한 문서 옵션은 디스플레이되지 않습니다.

영어원문

영어원문


제안 및 의견
피드백

난이도 : 중급

Bruce Tate, CTO, WellGood LLC

2007 년 9 월 11 일

ActiveRecord는 환상적인 영속성 프레임웍(persistence framework)이지만, 이 프레임웍은 상세한 부분을 숨기기 때문에 성능 문제를 일으키기 쉽습니다. 가장 일반적으로 발생하는 문제와, 이를 해결하는 방법을 배워봅시다.

Ruby on Rails 프로그래밍이 여러분을 망칠지도 모른다. 이 프레임웍은 다른 프레임웍들에 만연해 있는 단조로움도 없다. 여러분은 코드 라인을 통해 여러분의 생각을 펼치게 될 것이다. 그리고 곧 ActiveRecord를 사용하게 될 것이다.

소셜 북마크

mar.gar.in mar.gar.in
digg Digg
del.icio.us del.icio.us
Slashdot Slashdot

오랫동안 자바™ 프로그래머로 활동해온 필자에게 ActiveRecord는 다소 낯설다. 자바 프레임웍에서, 필자는 독립 모델과 스키마들간 맵(map)을 구현했다. 이와 같은 프레임웍을 매핑 프레임웍(mapping frameworks)이라고 한다. ActiveRecord에서, 필자는 SQL 또는 Ruby 클래스로 데이터베이스 스키마만 정의한다. 객체 모델 디자인을 데이터베이스 구조에 의존하는 프레임웍을 래핑 프레임웍(wrapping frameworks)이라고 한다. 하지만, 대부분의 래핑 프레임웍과는 달리, Rails는 데이터베이스 테이블을 쿼리함으로써 객체 모델의 기능을 찾아낸다. 복잡한 쿼리를 구현하는 대신, 모델을 사용하여 SQL 대신 Ruby에서 관계를 트래버스 할 수 있다. 매핑 프레임웍의 기능을 활용하여 래핑 프레임웍을 단순화 한다. ActiveRecord는 사용하기 쉽고 확장하기도 쉽다. 가끔은, 너무도 쉽다.

여느 데이터베이스 프레임웍과 마찬가지로, ActiveRecord에서도 많은 문제가 생길 수 있다. 너무 많은 컬럼들을 파견하거나 인덱스나 무효 제약 조건 같은 중요한 구조적 데이터베이스 기능들을 중지시킬 수 있다. ActiveRecord가 나쁜 프레임웍이라는 것을 말하는 것이 아니다. 필자가 말하고 싶은 것은 확장을 원한다면 애플리케이션을 강화하는 방법을 알아야 한다는 것이다. 이 글에서, Rails 영속 프레임웍에서의 중요한 최적화 과정을 설명하도록 하겠다.

기본 관리하기

스키마에서 지원되는 모델을 생성하는 것은 script/generate model model_name 같은 코드를 생성하는 것만큼이나 쉽다. 여러분도 알다시피, 이 명령어는 모델, 마이그레이션, 단위 테스트, 심지어 기본 장치를 생성한다. 이 마이그레이션에 몇 가지 데이터 컬럼들을 채우고, 약간의 테스트 데이터를 입력하고, 테스트를 작성하고, 밸리데이션도 추가할 수 있다. 하지만 조심하라. 전체적인 데이터베이스 디자인을 생각해야 한다. 다음 사항을 염두 하라.

  • Rails에서도 기본적인 성능 문제가 생길 수 있다. 여러분의 데이터베이스가 잘 작동되기 위해서는 인덱스의 형태로 된 정보가 필요하다.
  • Rails에서는 무결성 문제도 생길 수 있다. 대부분의 Rails 개발자들은 데이터베이스에서 제약 조건들을 유지하고 싶어하지 않지만, 무효 가능 컬럼(nullable columns) 같은 문제를 다뤄야 한다.
  • Rails는 많은 엘리먼트들을 위한 기본 사항들을 갖고 있다. 가끔, 텍스트 필드의 길이 같은 기본 애트리뷰트는 실제 애플리케이션에는 너무 크다.
  • Rails에서는 효율적인 데이터베이스 디자인을 생성할 수 없다.

ActiveRecord를 연구하기 전에, 확고한 기반을 다져야 한다. 인덱스 구조가 잘 작동하는지를 확인해야 한다. 테이블이 너무 크거나, id 외의 컬럼을 검색하거나, 인덱스를 통해서 상세한 부분을 보기 위해 데이터베이스 매니저 문서를 본다면(다른 데이터베이스는 다른 방식으로 인덱스를 사용한다.), 인덱스를 반드시 생성해야 한다. 인덱스를 만들기 위해 SQL까지 갈 필요가 없다. 간단히 마이그레이션을 사용할 수 있다. create_table 마이그레이션을 사용하여 인덱스를 생성하거나, 인덱스를 생성하는 추가 마이그레이션을 생성할 수 있다. 다음은 ChangingThePresent.org에 사용하는 인덱스를 만드는 마이그레이션 예제이다. (참고자료):


Listing 1. 마이그레이션에서 인덱스 생성하기
                
class AddIndexesToUsers < ActiveRecord::Migration
  def self.up
    add_index :members, :login
    add_index :members, :email
    add_index :members, :first_name
    add_index :members, :last_name
  end

  def self.down
    remove_index :members, :login
    remove_index :members, :email
    remove_index :members, :first_name
    remove_index :members, :last_name
  end
end


ActiveRecord는 id에 대한 인덱스를 관리하기 때문에, 다양한 검색에서 사용하는 인덱스를 추가한다. 테이블은 크고, 가끔 업데이트 되며, 자주 검색되기 때문이다. 종종, 액션을 취하기 전에 쿼리에서 문제가 파악될 때까지 기다릴 것이다. 이 전략을 사용하면 데이터베이스 엔진을 예측하지 않아도 된다. 하지만, 사용자의 경우, 테이블은 곧 수백만 명의 사용자로 빠르게 늘어날 것이고, 자주 검색되는 컬럼에 대한 인덱스 없이는 효과가 없다는 것을 알고 있다.

마이그레이션과 관련한 문제가 두 가지 더 있다. 무효가 되어서는 안되는 스트링과 컬럼을 갖고 있다면, 마이그레이션을 올바르게 코딩 해야 한다. 대부분의 DBA(데이터베이스 관리자)들은 Rails가 무효 컬럼을 위한 그릇된 기본 사항을 갖고 있다고 생각하고 있다. 기본적으로, 컬럼들은 무효가 될 수 있다. 무효가 될 수 없는 컬럼을 만들려면, 매개변수, :null => false를 추가해야 한다. 스트링 컬럼을 갖고 있다면, 적절한 제한도 마련해야 한다. 기본적으로, Rails 마이그레이션은 string 컬럼을 varchar(255)로서 인코딩 할 것이다. 일반적으로, 이것은 너무 크다. 애플리케이션을 반영하는 데이터베이스 구조를 관리하기 위해 최선을 다해야 한다. 비 제한 로그인을 갖는 것 보다, 애플리케이션이 로그인을 10 문자로 제한한다면, 이에 맞게 데이터베이스를 코딩 해야 한다. (Listing 2)


Listing 2. limit과 무효가 될 수 없는 컬럼을 사용하여 마이그레이션 코딩하기
                
t.column :login, :string, :limit => 10, :null => false

여러분은 또한, 기본 값들과 안전하게 제공할 수 있는 다른 정보도 생각해야 한다. 이 작은 작업을 통해, 데이터 무결성 문제를 찾느라 많은 시간을 보내지 않아도 된다. 데이터베이스 기본을 고려하는 동안, 어떤 페이지가 정적이고, 캐싱하기 쉬운지를 생각해야 한다. 쿼리를 최적화 하고 페이지를 캐싱하는 것 중에 선택해야 한다면 페이지를 캐싱하는 것이 더 많은 리턴을 제공한다. 가끔, 페이지 또는 조각들이 상태 리스트처럼 정적이거나, 자주 묻는 질문이다. 이 경우, 캐싱은 효과적이다. 복잡성을 제한하고, 대신 데이터베이스 성능을 공격한다. 쿼리 성능을 공격하려면 계속 읽어나가기 바란다.

N+1 문제

기본적으로, ActiveRecord 관계는 매우 게으르다. (lazy) 다시 말해서, 프레임웍은 여러분이 실제로 액세스 할 때까지 관계로 액세스 하는 것을 기다린다. 주소를 갖고 있는 멤버의 예를 들어보자. 콘솔을 열고 명령어, member = Member.find 1을 입력할 수 있다. 로그에 다음과 같은 것이 붙는다. (Listing 3)


Listing 3. Log from Member.find(1)
                
^[[4;35;1mMember Columns (0.006198)^[[0m   ^[[0mSHOW FIELDS FROM members^[[0m
^[[4;36;1mMember Load (0.002835)^[[0m   ^[[0;1mSELECT * FROM members WHERE
 (members.`id` = 1) ^[[0m

Member는 주소와 관계를 갖고 있는데, 이는 has_one :address, :as => :addressable, :dependent => :destroy 매크로로 정의되었다. ActiveRecord가 Member를 로딩했을 때 이 로그에 주소 필드가 없었다. 콘솔에 member.address를 입력하면, development.log에서 Listing 4의 내용을 보게 될 것이다.


Listing 4. 관계에 액세스 하여 데이터베이스에 액세스 하기
                
  ^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;35;1mAddress Load (0.252084)^[[0m   ^[[0mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 1 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
  ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m

ActiveRecord는 member.address에 실제로 액세스 할 때까지 주소 관계에 대한 쿼리를 실행하지 않기 때문에, 이러한 레이지(lazy) 디자인이 잘 작동한다. 영속성 프레임웍은 멤버를 로딩하기 위해 많은 데이터를 이동할 필요가 없기 때문이다. 하지만, 여러분이 멤버의 리스트와 이들의 주소에 액세스 해야 한다고 가정해 보자. (Listing 5)


Listing 5. 주소를 가진 여러 멤버들 검색하기
                
Member.find([1,2,3]).each {|member| puts member.address.city}

각 주소에 대한 쿼리를 봐야 하므로, 결과는 성능의 측면에서 볼 때 좋지 않다. Listing 6이 그 이유를 말해준다.


Listing 6. N+1 문제에 대한 쿼리
                
^[[4;36;1mMember Load (0.004063)^[[0m   ^[[0;1mSELECT * FROM members WHERE
 (members.`id` IN (1,2,3)) ^[[0m
  ^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;35;1mAddress Load (0.000989)^[[0m   ^[[0mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 1 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
  ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;36;1mAddress Columns (0.073840)^[[0m   ^[[0;1mSHOW FIELDS FROM addresses^[[0m
^[[4;35;1mAddress Load (0.002012)^[[0m   ^[[0mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 2 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
  ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;36;1mAddress Load (0.000792)^[[0m   ^[[0;1mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 3 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
  ^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m

결과는 예상했던 그대로이다. 모든 멤버에 대한 하나의 쿼리를 사용하고, 각각의 주소에 또 다른 쿼리를 사용한다. 세 개의 멤버를 검색했고, 네 개의 쿼리를 사용했다. 멤버가 N 이라면 쿼리는 N+1 이 된다. 바로 이 문제가 무시무시한 N+1 현상이다. 대부분의 영속성 프레임웍은 이 문제를 제휴(association)를 사용하여 해결한다. Rails도 예외는 아니다. 관계에 액세스 해야 한다는 것을 알고 있다면, 초기 쿼리를 사용하여 이것을 포함시키기 쉽다. ActiveRecord는 :include 옵션을 사용한다. 이 쿼리를 Member.find([1,2,3], :include => :address).each {|member| puts member.address.city}로 변경했다면, 훨씬 더 나은 결과를 얻게 된다.


Listing 7. N+1 문제 해결하기
                
^[[4;35;1mMember Load Including Associations (0.004458)^[[0m   ^[
   [0mSELECT members.`id` AS t0_r0, members.`type` AS t0_r1,
   members.`about_me` AS t0_r2, members.`about_philanthropy`

   ...

   addresses.`id` AS t1_r0, addresses.`address1` AS t1_r1,
   addresses.`address2` AS t1_r2, addresses.`city` AS t1_r3,

   ...

   addresses.`addressable_id` AS t1_r8 FROM members
   LEFT OUTER JOIN addresses ON addresses.addressable_id
   = members.id AND addresses.addressable_type =
   'Member' WHERE (members.`id` IN (1,2,3)) ^[
   [0m
 ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:
  98:in `find'^[[0m


이 쿼리는 훨씬 더 빠르다. 모든 멤버들과 주소들을 검색하는 하나의 쿼리를 볼 수 있다. 바로 이것이 제휴가 작동하는 방식이다.

ActiveRecord에서, :include 옵션을 중첩시킬 수 있지만, 한 레벨만 중첩시킬 수 있다. 예를 들어, 많은 contacts를 갖고 있는 Member가 있고, Contact는 한 개의 address를 갖고 있다고 해보자. 멤버의 연락처 마다 모든 도시들을 보여주고자 한다면, Listing 8의 코드를 사용할 수 있다. .


Listing 8: 멤버의 연락처에서 도시 검색하기
                
member = Member.find(1)
member.contacts.each {|contact| puts contact.address.city}

이 코드도 충분히 잘 작동하지만, 멤버, 각각의 연락처, 연락처의 주소를 쿼리해야 한다. :contacts:include => :contacts를 포함시킴으로써 성능을 더 향상시킬 수 있다. (Listing 9)


Listing 9: 멤버의 연락처에서 도시 검색하기
                
member = Member.find(1)
member.contacts.each {|contact| puts contact.address.city}

중첩된 include 옵션을 사용함으로써 훨씬 더 잘 수행할 수 있다.


                
member = Member.find(1, :include => {:contacts => :address})
member.contacts.each {|contact| puts contact.address.city}

중첩된 include는 Rails에게 contactsaddress 관계를 포함시킬 것을 명령하고 있다. 주어진 쿼리에서 관계를 사용한다는 것을 알 때 마다 이러한 로딩 기술을 사용할 수 있다. 이 기술은 ChangingThePresent.org에 사용하는 성능 최적화 기술이지만, 한계도 있다. 두 개 이상의 테이블을 결합해야 할 때, SQL을 사용하는 것이 더 낫다. 리포팅을 해야 한다면, 데이터베이스 연결을 사용하여 ActiveRecord::Base.execute("SELECT * FROM...")과 함께 ActiveRecord를 우회하는 것이 낫다. 일반적으로, 제휴는 충분하다. 이제는 상속(inheritance)에 대해 알아보자.

상속과 Rails

대부분의 Rails 개발자들이 Rails를 처음 만나면 매료된다. 매우 쉽기 때문이다. 데이터베이스 테이블에 type 컬럼을 생성하고 부모로부터 하위 클래스를 상속받는다. Rails가 그 나머지를 책임진다. 예를 들어, Person이라고 하는 클래스로부터 상속 받은 Customer라고 하는 테이블을 갖게 된다. 고객은 Person의 모든 컬럼을 갖게 되고, 여기에 로열티 넘버와 주문 내역을 갖게 된다. Listing 10은 이러한 솔루션의 강점을 드러내고 있다. 마스터 테이블에는 부모와 모든 하위 클래스들의 컬럼을 갖고 있다.


Listing 10. 상속 구현하기
                
create_table "people" do |t|
  t.column "type", :string
  t.column "first_name", :string
  t.column "last_name", :string
  t.column "loyalty_number", :string
end

class Person < ActiveRecord::Base
end

class Customer < Person
  has_many :orders
end

이 같은 솔루션은 대부분 잘 작동한다. 코드는 간단하고 반복이 없다. 쿼리는 단순하고 성능이 우수하다. 다중 하위 클래스에 액세스 하기 위해 join을 수행할 필요가 없고, ActiveRecord가 type 컬럼을 사용하여 어떤 레코드를 리턴할 것인지를 결정한다.

어떤 부분에서는, ActiveRecord 상속은 매우 제한적이다. 너무 광범위한 상속 계층을 갖고 있다면, 상속은 나뉘게 된다. 예를 들어, ChangingThePresent에서, 각각 이름, 디스크립션, 공통적인 표현 애트리뷰트, 여러 커스텀 애트리뷰트를 가진 여러 콘텐트 유형을 갖고 있다. 공통 베이스 클래스에서 causes, nonprofits, gifts, members, drives, registries, 기타 객체들을 상속받는다. Rails 모델은 단일 테이블에서 객체 모델의 핵심을 갖고 있기 때문에 그럴 수 없다. 이것은 현실적인 솔루션이 아니다.

대안

이 문제에 대한 세 가지 솔루션을 살펴보았다. 하나는, 고유 테이블에 각각의 클래스를 갖고, 뷰를 사용하여 콘텐트에 대한 공통의 테이블을 구현한다. Rails는 데이터베이스 뷰를 잘 다루지 못하기 때문에 일찌감치 이 솔루션을 포기했다.

두 번째 솔루션은 다형성(polymorphism)을 사용하는 것이다. 이러한 전략을 사용하여 각각의 적절한 하위 클래스는 고유의 테이블을 갖는 것이다. 공통의 컬럼들을 각 테이블로 보낸다. 예를 들어, Gift, Cause, Nonprofit 하위 클래스와 name 프로퍼티만 가진 Content 라고 하는 하위 클래스가 필요하다고 가정해 보자. Gift, Nonprofit, Cause 모두는 name 프로퍼티를 갖고 있다. Ruby는 동적으로 유형화 되므로, 공통의 베이스 클래스에서 상속받을 필요가 없다. 같은 메소드 세트에 반응하면 된다. 특히 이미지를 다룰 때, ChangingThePresent는 여러 장소에서 다형성을 사용하여 공통의 작동을 제공한다.

세 번째 대안은, 공통의 기능을 제고하는 것이지만, 상속 대신 제휴를 사용한다. ActiveRecord는 공통의 작동을 상속 없이 클래스에 붙이는데 이상적인 Polymorphic Association이라고 하는 기능을 갖고 있다. Address에서 Polymorphic Association에 대한 예제를 보았다. 필자는 같은 기술을 사용하여 상속 대신에 콘텐트 관리에 공통의 애트리뷰트를 붙일 수 있다. ContentBase라고 하는 클래스를 생각해 보자. 일반적으로, 이 클래스와 또 다른 클래스를 제휴시키려면, has_one 관계와 단순한 외래 키를 사용한다. 하지만, ContentBase가 한 개 이상의 클래스와 작동하도록 해야 한다. 여러분은 대상 클래스의 유형을 정의하는 외래 키와 컬럼이 필요하다. 이는 ActiveRecord Polymorphic Association이 작동하는 방식이다. Listing 11의 클래스를 보자.


Listing 11. 사이트 콘텐트 관계의 양 측면
                
class Cause < ActiveRecord::Base
  has_one :content_base, :as => :displayable, :dependent => :destroy
  ...
end

class Nonprofit < ActiveRecord::Base
  has_one :content_base, :as => :displayable, :dependent => :destroy
  ...
end


class ContentBase < ActiveRecord::Base
  belongs_to :displayable, :polymorphic => true
end

일반적으로, belongs_to 관계는 단 하나의 클래스를 사용하지만, ContentBase의 관계는 다형적(polymorphic)이다. 이 외래 키는 레코드를 구분하는 식별자뿐만 아니라 테이블을 구분하는 유형도 갖고 있다. 이 기술을 사용하여 상속을 활용할 수 있다. 공통 기능은 모두 하나의 클래스에 있다. 하지만, 몇 가지 가외 효과도 있다. 하나의 테이블에 CauseNonprofit 모두에 모든 컬럼을 가질 필요가 없다.

일부 데이터베이스 관리자들은 Polymorphic Association을 좋아하지 않는다. 진정한 외래 키를 사용하지 않기 때문이다. 하지만, ChangingThePresent의 경우, 자유롭게 이들을 사용할 수 있다. 사실, 데이터 모델은 이론만큼 좋은 것은 아니다. 참조적 무결성 같은 데이터베이스 기능을 사용할 수 없고, 컬럼의 이름에 기반하여 관계를 발견하는 툴에 의존할 수 없다. 깨끗하고 단순한 객체 모델의 강점은 이러한 방식이 가진 문제를 뛰어 넘는다.


                
create_table "content_bases", :force => true do |t|
  t.column "short_description",          :string

  ...

  t.column "displayable_type", :string
  t.column "displayable_id",   :integer
end

결론

ActiveRecord는 유능한 영속성 프레임웍이다. 이 프레임웍에서 확장 가능하고, 신뢰성 있는 시스템을 구현할 수 있지만, 다른 데이터베이스 프레임웍들처럼, 여러분의 프레임웍이 생성하는 SQL에 주의해야 한다. 문제가 생기면, 방식을 조정해야 한다. 인덱스를 유지하면서, include와 함께 로딩을 사용하고, 상속 대신에 Polymorphic Association을 사용하는 것은 코드 베이스를 향상시킬 수 있는 방법들이다. 다음 달에는, Rails를 작성하는 또 다른 예제를 설명하도록 하겠다.



참고자료

교육

제품 및 기술 얻기
  • Ruby on Rails: 오픈 소스 Ruby on Rails 웹 프레임웍 다운로드.



필자소개

Bruce Tate

Bruce Tate 는 텍사스 주 오스틴에서 살고 있으며, 마운틴 바이크와 카약을 즐겨 하고 있다. WellGood, LLC의 CTO이며 ChangingThePresent.org의 주 아키텍트이며, Beyond Java, From Java to Ruby, and Ruby on Rails: Up and Running등 아홉 권의 책 저자이다. IBM에서 13년 동안 근무했고, 후에는 Ruby 및 Ruby on Rails 프레임웍 기반 개발 전략과 아키텍처를 전문으로 하는 RapidRed 컨설팅 회사를 설립했다. 현재, Rails 개발자 팀과 함께 작업하면서 자선 포털 사이트인 ChangingThePresent.org를 구현 및 관리하고 있다.




기사에 대한 평가


보다 나은 서비스를 제공하기 위함이오니 잠시 짬을 내어 이 양식을 제출하여 주십시오.



아니오잘 모르겠음
 


 


12345
 



위로


developerWorks 콘텐트를 다른 사이트에 전재하기:
developerWorks 콘텐트에 대한 저작권은 IBM에 있습니다. IBM의 서면 허가나 원본 저자의 허락이 없이는 전재를 금합니다. 저희 콘텐트를 전재하시려면 IBM developerWorks 담당자 에게 문의하십시오.
    IBM 소개 개인정보 보호정책 문의