logrotate로 리눅스 로그 관리


웹 서비스를 운영할 때 쉽게 놓칠 수 있는 것 중 하나가 로그 관리다. 로그를 관리하지 않아도 운영하는데는 문제가 없으며 운영 초반에는 로그를 관리해야할 정도로 트래픽이 발생하는 경우가 드물기 때문이다.

하지만 시간이 점점 지나고 트래픽이 늘어나면 로그는 고스란히 디스크에 쌓이게 되고, 이를 그대로 방치해두면 엄청난 디스크 용량을 낭비하게 된다.

끝도 없이 늘어나는 로그 파일을 관리하기 위해 리눅스에서는 logrotate라는 프로그램을 사용하면 좋다. logrotate는 정해진 시간마다 로그 파일을 백업시켜주는데, 로그 파일이 무작정 늘어나는 것을 방지하기 위해 로그 파일의 최대 개수를 정해놓으면 최대 개수를 초과했을 때 가장 오래된 로그 파일을 삭제하고 새로운 로그 파일을 생성하면서 rotating 해주는 툴이다.

시스템에 설치되어있지 않다면 설치해준다.

sudo yum install -y logrotate

/etc/logrotate.conf 파일에 어떤 로그를 로테이팅할지 작성할 수 있다. 파일을 열어보면 기본으로 작성된 코드가 있을 것이다.

# see "man logrotate" for details
# rotate log files weekly
weekly

# keep 4 weeks worth of backlogs
rotate 4

# create new (empty) log files after rotating old ones
create

# use date as a suffix of the rotated file
dateext

# uncomment this if you want your log files compressed
#compress

# RPM packages drop log rotation information into this directory
include /etc/logrotate.d

# no packages own wtmp and btmp -- we'll rotate them here
/var/log/wtmp {
    monthly
    create 0664 root utmp
        minsize 1M
    rotate 1
}

/var/log/btmp {
    missingok
    monthly
    create 0600 root utmp
    rotate 1
}

# system-specific logs may be also be configured here.

중간에 include /etc/logrotate.d를 보면 /etc/logrotate.d 디렉토리의 파일들을 전부 불러오는 것을 알 수 있다. 그럼 이제 /etc/logrotate.d 디렉토리에 새로운 설정 파일을 생성하고 레일즈 프로젝트의 로그를 관리하도록 작성해보자.

# /etc/logrotate.d/myproject

/home/ec2-user/myproject/log/production.log {
  weekly
  rotate 4
  missingok
  dateext
  postrotate
    touch /home/ec2-user/myproject/tmp/restart.txt
  endscript
}

이렇게 설정한 뒤에 한 번 실행해보자.

sudo /usr/sbin/logrotate /etc/logrotate.d/myproject

처음 실행했을 때는 아무 일도 일어나지 않는다. 왜냐면 logrotate는 실행될 때 /var/lib/logrotate.status 파일을 통해 정해진 기간이 지났는지 확인하는데 방금은 아무 정보가 없었기 때문이다. /var/lib/logrotate.status 파일에 현재 날짜만 기록하고 로테이팅이 실행되지는 않았다.

# /var/lib/logrotate.status

logrotate state -- version 2
"/var/log/yum.log" 2016-1-1
"/home/ec2-user/myproject/log/production.log" 2016-12-10
"/var/log/dracut.log" 2016-1-1
"/var/log/wtmp" 2015-12-1
"/var/log/spooler" 2016-12-4
"/var/log/btmp" 2016-12-1
"/var/log/maillog" 2016-12-4
"/var/log/secure" 2016-12-4
"/var/log/messages" 2016-12-4
"/var/account/pacct" 2015-12-1
"/var/log/cron" 2016-12-4

production.log 파일이 오늘 날짜(2016-12-10)로 실행되었다는 것이 저장되었다. 이제 12월 17일이 되어야 1주일이 지났다는 것을 인식하고 로테이팅이 실행될 것이다.

