초보 서버개발자를 위한 Gem! Bullet

안드로이드 어플만 개발하다보면 아무래도 클라 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에 대한 소개를 마친다.

Rails 어플리케이션 개발에서 쉽게 하는 실수

안드로이드 개발을 할때는 그래도 경험에서 판단할 수 있는 정도의 리팩토링 기준이 있었는데 서버의 경우 특히 루비 코드의 경우 코드 자체는 보기에 참 예뻐서 나같은 초보 서버 개발자의 경우 뭐 잘돌아가네 정도로 만족할 수도 있겠다는 생각이 들었다. 최근에 그래서 뭔가 코드 리팩토링을 좀 해야하지 않을까해서 찾은 블로그 글이 있었고 그 중 참고할만한 부분만 정리해봤다. 원글보기

  1. Using Query Method Outside Models

말그대로 액티브레코드 쿼리메소드를 모델에서가 아닌 컨트롤러에서 사용하는 것을 지양하자는 것이다. 알고는 있었지만 생각없이 개발하다보니 where, limit, group, order 등의 쿼리 메소드를 컨트롤러에서 하고 있는 코드가 많이 있었고 비즈니스 로직은 최대한 모델에 있어야 한다는 것이다.

  • Bad
# app/controllers/users_controller.rb
class UsersController < ApplicationController

  def index
    @users = User.where(active: true).order(:last_login_at)
  end

end
  • Good
# app/models/user.rb
class User < ActiveRecord::Base

  scope :active, -> { where(active: true) }
  scope :by_last_login_at, -> { order(:lasst_login_at) }

end
class UsersController < ApplicationController

  def index
    @users = User.active.by_last_login_at
  end

end

방법은 간단하다. 컨트롤러에 있는 쿼리메소드를 scope 를 이용해 모델로 분리하고 컨트롤러에서는 해당 scope 만 가져다 사용하면 끝. 비즈니스 로직이 분리되기때문에 같은 scope 를 재활용해서 사용할 수 있다는 장점이 있다.

2. Using Logic Inside The Views

쿼리메소드가 모델에 있어야 한다면 뷰에서의 로직을 사용하는 것이 안좋다는 뜻. 이것도 역시 아무생각없이 짜면 나중에 리팩토링하기 정말 어려워지는 부분이다. 플리마켓의 경우 홈페이지와 어드민 페이지 빼곤 전부 뷰가 json 으로 이루어져있어서 뷰에 로직이 들어가있는 경우가 거의 없기는하다. 🙂

  • Bad
<!-- app/views/posts/show.html.erb -->
...

<h2>
  <%= "#{@comments.count} Comment#{@comments.count == 1 ? '' : 's'}" %>
</h2>

...

위의 코드가 사실 언뜻보기에 큰문제가 있어보이진 않는다. 하지만 코드가 복잡해질수록 이와 같은 로직들은 코드를 한눈에 파악하기 힘들게 만든다. 추가로 위에 코드는 재사용이 불가능하다는 문제도 있다.

  • Good
# app/helpers/comments_helper.rb
module CommentsHelper
  def comments_count(comments)
    "#{comments.count} Comment#{comments.count == 1 ? '' : 's'}"
  end
end
<!-- app/views/posts/show.html.erb -->
<h2>
  <%= comments_count(@comments) %>
</h2>

위 처럼 뷰 헬퍼에 로직을 분리한 경우에 가장 눈에 띄는 변화는 html 코드를 한눈에 파악할 수 있게 됐다는 점이다. 아주 기초적인 부분이지만 개발일정에 쫒기다 보면 생각보다 신경쓰기 어려울 수 있다. 지금의 편함이 나중에 어떻게 돌아올지는 아무도 모르니…

3.  Using “self.” On Models Instance Methods When There’s No Need To

  • Bad
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    "#{self.first_name} #{self.last_name}"
  end

end
  • Good
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    "#{first_name} #{last_name}"
  end

end

자바 개발이 익숙하다면 처음 레일즈를 할때 이런 실수를 해본 사람이 있을것이다. 위같은 경우 self 는 전혀 불필요하다. self 의 경우 모델에서 메소드가 아닌경우 사용되지 않는다.

위 3가지 모두 사실 굉장히 기초적인 부분이지만 초보자들이 쉽게 잘못하는 실수이기도하다. 원글의 내용에는 더 많은 조언들이 있으니 빠르게 한번 읽어보면 좋을 듯하나 어떻게 보면 너무 당연한 얘기들이라 생략했다.

