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에 해당한다.)

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은 적용할 수 있는 상황에서 적용하면 유용해 보인다.