지금 당장 확인해보려면 2016-12-10을 일주일 전인 2016-12-03으로 바꾼 뒤 sudo /usr/sbin/logrotate /etc/logrotate.d/myproject를 한번 더 실행해보면 된다.


MVC 웹 프레임워크의 문제점


나는 2014년 10월에 레일즈를 처음 접했고 2년 동안 거의 모든 웹 프로젝트에 레일즈를 사용했다.

그리고 레일즈를 배우면서 루비를 배웠다. 그 때의 나에게 루비는 “파이썬과 비슷한 동적 스크립트 언어” 그 이상도 이하도 아니었다. C나 Java와는 달리 컴파일도 필요없고 변수에 타입도 없다. 딱 이 수준이었다.

C, Java, Python을 조금씩 해봤기 때문에 변수, 반복문, 조건문 이런 기본적인 개념은 금방 익혔고 이 정도만 알아도 레일즈로 개발하는데는 큰 문제가 없었다. 웬만한 필요한 기능은 Gem으로 만들어져 있었고 막히는 부분은 가이드 문서나 튜토리얼 아니면 스택오버플로우를 찾아 봤다. 시간이 좀 지나고나니 웬만한건 레일즈로 만들어낼 수 있게 되었다.

프로젝트의 규모가 조금 커지자 문제가 발생했다. 기존의 기능을 수정하거나 새로운 기능을 추가하는 등 시스템에 변화가 생길 때마다 코드를 짜기가 너무 힘들어졌다. 비즈니스 로직들은 한 눈에 알아보기도 힘들 정도로 여기저기 흩어져 있어서 이 코드가 무슨 코드였는지 파악하기 위해 전체 코드들을 다 살펴봐야 했다. 대체 뭐가 문제였을까?

MVC

레일즈는 MVC 패턴을 적용한 웹 프레임워크이며 View는 사용자와, Model은 데이터베이스와 상호작용하며 Controller는 View와 Model 사이에서 다리 역할을 한다.

레일즈를 처음 접했을 때 한 번씩은 읽어봤을법한 MVC 패턴 설명이다. 레일즈는 완벽한 MVC 프레임워크이다. 모델은 ActiveRecord를 상속받아서 데이터베이스와 쉽게 통신할 수 있고, 뷰는 HTML으로 이루어져있으며 컨트롤러는 모델 객체를 활용하여 특정 기능을 수행한 뒤 사용자에게 뷰를 렌더한다.

그렇다면 우리가 개발할 앱의 비즈니스 로직은 대체 어디에 들어가야할까? Controller일까? 아니면 Model일까? (View는 당연히 아니다)

1. Controller

다음은 Rails Tutorial에 작성된 로그인 코드다. 유저를 데이터베이스에서 찾아서 비밀번호를 검사하는 간단한 비즈니스 로직이 컨트롤러에 들어가있는 것을 볼 수 있다.

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # Log the user in and redirect to the user's show page.
    else
      # Create an error message.
      render 'new'
    end
  end

  def destroy
  end
end

단일 책임 원칙(Single Responsibility Principle)이라는 객체지향 원칙이 있다. 이 원칙을 지키려면 “코드를 변경하는 이유”가 딱 한가지만 있으면 된다. 위 코드는 크게 2가지 이유로 변경될 수 있다.

  1. 로그인 처리 후 렌더링하는 페이지가 바뀔 경우
  2. 로그인 방식이 바뀔 경우(ex. 이메일이 아니라 이름으로 로그인)

컨트롤러에는 고유의 책임이 이미 주어져 있다. 유저로부터 요청을 받아서 적당한 응답을 던져주는 것이 바로 컨트롤러의 책임이다. 이미 책임이 부여된 컨트롤러에 비즈니스 로직을 작성하는 것은 당연히 단일 책임 원칙을 위배하며 따라서 컨트롤러에 비즈니스 로직을 넣는 것은 좋은 방법이 아니다.

