이번에 정리하려는 내용은 Django 내부의 Model Field 자체적으로 데이터를 효율적으로 압축하는 방법들 입니다. 이번 페이지에서 정리할 내용은 2가지로 다음과 같습니다.

  1. CharField 에 string 데이터를 zlib 알고리즘으로 압축저장
  2. IntegerField 에 float 데이터는 자릿수를 제한하여 저장 및 호출


Django with zlib

zlib 압축 알고리즘

zlib 알고리즘으로 문자열을 압축하고, 압축한 내용을 풀어내는 과정은 다음과 같습니다.

import zlib

def compress_data(data):
    return zlib.compress(data.encode('utf-8'))

def decompress_data(compressed_data):
    return zlib.decompress(compressed_data).decode('utf-8')


original_data = "This is a long string that will be compressed."
compressed_data   = compress_data(original_data)     # 데이터 압축
decompressed_data = decompress_data(compressed_data) # 압축해체
decompressed_data, type(compressed_data)
# ('This is a long string that will be compressed.', bytes)

압축을 하면 bytes 형식으로 저장을 하게 됩니다. DATABASE 특성상 해당 타입을 지원하지 않는 경우가 있는데, 이러한 경우에는 base64 로 문자열로 변환을 하면, 문자열 데이터로 저장 가능합니다. 대신 저장용량이 대략 33% 커지는 문제가 있습니다.

import zlib, base64

def compress_data(value):
    compressed_data = zlib.compress(value.encode('utf-8'))
    return base64.b64encode(compressed_data).decode('utf-8')

def decompress_data(value):
    decoded_data = base64.b64decode(value.encode('utf-8'))
    return zlib.decompress(decoded_data).decode('utf-8')


original_data = "This is a long string that will be compressed."
compressed_data   = compress_data(original_data)     # 데이터 압축
decompressed_data = decompress_data(compressed_data) # 압축해체
decompressed_data, type(compressed_data)
# ('This is a long string that will be compressed.', str)

Django Field API reference

Django 필드별 내부에 사용자 정의 함수를 추가할 수 있습니다. Django Models Field 는 데이터베이스 테이블 열을 정의하는 추상 클래스 입니다. Django 는 Field Method 를 활용하여 데이터베이스 테이블을 생성하고(db_type()), 파이썬 유형을 데이터베이스에 매핑하며(get_prep_value()), 그 반대로도 (from_db_value()) 작동하고 있습니다. 출처 : Django Field API reference

다음의 예제는 문자열로 압축을 하는 내용 입니다. Django 에서 models.TextField 필드를 상속하여 base64 와 zlib 를 활용하여 저장시 압축을 하고, 사용시 압축 해제를 하는 내용 입니다.

# Text Field 를 활용한 압축
class CompressedTextField(models.TextField):
    """데이터를 zlib으로 압축하여 저장하는 TextField"""
    
    def from_db_value(self, value, expression, connection):
        """데이터베이스에서 값을 가져올 때 자동으로 압축 해제"""
        if value is None:
            return value
        return self.decompress(value)

    def get_prep_value(self, value):
        """저장하기 전에 데이터를 압축"""
        if value is None:
            return value
        return self.compress(value)

    def compress(self, value):
        """zlib을 사용하여 문자열 압축 후 base64로 인코딩"""
        if isinstance(value, str):
            compressed_data = zlib.compress(value.encode('utf-8'))
            return base64.b64encode(compressed_data).decode('utf-8')  # Base64로 인코딩하여 저장
        return value

    def decompress(self, value):
        """zlib을 사용하여 base64로 저장된 데이터를 압축 해제"""
        try:
            decoded_data = base64.b64decode(value.encode('utf-8'))
            return zlib.decompress(decoded_data).decode('utf-8')
        except Exception:
            return value

Mysql, MariaDB 는 bytes 데이터를 저장할 수 있습니다. DATABASE 에서는 BLOB 형식으로 저장 되는데, 최대 용량이 64Kb로 제한이 되어 있습니다. 출처 : String Data Types - MariaDB

다음의 예제는 압축 데이터를 bytes 형식으로 저장하는 내용 입니다. Django 에서 models.BinaryField 필드를 상속하여 zlib 를 활용하여 저장시 압축을 하고, 활용시 압축을 해제를 하는 내용 입니다.

# Binary Field 를 활용한 압축
class CompressedBinaryField(models.BinaryField):

    def from_db_value(self, value, expression, connection):
        """데이터베이스에서 값을 가져올 때 : 자동으로 압축 해제"""
        if value is None:
            return value
        return self.decompress(value)

    def get_prep_value(self, value):
        """저장하기 전에 : 데이터를 압축"""
        if value is None:
            return value
        return self.compress(value)

    def compress(self, value):
        """zlib을 사용하여 문자열 압축 후 base64로 인코딩"""
        if isinstance(value, str):
            return zlib.compress(value.encode('utf-8'))
        return value

    def decompress(self, value):
        """zlib을 사용하여 base64로 저장된 데이터를 압축 해제"""
        try:
            return zlib.decompress(value).decode('utf-8')
        except Exception:
            return value

Round Up Float

실수 데이터를 정수 필드로 변환저장 및 활용하는 예시는 다음과 같습니다.

class FloatRoundUpField(models.IntegerField):

    round_tens = 1000 # 반올림 숫자자리

    def get_prep_value(self, value):
        r"""저장할 때, 실수를 정수로 변환"""
        if value is None:
            return value

        float_roundup_int = value * self.round_tens
        return int(float_roundup_int)


    def from_db_value(self, value, expression, connection):
        r"""DB에서 호출할 때, 자릿수 복귀"""
        if value is None:
            return value

        float_number = value / self.round_tens
        return float_number


남은과제

향후 PostgreSQL 을 활용하는 프로젝트에서는 보다 다양한 설정이 가능하다고 합니다. 이 내용은 향후에 알아보겠습니다.

방법 장점 단점
BinaryField + zlib 압축률이 좋고, 바이너리 데이터 저장 가능 직접 압축 및 해제 메서드를 호출해야 함
TextField + Custom Field 자동으로 압축 및 해제 저장 공간은 줄지만, 조회 성능이 다소 떨어질 수 있음
PostgreSQL BYTEA 필드 대용량 데이터 저장에 적합 PostgreSQL 전용


참고사이트