Text

Django의 ORM에서는 SQL문을 생성할때 String Concatenation을 어떻게 수행하고 있을까?

어제 포스팅 했던 Python에서 효율적인 String Concatenation 방법 글에 이어서 추가적으로 Django에서는 어떻게 String Concatenation을 하고 있는지 살펴보도록 하겠다.

django/db/models/sql/compiler.py 파일에서 SQL 문 생성에 관련된 코드들을 요약해서 추려보면 다음과 같다.

1. result = ['SELECT']
2. result.append(‘select 하고자 하는 column’)
3. 
result.append('FROM')
4. result.extend(from_)
5. result.append('WHERE %s % where)
6. 기타 조건들……
7. return ' '.join(result)

Concatenation 할 요소들을 담은 리스트를 생성한다음 마지막에 join을 이용해 쿼리문을 생성하는 방법을 사용하고 있다. (이전 포스팅에서 소개했던 Method 4에 해당한다.)

Text

Python에서 효율적인 String Concatenation 방법

Garbage Collection 이 있는 언어를 사용할때 실수하기 쉬운 부분이 String Concatenation인것 같다.

예를들어 SQL 쿼리를 다음과 같이 생성한다고 하자.

query = 'SELECT * FROM Article '
query += 'WHERE '
query += WHERE 조건들…
query += 'ORDER BY '
query += 블라블라블라…..

이렇게 코드를 작성하면 프로덕션 환경에서 심각한 성능저하 현상이 발생한다. 왜냐하면, 매 줄이 실행될 때마다 새 객체가 만들어지고 기존 객체는 GC의 대상이 되기 때문이다. 좀더 구체적으로 설명하면 다음과 같다.

1번째줄 : [ SELECT * FROM Article ] 이라는 문자열을 담은 객체 생성
2번째줄 : [ SELECT * FROM Article WHERE ] 라는 문자열을 담은 새 객체가 생성되어 query에 할당됨. 이전 객체는 쓸모 없어졌으므로 GC의 대상이됨.

따라서 이렇게 비효율적으로 String Concatenation을 수행할경우 쿼리를 10000번 생성하면 코드 작성자가 의도하지 않았던 수십만개의 쓸모없는 객체들이 생성되었다가 사라진다. Garbage Collection을 지원하는 모든 언어에 해당되는 문제인데, Java의 경우에는 JDK 5.0 이상에서는 String 클래스로 객체를 생성하여 Concatenation을 수행하면, 컴파일러가 자동으로 StringBuilder로 바꿔준다고 한다. (Java에서 StringBuffer, StringBuilder는 String Concatenation을 할때 새 객체를 생성하지 않고 기존 객체를 변경한다.) 하지만 컴파일러가 언제나 최적의 코드를 생성한다는 보장이 없으므로 String Concatenation을 할때는 신경써서 코드를 작성할 필요가 있다.

Java에서는 StringBuffer, StringBuilder를 사용하면 되는데, 과연 Python에서는 어떤 방법이 가장 효율적일까? 검색해 보니 < Efficient String Concatenation in Python > 이라는 좋은 글이 있어 요약정리를 해 보았다.

( 원문링크 : http://www.skymind.com/~ocrow/python_string/ )

이 글의 작성자는 Python에서 String Concatenation을 하는 방법은 크게 6가지가 있다고 소개하고 있다.

Method 1: Naive appending

def method1():
  out_str = ''
  for num in xrange(loop_count):
    out_str += `num`
  return out_str

이 방법은 내가 위에도 언급했듯이 별로 좋은 방법이 아니다. 쓸모없는 객체들이 생성되었다가 GC의 대상이 되는 일이 반복된다.

Method2는 사용하고 있는 UserString이라는 모듈이 최신 버전 문서에서 사용을 권장하지 않는다고 되어있어서 생략한다. (남아 있기는 한데 오직 backward compatibility를 위해서만 존재한다고 한다.) 그리고 6가지 방법중에 성능도 제일 나쁘다.

Method 3: Character arrrays

def method3():
  from array import array
  char_array = array('c') 
  for num in xrange(loop_count):
    char_array.fromstring(`num`)
  return char_array.tostring()

array를 이용해 Concatenation하는 방법이다. Method 1 보다는 성능이 좋지만 뒤에 나오는 방법들에 비해서는 성능이 떨어진다.

Method 4: Build a list of strings, then join it

def method4():
  str_list = []
  for num in xrange(loop_count):
    str_list.append(`num`)
  return ''.join(str_list)

이 방법은 일반적으로 추천되는 pythonic way라고 소개하고 있다. Concatenation할 요소들을 list에 담은다음 join으로 합쳐서 문자열을 생성하는 방법이다.

Method 5: Write to a pseudo file

def method5():
  from cStringIO import StringIO
  file_str = StringIO()
  for num in xrange(loop_count):
    file_str.write(`num`)

  return file_str.getvalue()

StringIO를  이용하는 방법이다. Method4 보다 조금 더 빠른 성능을 보여준다.

Method 6: List comprehensions

def method6():
  return ''.join([`num` for num in xrange(loop_count)])

Method 4와 동일한 아이디어 인데, list comprehension을 이용해 리스트를 생성해 더 빠른 성능을 얻었다. 작성자의 예제가 특수한 상황이어서 가능하고, 일반적인 경우에는 사용하지 못할 수도 있다.

성능 측정 결과는 다음과 같았다고 한다.

 

Python에서 String Concatenation을 할때는 Method 4나 Method 5를 사용하는게 가장 효율적으로 보인다. Method 6은 적용할 수 있는 상황에서 적용하면 유용해 보인다.

Text

핌피(PIMFY) 서비스 구성 스택

핌피(http://pimfy.com)는 내가 개발에 참여하고 있는 서비스이다. 내가 가장 익숙한 랭귀지인 Python과 Django 프레임워크를 이용해 개발했고, 이 글을 통해 구성 스택에 대해서 공유하고자 한다. 

개발중에는 Instagram, mozilla.com 등 많은 Django 어플리케이션 구현 사례를 참고했고, 아직 scale 이 필요한 단계가 아니므로 적용되지 않은 사항들이 많이 남아있다. 앞으로 서비스 규모가 성장하면 단계적으로 적용하면서 이 블로그에 정보를 공유하도록 하겠다.

핌피 서비스 구성 스택은 다음과 같다.

  • CentOS
  • Nginx
  • uWSGI
  • MySQL
  • Membase
  • Redis
  • Celery
  • Solr

1) Nginx
 Nginx는 Reverse Proxy(http://en.wikipedia.org/wiki/Reverse_proxy)로 사용하고 있다. Reverse Proxy를 매우 간단히 설명하자면, 요청에 따라 알맞은 서버로 분배시켜 주는 역할을 한다. Proxy와는 반대의 역할을 하기 때문에 Reverse Proxy라는 이름을 갖게 되었다. 핌피는 Nginx를 이용해 Static File 요청이 들어오면 바로 파일을 보내주고, 나머지 요청은 Application Container인 uWSGI로 보내주고 있다.

2) uWSGI (http://projects.unbit.it/uwsgi/)
 uWSGI는 WSGI(http://en.wikipedia.org/wiki/Web_Server_Gateway_Interface)를 정의하고 있는 PEP333, PEP3333 명세(http://www.python.org/dev/peps/pep-3333/)의 구현체이다. C로 구현되어 있어 매우 빠르고, 다른 구현체들에 비해 좋은 성능을 보여준다. 핌피의 실질적인 로직은 모두 uWSGI위에서 동작한다. Nginx에서 요청을 넘겨주면 uWSGI에서 결과를 만들어 돌려준다.

3) Membase, Johnny cache(http://packages.python.org/johnny-cache/)
  핌피는 Membase와 Johnny cache를 이용해 어플리케이션 전체에서 캐쉬를 적용하고 있다. 웹서비스는 보통 I/O Bound이고, 특히 Database가 병목이 되기 쉽다. Application Server에 비해서 Database는 상대적으로 scale이 힘들기 때문에 최대한 Database 요청을 줄이는 것이 중요하다. 
 Django는 view 단위 cache, 템플릿 캐쉬 등을 지원하지만 실제로 사용하기에는 매우 조악하다(모두 시간을 기준으로 invalidation 한다! 얼마나 어리석인 짓인가!). 결국에는 low level cache api 를 통하여 캐쉬 데이터가 쓸모가 없어지는 시점에 적절하게 invalidation 하는 코드를 작성해야 하는데, 이는 매우 지루하고 힘든 작업이다. 가장 빈번하게 사용되는 몇몇 Model에 적용하는게 아니고 전체 Model에 적용하려면 매우 긴 시간이 소요될 것이다.
 Johnny cache는 이 불편을 해소해 주는 훌륭한 Cache Framework이다. Johnny cache는 Middleware에서 Model들에 Monkey Patch를 적용해 Model이 생성, 수정, 삭제 될 때 자동으로 invalidation을 수행해 준다. Django에는 django-autocache, django-cache-machine 등 많은 cache framework가 있지만, Johnny cache가 가장 완성도가 높아 보인다. Johnny cache에 대해서는 다음 포스트를 통해 자세히 다루도록 하겠다.

4) Celery, Redis
 핌피는 Task Queue로 Celery를 사용하고 있다. 시간이 조금이라도 소요되는 모든 작업은 Celery로 보내, 빠른 응답속도를 보장하고 있다. Celery를 사용하기 위해서는 작업 분배를 해주는 Broker가 필요한데, 핌피는 Redis를 사용하고 있다(대표적인 Broker로는 RabbitMQ가 있다). 사용중인 RDB를 Broker로 사용하는 옵션도 있지만 성능을 위해 권장하지 않는다. 

5) Solr, Haystack(http://haystacksearch.org/), 오픈소스 한글 형태소 분석기(http://cafe.naver.com/korlucene/)
 핌피는 검색을 위해 Apache Solr, Haystack, 그리고 한글 형태소 분석기를 사용하고 있다. 오픈소스 한글 형태소 분석기의 경우에는 lucene용으로 제작된 것이지만, Solr용으로 빌드해서 사용 가능하다. 검색 아키텍쳐에는 크게 grep형, Suffix형, 역인덱스(Inverted Index)형이 있는데, Solr는 역인덱스형 검색엔진이다. 설정도 간편하고, 한글 형태소 분석기도 쉽게 적용할 수 있어 단시간 내에 높은 품질의 검색엔진을 구축할 수 있다. 핌피는 일정 시간마다 cron으로 검색 인덱스를 생성하고 있다. 검색엔진에 대해서는 다음 포스트에서 자세히 설명하도록 하겠다.