깨끗한 코드는 개발자의 자존심!

Capistrano 를 이용한 서버 배포 및 ssl 설정

1. 배포

서버 설정을 마쳤으니 이제 실제 서버 배포 방법을 알아보자..

간혹가다 서버 배포설정을 안하고 ftp를 이용해 파일을 올린다던가 하는식으로 운영을 하는 경우가 있으나 상당히 위험하고 불편하다. 개인적으로 서버 배포 시스템은 필수로 구축해야 한다고 생각한다.

rails 배포 툴 중에 가장 잘 알려진 것이 capistrano 라는 gem 이다. 설정이 쉽고 다양한 플러그인을 지원한다. (예를 들면 옵션에 따라 배포시 db:migrate 를 자동으로 해주거나 데몬을 실행해주거나 하는등등) 현재 최신버전은  capistrano 3 이다.

capistrano + nginx + unicorn 조합인 경우 capistrano-unicorn-nginx 라는 플러그인이 있으니 그걸 함께 사용해보도록 하자.

2. 설정

  • 로컬 프로젝트 GemFile 에 capistrano 와 capistrano-unicorn-nginx 추가 후 bundle 명령어로 gem 설치해보자
group :development do
 gem 'capistrano', '~> 3.2.1'
 gem 'capistrano-unicorn-nginx', '~> 3.2.0'
end

$bundle

이제 config/deploy/production.rb 에 적절히 설정값을 넣어준다.

server 'xxx.xxx.xxx.xxx(ip주소)', user: 'deployer', roles: %w{web app db} 
set :nginx_server_name, 'fleamarkets.co.kr' #아직 도메인이 없는 경우 생략 가능

여기서 role 을 설정할때 db를 반드시 넣어줘야 서버 배포시 db migration 이 정상적으로 동작한다.

로컬 app config/database.yml db 설정은 빼먹지 말자.

production:
 adapter: postgresql
 database: fleamarket_production
 encoding: unicode
 host: localhost
 pool: 5
 username: fleamarket
 password: xxxxxxxxx
 timeout: 5000

Capfile 도 적절히 세팅해준다. db migration, asset pipeline 이 배포시 잘 동작하도록 설정해야한다. 나같은 경우 rbenv 로 루비 버전 관리를 하기 때문에 rbenv 플러그인도 포함시켜주었다.

# Load DSL and Setup Up Stages
require 'capistrano/setup'

# Includes default deployment tasks
require 'capistrano/deploy'

# Include plugin
require 'capistrano/unicorn_nginx'
require 'capistrano/bundler'
require 'capistrano/rbenv'
require 'capistrano/rails'

# Loads custom tasks from `lib/capistrano/tasks' if you have any defined.
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

만약 https 를 지원하고 싶으면 추가 적인 설정을 해줘야한다. https 를 지원하는 부분은 다음 포스팅에서 기본적인 부분만 다루도록할 예정이다.

자, 이제 거의 배포를 위한 준비가 완료된 상태이다.

capistrano-unicorn-nginx gem 의 경우 디폴트 배포 위치가 /var/www/app_name/ 안에 배포가 됨으로 미리 서버에 해당 디렉토리를 만들고 적절한 권한을 부여해야한다.

sudo mkdir /var/www/fleamarket
sudo chown deployer:admin /var/www/fleamarket/

bundle exec cap production setup

여기까지하면 잘되야하지만 실제로 해보니

sudo stdout: Nothing written
sudo stderr: sudo: no tty present and no askpass program specified

와 같은 에러를 뿜으며 종료되었다.

구글링을 해보니 sudo 명령어를 사용할때 deployer 유저인 경우 서버 password를 묻지 않도록 해야한다는 글이 있다.

특정 유저에게 sudo 권한을 주는건 간단하다.

sudo visudo

%admin ALL=(ALL) ALL 아래에
deployer ALL=(ALL:ALL) NOPASSWD:ALL 를 추가해주고 저장 해준다. editor 가 익숙하지 않다면 직접 vim을 이용해 sudoers 파일을 수정해줘도 된다.

수정시에 위치가 중요한데 어드민 설정 아래에 넣지 않으면 어드민이 NOPASSWD를 오버라이드해서 설정이 적용되지 않는다.

마지막으로 실서버 환경에 배포하려면 config/secret.yml 에 secret_key 를 세팅해줘야한다.

secret_key는 rake secret 명령어를 사용하면 자동으로 생성된다. 해당 값을 copy paste 로 붙여넣어준다. (실제 서버를 운영할때는 서버 환경 변수로 넣어주는 것이 보안상 좋다!)

