안드로이드 어플만 개발하다보면 아무래도 클라 DB 쿼리 성능에 대해서는 신경을 많이 쓰지않는다. 사실 그다지 쿼리가 복잡할 일도 없고 테이블도 최소한으로 유지하기 때문에 성능이슈가 많지않다. 클라이언트 DB의 경우 인덱스만 적절히 잘 달아주는 정도로도 충분하고 오히려 UI를 얼마나 최적화된 상태로 그려주는지 IO, network 작업을 를 얼마나 적절히 백그라운드로 돌려주는지 혹은 캐시를 적절히 활용해주는지 여부가 더 중요하다.
하지만 서버 개발을 하면서 부터는 이야기가 달라진다. 즉, 모든 유저의 쿼리에 대응해야해서 성능이 중요하고 캐싱또한 더 중요해진다. 슬로우 쿼리하나가 서비스 성능에 큰 영향을 끼친다. 나같은 초보 서버 개발자가 가장 처음 어려움을 겪는 부분이 아마 이런 부분인것 같다. 즉, 도대체 테이블 구성을 어떻게 할것이며 쿼리는 어떻게 해야 최적화된 쿼리를 할 수 있는건지.
DB 테이블이 관계를 맺는 경우 레일즈 개발에서 신경쓰지 않으면 N+1 쿼리를 수행하게 된다. 즉 99명의 유저에게 원하는 정보를 얻기위해 100번 쿼리를 수행한다는 것이다.
나같은 초보의 경우 충분히 저지를 수 있는 실수고 이 문제를 경고해주는 라이브러리가 바로 Bullet 이다. Bullet 은 액티브레코드의 쿼리메소드를 수행하는 순간 이 쿼리가 N+1 쿼리를 발생시키는지를 보고 경고를 주고, 해결책또한 제시해준다!
Bullet 이 기본적으로 제공하는 3가지 핵심 기능이다.
- Detect N+1 queries
- Detect eager-loaded associations which are not used
- Detect unnecessary COUNT queries which could be avoided
개발의 편의를 위해 다양한 상세 설정이나 white list 기능등을 이용해 bullet 의 기능을 disable 할 수 있으나 디폴트로 모두 켜두어도 특별히 불편한 부분은 없다.
1. gem 설치
# Gemfile.rb group :development do gem 'bullet' end
deployer@fleamarket:~$ bundle
개발환경에서만 필요하므로 development 그룹에 넣어준다.
2. Log
Bullet 은 rails 콘솔 로그를 통해 검출 결과를 알려준다. 개발환경에서만 나오고 거의 로그 맨 마지막에 detect 결과를 보여주기 때문에 보기에 불편함은 없다. 대표적인 3가지 로그는 다음과 같다.
- N+1 Query
2009-08-25 20:40:17[INFO] N+1 Query: PATH_INFO: /posts; model: Post => associations: [comments]·
Add to your finder: :include => [:comments]
2009-08-25 20:40:17[INFO] N+1 Query: method call stack:·
/Users/richard/Downloads/test/app/views/posts/index.html.erb:11:in `_run_erb_app47views47posts47index46html46erb'
/Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each'
/Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `_run_erb_app47views47posts47index46html46erb'
/Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index'
- Unused eager loading
2009-08-25 20:53:56[INFO] Unused eager loadings: PATH_INFO: /posts; model: Post => associations: [comments]·
Remove from your finder: :include => [:comments]
- Need counter cache
2009-09-11 09:46:50[INFO] Need Counter Cache
Post => [:comments]
3. 코드 수정
다음과 같은 relation 이 있는 경우를 예로 들어보자.
class Post < ActiveRecord::Base has_many :comments end class Comment < ActiveRecord::Base belongs_to :post end
@post = Post.find(1) @post.comments //이시점에 N+1 query 발생!
@post 객체를 받아올때까지는 comments 의 사용여부를 알 수 없으므로 엑티브레코드는 최적의 쿼리를 위해 comments 에 대한 고려를 하지 않는다.
그래서 @post.comments 를 호출하는 순간 comments 수만큼의 N번 쿼리가 일어나게되며 Bullet 은 이에대해 N+1 Query 가 있다는 것을 알려준다.
N+1 query 를 해결하는 방법은 간단하며 해결방법 또한 bullet 이 제공해준다.
@post = Post.includes(:comments).find(1) @post.comments //N+1 쿼리가 발생하지 않는다.
위처럼 레일즈의 includes 만 추가해주면 comment id 를 이용해 한번에 쿼리를 할 수 있다.
만약 여기서 @post.comments.size 를 호출하면 어떻게 될까? size 를 알기위해서 다시 N+1 쿼리가 발생하게 된다.
일반적으로 레일즈에서는 이와같은 문제를 해결하기 위해 counter_cache 를 제공한다. 즉, 특정 모델에 insert delete 가 일어나는 순간 마다 count 를 기록해주는 컬럼값을 증가 혹은 감소 시켜주는 것이다. 그래서 counter_cache는 정확하게 싱크가 되지 않으면 잘못된 값을 가질 수 있다.
counter_cache 를 사용하는 방법은 간단하다.
has_many 의 many 쪽 테이블에 :counter_cache => true 라고 명시해주고 Post 테이블에 comments_count 라는 컬럼만 추가해주면 레일즈에서 자동으로 count 값을 맞춰준다. 그리고 @post.comments.size 를 호출하면 알아서 counts_count 컬럼에서 값을 꺼내온다!
class Comment < ActiveRecord::Base belongs_to :post, :counter_cache => true end
Bullet 로그는 console 을 통해서도 확인이 가능하지만 front 개발이 있는 경우 브라우져에서도 팝업으로 로그를 보여준다.
숙련된 서버 개발자는 사실상 별로 필요없는 gem이 되겠지만 나같은 초보 서버 개발자에게는 가뭄의 단비같은 gem이다. 🙂
여기까지 Bullet gem에 대한 소개를 마친다.