2. Model

Skinny controllers, Fat models

레일즈 개발자라면 한 번쯤은 들어봤을만한 문장이다. 컨트롤러는 최대한 가볍게 유지하고, 웬만한 코드는 모델에 넣으라는 뜻이다.

하지만 생각해보면 이것도 틀렸다는 것을 알 수 있다. 컨트롤러는 고유의 책임이 이미 존재해서 비즈니스 로직을 넣을 수 없었다. 그렇다면 모델은 주어진 책임이 없을까? 모델은 ActiveRecord를 상속받음으로써 데이터베이스와 통신할 수 있게되며 이 때 상속받는 메서드만 200개가 훨씬 넘는다고 한다.

즉, 모델은 ActiveRecord를 상속받는 순간 Persistence layer의 책임을 갖게된다. 위의 로그인 코드에서 User.find_by(...)가 바로 모델이 해야하는 일이다. 결국 모델도 비즈니스 로직을 담을만한 곳은 아니다.

문제점?

레일즈에 비즈니스 로직을 넣을만한 곳이 없는 것이 문제일까? 레일즈는 프레임워크다. 프레임워크는 대부분의 일반적인 상황에서 사용할 수 있는 코드를 제공한다. 반면에 비즈니스 로직은 우리가 개발하는 애플리케이션에 specific하다. 따라서 프레임워크에 비즈니스 로직을 넣을 곳이 없는 것은 문제가 되지 않는다. 없으면 만들면 되기 때문이다.

문제는 바로 프레임워크 위에 모든 코드를 작성하게 하는 가이드와 수많은 튜토리얼들, 가장 큰 문제는 그것을 이상하다고 여기지 않고 그대로 받아들였던 나 자신이다. 아마 프레임워크를 통해 언어를 배웠기 때문에 기본기가 부족해서 그러지 않았나 싶다. 뭐든지 기본기가 탄탄해야 한다는 것을 다시 한 번 배우게 되었다.

해결 방법

몇 달 전부터 비즈니스 로직을 프레임워크로부터 분리시키는 방법을 찾으려고 했다. 이전에 번역한 [[번역] 액티브레코드 모델을 리팩토링하는 7가지 방법] 도 그런 의도에서 작성한 것이었다. 몇 가지 종류의 Plain Old Ruby Objects를 작성해서 비즈니스 로직을 분리시킬 수 있다.

그리고 좋은 해결책이 될 수 있는 밥 아저씨의 Clean Architecture 강의 영상을 찾았는데 다음 포스트에서는 이 영상을 다뤄봐야겠다.


[Rails] Rake task 사용하기


Rake는 Ruby 개발 환경에서 사용되는 빌드 프로그램이다. Unix에서 사용되는 Make와 비슷한 용도로 사용되며 Makefile과 비슷한 Rakefile이 존재한다.

Rake를 통해 실행되는 작업을 태스크(task)라고 하며 태스크들은 레일즈 서버의 실행여부와 상관 없이 단독으로 실행된다. 레일즈를 설치하면 Rake 젬도 같이 설치되어서 바로 Rake 커맨드를 사용할 수 있다.

레일즈로 개발해봤다면 rake 명령어를 써 본 경험이 있을텐데, 가장 익숙한 명령어는 rake db:migrate 또는 rake routes일 것이다. 이것들은 레일즈에 기본으로 탑재되어 있는 태스크다.

실행할 수 있는 태스크 목록은 rake -T 또는 rake --tasks를 실행하면 볼 수 있다.

Task 만들기

기본적으로 제공하는 태스크 뿐만 아니라 원하는 태스크를 생성하고 실행시킬 수 있다. lib/tasks/my_task.rake 파일을 생성해서 첫 번째 태스크를 작성해보자.

# lib/tasks/my_task.rake

task :random_fruit do
  puts ["Apple", "Banana", "Orange", "Kiwi"].sample
end