production:
 secret_key_base: xxxxxxx
 secret_token: xxxxxxx

3. 배포

자 이제 드디어 최종 배포를 해본다.

bundle exec cap production deploy

이 과정에서 한번에 잘 되는경우도 있지만 다양한 에러가 나올 수 있다.

내 경우엔 rails asset precompile 과정중에 서버 메모리가 부족해서 process 가 kill 되는 문제가 있었다. 관련해서는 잘 정리된 thread를 참고하면된다.

http://stackoverflow.com/questions/22272339/rake-assetsprecompile-gets-killed-when-there-is-a-console-session-open-in-produ

성공적으로 deploy 됐다면 브라우져에 내 rails app이 잘 나오는지 확인해본다! Good!!

Ruby on Rails 서버 세팅하기

  1. 배경

클라이언트 개발자의 경우 서버 혹은 웹개발에 대한 갈증을 가진 경우가 종종 있다. 나도 안드로이드 경력은 오래됐으나 서버를 배울 기회는 거의 없었고 항상 서버개발에 대한 갈증이 있었던 것 같다. 이번에 친한 지인들과 플리마켓이라는 서비스를 준비하면서 서버 개발을 해보고 싶어 뭣도 모르는 상태에서 급 서버 개발을 맡게 되었다.

서버 개발을 위해서는 현재 나의 상황에 맡게 어떤 OS를 사용할지 어떤 Web framework를 쓸지 배포는 뭘로할지 DB는 어떤걸 쓸지를 잘 선택해야한다. 내가 중요한게 생각했던건 2가지 첫째, “쉬워야한다” 둘째, “레퍼런스가 많아야한다” 어차피 한글로된 정보는 거의 없고 정말 막히는 부분에서는 결국 구글(스택오버플로우)의 도움을 받아야한다. 이 기준으로 정한 기준이다.

  • OS – Ubuntu 12.04
  • Web framework – Ruby on Rails
  • DB – Postgresql

Ubuntu 의 경우 16.04 까지 나와있으나 안정적인 개발을 위해 레퍼런스가 많은 12버전을 선택했다. RoR은 그나마 쉽고 API와 어드민을 개발하기에는 상당히 적합하고 또 국내 사이트에는 별로 없으나 해외 사이트를 기준으로 보면 장고나 플라스크등에 비해 레퍼런스가 많아서 어지간한 문제는 구글님께서 해결해주시는 장점이 있다.

호스팅 서버는 해외 서비스인 Vultr 을 선택했다. 해외 서비스의 경우 서버 스팩이 상당히 좋고 서비스를 관리하는데도 국내 서비스에 비해 많은 장점이 있다. 서버를 선택하면서 가장 많이 참고를 하게된 블로그 포스팅이다. 정리가 잘 되어있고 계속해서 최신정보를 업데이트 해주시는 감사한 분이다. 🙂

2. 서버 설정

가상서버를 발급받는건 Vultr 사이트에 가입하고 5분이면 된다. 너무 직관적이고 쉬워서 생략한다. 발급받은 ip와 비밀번호로 루트 계정으로 터미널을 이용해 로그인해보자.

1. 패키지 정보 최신으로 업데이트

설치해야할 것이 많으므로 패키지를 최신으로 업데이트 하자.

root@fleamarket:~# apt-get update
2. curl, git 등 서버 세팅에 기본적으로 필요한 패키지 설치

python-software-properties 는 ubuntu 12.04에 기본으로 되어있는 리포지토리의 일부 패키지가 너무 오래된 경우가 있다. 그래서 리포지토리를 쉽게 설치하기 위해 꼭 필요하다.

root@fleamarket:~# apt-get -y install curl git-core python-software-properties
3. nginx 설치 최신 버전 설치를 위해 리포지토리 추가
root@fleamarket:~# add-apt-repository ppa:nginx/stable
root@fleamarket:~# apt-get update
root@fleamarket:~# apt-get -y install nginx
root@fleamarket:~# nginx -v
(nginx version: nginx/1.8.0)
root@fleamarket:~# service nginx start

스크린샷 2015-05-30 오전 1.32.44

브라우져에 ip주소를 쳐보면 특별한 문제가 없으면 nginx 기본 페이지가 보인다! 이제 반은 끝난것 🙂

 

4. DB 설치 및 설정 (Postgresql)

DB 는 postgresql을 사용했다. 이 부분은 특별한 이유는 없고 기존에 한번 사용해본 경험이 있고 역시 레퍼런스가 많아서 선택하게됐다. 좋다고는 하는데 뭐가 좋은지는…