파일을 저장하고 rake random_fruit 명령을 입력하면 랜덤으로 과일 이름이 출력되는 것을 볼 수 있다.

이번에는 첫 번째와 마지막 과일을 출력해주는 태스크를 만들어 보자.

# lib/tasks/my_task.rake

arr = ["Apple", "Banana", "Orange", "Kiwi"]
task :random_fruit do
  puts arr.sample
end

task :first_fruit do
  puts arr.first
end

task :last_fruit do
  puts arr.last
end

실행 결과는 다음과 같다.

이렇게 비슷한 태스크들은 네임스페이스로 묶을 수 있다.


arr = ["Apple", "Banana", "Orange", "Kiwi"]
namespace :fruit do
  task :random do
    puts arr.sample
  end

  task :first do
    puts arr.first
  end

  task :last do
    puts arr.last
  end
end

실행 결과는 다음과 같다.

지금까지는 간단한 루비 코드로 태스크를 생성했는데 좀 더 복잡하게 레일즈 프로젝트의 실행 환경과 연동시킬 수도 있다. 다음 코드는 과일 목록으로 String array가 아닌 데이터베이스에 저장된 Fruit 모델을 사용하는 예제이다.


namespace :fruit do
  task :random => :environment do
    puts Fruit.all.sample.name
  end

  task :first => :environment do
    puts Fruit.first.name
  end

  task :last => :environment do
    puts Fruit.last.name
  end
end

코드의 재사용성을 높이기 위해 자주 사용되는 코드를 메서드로 분리시킬 수 있다. 다음 예제는 중복 코드를 없애지는 않지만 메서드 사용을 보여주기 위해 작성했다.

namespace :fruit do
  task :random => :environment do
    puts random_fruit
  end

  task :first => :environment do
    puts first_fruit
  end

  task :last => :environment do
    puts last_fruit
  end

  def random_fruit
    Fruit.all.sample.name
  end

  def first_fruit
    Fruit.all.first.name
  end

  def last_fruit
    Fruit.all.last.name
  end
end

마지막으로, 정의된 모든 태스크 목록을 보기 위해 rake -T 명령어를 사용했었는데 다시 한번 명령어를 입력해보면 위에서 정의한 태스크는 목록에 나오지 않는 것을 볼 수 있다. 태스크 목록에 커스텀 태스크가 나오게 하기 위해서는 태스크 상단에 Description을 입력해주면 된다.

namespace :fruit do
  desc "Pick a item randomly"
  task :random => :environment do
    puts random_fruit
  end

  desc "Pick the first item"
  task :first => :environment do
    puts first_fruit
  end

  desc "Pick the last item"
  task :last => :environment do
    puts last_fruit
  end

  def random_fruit
    Fruit.all.sample.name
  end

  def first_fruit
    Fruit.all.first.name
  end

  def last_fruit
    Fruit.all.last.name
  end
end

MYSQL에 이모티콘 저장하기


MYSQL에 이모티콘을 저장하려 하면 Incorrect string value 에러가 발생하면서 저장되지 않는 문제가 발생했다.

mysql> desc faqs;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| question   | varchar(255) | YES  |     | NULL    |                |
| answer     | text         | YES  |     | NULL    |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

mysql> insert into faqs(question, answer, created_at, updated_at)
          values('This is question', 'This is answer 😎 ', NOW(), NOW());
ERROR 1366 (HY000): Incorrect string value: '\xF0\x9F\x98\x8E ' for column 'answer' at row 1

mysql> select * from faqs;
Empty set (0.00 sec)

utf8 인코딩은 3바이트까지 지원하는데 이모티콘은 4바이트를 차지하기 때문에 문제가 발생한다고 한다. 이를 해결하기 위해 MYSQL 5.5.3 부터 4바이트를 지원하는 utf8mb4라는 캐릭터 셋이 추가되었다. 그러므로 이모티콘을 지원하기 위해서는 테이블의 캐릭터 셋을 utf8에서 utf8mb4로 변경하면 된다.