설치
root@fleamarket:~# apt-get install postgresql postgresql-contrib libpq-dev
콘솔 접속
root@fleamarket:~# sudo -u postgres psql
최초 설치시에는 postgres 유저의 비밀번호가 없으므로 설정해주는것이 좋다.
pastgres=# \password
Enter new password:
Enter it again:
db유저 생성 (난 항상 앱이름으로 유저 이름을 쓴다) 및 db 만들기
postgres=# create user fleamarket with password 'password';
postgres=# create database fleamarket_production owner fleamarket;
postgres=# \q (종료)

db 설정을 마쳤다.

5. postfix 와 node.js 설치

플리마켓의 경우 이메일 인증이 들어가므로 postfix 패키지를 설치해야한다. postfix 설치시에 Internet Site 를 선택후 나머진 기본 값을 선택해주면된다. 추가로 레일즈에서 자바 스크립트의 효율적으로 사용과 assets pipeline 을 위해 node.js를 설치해준다.

root@fleamarket:~# apt-get install postfix
root@fleamarket:~# add-apt-repository ppa:chris-lea/node.js (리포지토리 추가)
root@fleamarket:~# apt-get update
root@fleamarket:~# apt-get -y install nodejs
6. 루비 설치

루비 설치전에 배포를 위한 계정을 생성해주자. 보안을 위해서는 서버 배포관련된 부분은 루트계정을 사용하지 않는것이 정석이다. 루비를 설치하기전에 계정을 미리생성하는것은 루비 버전 관리자를 통해 루비를 설치하고 싶어서이다. 루비버전을 관리하는 잘나가는(?) 루비 버전관리자가 rbenv, rvm 이렇게 두가지가 있다. 난 최근 가장 많이 사용된다는 rbenv를 사용해보았다.

root@fleamarket:~# addgroup admin
root@fleamarket:~# adduser deployer --ingroup admin
root@fleamarket:~# su deployer
deployer@fleamarket:/root$ cd
deployer@fleamarket:~$ curl -L https://raw.github.com/fesplugas/rbenv-installer/master/bin/rbenv-installer | bash

여기까지 하면 rbenv 설치까지 완료된다. 완료 후 나오는 콘솔 메세지를 잘 살펴보면 환경변수를 설정하라고 설명이 되어있다.

export RBENV_ROOT="${HOME}/.rbenv"

if [ -d "${RBENV_ROOT}" ]; then
 export PATH="${RBENV_ROOT}/bin:${PATH}"
 eval "$(rbenv init -)"
fi
deployer@fleamarket:~$ vim /home/deployer/.bashrc

스크린샷 2015-05-30 오전 11.52.11

deployer@fleamarket:~$ . ~/.bashrc
deployer@fleamarket:~$ rbenv bootstrap-ubuntu-12-04
deployer@fleamarket:~$ rbenv install 2.2.2

deployer@fleamarket:~$ rbenv global 2.2.2
deployer@fleamarket:~$ ruby -v
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux]
deployer@fleamarket:~$ gem install bundler --no-ri --no-rdoc
deployer@fleamarket:~$ bundle -v
Bundler version 1.10.1

환경 변수를 정상적으로 추가하고 적용해준다.

bootstrap-ubuntu-12-04 는 ruby를 설치하기전에 필요한 디펜던시를 미리 설치해준다.

루비를 설치하는과정은 서버 성능에 따라 상당히 오래걸리니 커피한잔 마시고 오는것을 추천한다. 🙂

루비 설치가 완료되면 레일즈 앱 배포를 위한 서버 설정은 끝난것이다. 만세!

다음번에는 개발중인 레일즈 앱을 실제 서버에 deploy 하는 방법을 포스팅해보겠다.

안녕하세요 페퍼앤쏠트 개발자 블로그입니다.

안녕하세요 페퍼앤쏠트의 서버개발자입니다.

사실은 안드로이드 개발자이지만 플리마켓 서비스 개발을 시작하면서 서버개발이 하고 싶어서 전향(?) 아닌 전향을 하게됐습니다.

이공간은 제가 개인적으로 서버개발을 처음 접하면서 습득한 지식 위주로 포스팅을 해보고 싶어서 만들었습니다.

원래 서버 개발을 하셨던 분들은 별볼일이 없으실꺼같구요~ 처음 접하시는 분들에게 도움이 될 수 있도록 노력하겠습니다.

감사합니다.