레일즈 프로젝트에서 마이그레이션 하기

기존 테이블들의 캐릭터 셋을 변경하는 마이그레이션 파일을 생성한다.

rails g migration ConvertCharsetToUtf8mb4

Raw SQL을 작성해서 원하는 테이블의 캐릭터 셋 속성을 변경해준다. 그리고 인덱싱되는 VARCHAR 형식의 컬럼이 있다면 최대 길이를 191로 변경해야 한다. InnoDB 엔진의 인덱스 최대 길이는 767 bytes 이기 때문에 최대 글자 수는 3 bytes의 utf8은 255자, 4 bytes의 utf8mb4에서는 191자가 된다.

class ConvertCharsetToUtf8mb4 < ActiveRecord::Migration[5.0]
  def change
    execute("ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
    execute("ALTER TABLE table_name CHANGE column_name VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
  end    
end  

config/database.yml 파일에 인코딩을 변경해준다.

production:
  ...
  encoding: utf8mb4
  collation: utf8mb4_unicode_ci

이제 마이그레이트를 실행하면 utf8 대신 utf8mb4가 적용되고 이모티콘이 잘 저장되는 것을 볼 수 있다.

mysql> insert into tags(name, created_at, updated_at) values ("Emoji😎 ", NOW(), NOW());
Query OK, 1 row affected (0.00 sec)
mysql> select * from tags;
+----+------------+---------------------+---------------------+
| id | name       | created_at          | updated_at          |
+----+------------+---------------------+---------------------+
|  1 | Emoji😎     | 2016-10-11 20:19:02 | 2016-10-11 20:19:02 |
+----+------------+---------------------+---------------------+
1 row in set (0.00 sec)

마이그레이트를 진행하면서 Mysql2::Error: Specified key was too long; max key length is 767 bytes: CREATE UNIQUE INDEX 에러가 발생할 수도 있다. 이 문제는 이미 생성되어 있는 인덱스의 길이가 아직 255로 지정되어 있어서 발생하는 것 같다. 만약 이 문제가 발생한다면 마이그레이션 파일에 인덱스를 지우고 새로 만드는 코드를 작성해주자.

class ConvertCharsetToUtf8mb4 < ActiveRecord::Migration[5.0]
  def change
    remove_index(table_name, index_name)
    add_index(table_name, index_name, unique: true, using: :btree, length: {column_name: 191}

    execute("ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
    execute("ALTER TABLE table_name CHANGE column_name VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
  end    
end  

참고 링크


터미널 멀티플렉서 tmux를 배워보자


tmux는 터미널 화면을 여러개로 분할하고, 세션을 생성하여 attach/detach를 자유롭게 할 수 있는 터미널 멀티플렉서이다. Vim과 tmux를 함께 사용하는 동영상 을 계기로 tmux를 알게 되었는데, 좋은 영상이니 한 번쯤 보는 것을 추천한다. 초중반엔 Vim 튜토리얼, 후반엔 tmux 소개와 사용법을 설명한다.

이전에 Vim을 IDE처럼 만드는걸 포스팅 했었는데 Vim만으로는 부족했던 부분을 tmux가 채워줄 수 있을 것 같다. GUI 작업 환경을 별로 좋아하지 않아서 대부분의 작업을 터미널에서 하는 편인데 이걸 이제 알았다는게 아쉬울 정도로 좋다. 아직은 배우는 중이라 제대로 쓰고 있는 것 같지는 않지만 그럼에도 불구하고 굉장히 만족스럽게 사용하고 있다.

tmux는 Vim과 마찬가지로 커스텀이 매우 자유로워서 많은 플러그인들이 이미 만들어져 있고 설치하기도 쉽다. 상태바에 현재 배터리 용량이나 CPU 사용율을 표시할 수도 있고 위의 동영상을 보면 애플 뮤직과 연동해서 현재 실행되는 음악의 제목을 표시하기도 한다. 이렇게 플러그인을 찾아서, 혹은 만들어서 하나 둘 씩 붙여가는게 나름 재미있다.

사용법을 소개하기 전에 tmux를 사용하면 어떤 것을 할 수 있는지 알아보자. 먼저 tmux의 기본 기능은 화면 분할이다. 하나의 터미널 화면을 여러 개의 터미널로 쪼갤 수 있고 쪼개진 화면의 크기를 자유자재로 조절할 수 있어서 나만의 레이아웃을 만들 수도 있다. tmux를 사용하지 않았을 땐 여러 개의 터미널 탭을 켜놓고 이리저리 왔다갔다 했었지만 이젠 하나의 화면에서 모두 처리할 수 있다.

또 다른 장점은 터미널을 세션으로 관리할 수 있다는 것이다. 터미널 환경을 하나의 세션으로 분리시켜서 언제든 attach/detach 할 수 있다. 하나의 분리된 세션이기 때문에 detach 되어도 백그라운드에서 계속 실행된다. 그리고 세션은 모두에게 공유되기 때문에 2명이 1개의 세션에 attach 해서 페어프로그래밍 하기에도 아주 좋다. 혼자 사용한다면 각 프로젝트별로 세션을 나누어서 작업 환경을 분리하기도 좋다.

하지만 무엇보다도 터미널에서 간지가 난다는 것이 가장 큰 장점이다.

설치 방법

OS X의 Homebrew로 설치한다.

brew install tmux

설치가 완료되면 tmux -V를 실행시켜 보자. 제대로 출력되면 성공이지만 이런 에러가 뜰 수도 있다.

dyld: Library not loaded: /usr/local/lib/libevent-2.0.5.dylib
...
Trace/BPT trap: 5

이 문제는 아마도 설치되어 있는 libevent 라이브러리와 tmux가 필요로 하는 libevent의 버전이 달라서 생기는 문제인 것 같다. 해결 방법은 기존에 설치되어 있는 libevent를 제거하고 tmux를 설치하는 것이다. tmux를 설치할 때 올바른 버전의 libevent가 같이 설치되기 때문이다.

brew uninstall libevent
brew install tmux

용어 설명

본격적으로 사용법을 배우기 전에 몇 가지 용어를 알고 가야 한다.

Session

세션 실행

# 세션 생성(이름은 숫자로 정해짐)
$ tmux

or

# 세션 생성하며 이름 지정
$ tmux new -s session_name
$ tmux new-session -s session_name

세션 종료

# 세션 안의 마지막 팬에서 실행
$ exit

or

# 세션 바깥에서 특정 세션을 종료
$ tmux kill-session -t session_name

세션 attach

# 특정 세션으로 진입하기
$ tmux attach -t session_name

세션 detach

# 세션 빠져나오기
# 세션 안에서 실행
<prefix> + d

세션 목록 보기

$ tmux ls

Window

윈도우와 관련된 명령어 & 단축키는 모두 세션 안에서 실행해야 한다.

윈도우 생성

<prefix> + c

윈도우 이름 변경

<prefix> + ,

이전, 다음 윈도우 이동

# 다음 윈도우
<prefix> + n

# 이전 윈도우
<prefix> + p

모든 윈도우 리스트 보기

<prefix> + w

Pane

세로로 윈도우 분할

<prefix> + %

가로로 윈도우 분할

<prefix> + "

팬 이동

<prefix> + q + 숫자

or

<prefix> + q + 방향키

# 특정 팬을 전체화면으로 전환
# 한번 더 누르면 원상태 복구
<prefix> + z

마치며

이번 포스팅에서는 간단한 tmux 사용법을 알아봤다. 이제 세션, 윈도우, 팬을 자유롭게 생성하고 종료할 수 있을 것이다. 다음에는 tmux에 플러그인을 설치하고 설정파일을 커스텀하는 것을 배워보자.


Pagination