인덱스는 데이터베이스 테이블에 대한 검색 성능을 높혀주는 도구이다. 이런 인덱스를 구현하는 알고리즘에는 여러가지가 있는데, 각 상황에 맞게 사용된다.

먼저 인덱스가 무엇인지 알아보고, 어떤 기준으로 상황에 맞는 알고리즘을 고르면 될지 알아보도록 하자

그리고, 이 글은 꽤나 길기때문에 원하는 정보가 있다면 우측의 인덱스를 잘 활용하길 바란다 👉

🤔 인덱스란?

인덱스는 메모리 영역에 존재하며, 지정된 컬럼을 기준으로 생성된 목차를 의미한다. 아래 그림을 예시로 들어보자.

위 인덱스는 지정된 컬럼(company_id)을 기준으로 정렬되어 있고, pointer가 테이블의 row를 가리키고 있다.

만약 인덱스를 거치지 않고 company_id가 18인 값을 찾으러면 테이블은 company_id 로 정렬되어 있지 않기 때문에 모든 row를 탐색해야 할 것이다 (full scan)

이런 인덱스를 정렬하는 알고리즘은 B-tree, Hash, 비트맵 등 여러가지가 있는데, 알고리즘 마다 쓰이는 상황이 다르다. 자세히 알아보자!

B-tree

B-tree 알고리즘은 인덱스에서 가장 일반적으로 사용되는 알고리즘이다.
B tree의 구조는 아래와 같은데,


이 구조의 특징은

  • 하나의 노드가 여러개의 데이터를 가질 수 있으며
  • 하위 노드의 최대 갯수는 상위 노드 데이터 수 + 1 로 결정된다는 점이 있다.

이런 구조 덕분에 검색 속도의 균일성(logN)을 보장할 수 있는데, 그 이유는 다음과 같다.


트리가 만약 왼쪽과 같이 비균형상태라고 쳐보자,
그럼 6이라는 값을 찾기 위해서는 몇개의 노드를 탐색해야 할까? 루트 노드부터 순서대로 6번 탐색해야 할 것이다.

하지만 트리 구조가 오른쪽과 같이 균형이 잡혀있다면 최대 3번의 탐색 안에 무엇이든 원하는 값을 가지게 될 수 있을 것이다.

이런 B-tree 인덱스는 보통 데이터의 값의 종류가 많고 동일한 데이터적은 컬럼에 사용된다.

주의할 점은 OR 오퍼레이터를 사용하는 쿼리문에 비효율적이다. 이를 해결하기 위해 보통 IN 오퍼레이터를 사용한 쿼리문으로 튜닝하곤 한다.

Hash

Hash 인덱스는 범용적으로 쓰이는 인덱스는 아니지만 동등 비교 검색(=)에서 빠른 속도를 보여주는 인덱스이다.


동작 방식은 아래와 같다

  • 지정된 컬럼의 검색하고자 하는 값을 hash function에 입력
  • hash function 출력값으로 해시값을 bucket에서 찾아 데이터 레코드 주소를 알아냄
  • 알아낸 데이터 레코드 주소로 원하는 데이터 레코드에 접근

해시맵을 사용하기 때문에 (hash collision이 없는 경우) 시간 복잡도가 1이라는 장점이 있다.

시간 복잡도가 1이라면 Hash 인덱스가 최고 아닌가? 라는 의문이 들 수도 있다. 물론 동등 비교 검색에서는 빠른 성능을 보여주지만, 범위 검색(>, between) 에서는 N의 시간 복잡도를 가지게 때문에 특수한 상황에서만 쓰이는 것이다.

이런 Hash 인덱스는 아래의 상황에서 쓰인다.

  • 데이터 값의 종류가 많음 (hash collision 방지)
  • InnoDB Adaptive Hash Index(자주 사용되는 값만 해싱하여 버킷에 저장)비트맵비트맵 인덱스는 기존 B-Tree가 가지고 있던 단점을 보완하기 위해 등장한 인덱스다.
    B-Tree는 아래와 같은 단점을 가지고 있었다.
  • 실제 칼럼 값을 인덱스에 저장하고 있다보니 저장 공간 낭비가 심하다
  • NOT 이나 NULL을 사용하거나 복잡한 OR 조건에서 성능이 떨어짐

이런 단점을 비트맵 인덱스는 아래와 같이 해결했다.

  • 칼럼 값을 인덱스에 저장하는 것이 아닌 컴퓨터의 가장 작은 단위인 비트로 값을 저장해 저장 공간 절약
  • bitwise(|, &, ^) 연산으로 복잡한 OR 연산이 빠름


비트맵 탐색을 예시를 들어보자면 아래 조건에 만족하는 row를 찾고 싶다고 가정해보자

  • 여성
  • 미혼
  • 18 - 34세

위 조건과 같을 때 비트맵 값이 101000인 컬럼을 찾으면 되는 것이다.

이런 비트맵 인덱스는 아래의 상황에서 쓰인다.

  • 데이터 값의 종류가 적음(성별, Enum)
  • 쿼리가 OR 연산자를 포함하는 여러개의 WHERE 조건을 가질 때

하지만 비트맵 인덱스는 레코드 하나만 변경되더라도 모든 레코드에 lock이 걸린다는 점 때문에 다른 인덱스 알고리즘에 비해 데이터 갱신비용이 크다는 단점이 있다.
또한, 하나의 비트맵 인덱스만으로는 별로 효과가 없고, 여러 비트맵 인덱스를 동시에 사용할 때 성능 향상에 도움을 준다.

full text search

클러스터링

Ref.

https://jojoldu.tistory.com/243
http://www.gurubee.net/lecture/1109

version: '3'
services:
  rabbitmq:
    image: rabbitmq:3-management-alpine
    container_name: rabbitmq-stream
    volumes:
      - ./.docker/rabbitmq/etc/:/etc/rabbitmq/
      - ./.docker/rabbitmq/data/:/var/lib/rabbitmq/
      - ./.docker/rabbitmq/logs/:/var/log/rabbitmq/
    ports:
      - "5672:5672"
      - "15672:15672"
    environment:
      RABBITMQ_ERLANG_COOKIE: "RabbitMQ-My-Cookies"
      RABBITMQ_DEFAULT_USER: "admin"
      RABBITMQ_DEFAULT_PASS: "rabbitpassword"

docker-compose.yml 파일은 위와 같다. 옵션별로 무엇을 의미하는지 알아보자


image: rabbitmq:3-management-alpine

rabbitmq 이미지중에 3-management-alpine 버전을 선택하겠다는 의미이다. 3-management-alpine 버전은 두가지 특징이 있다

  • management : 관리자 UI를 사용할 수 있게 해주는 management plugin이 설치되어 있는 이미지이다. 기본 관리자 username/passwdguest/guest 이다
  • alpine : 경량 리눅스 배포판 이미지, 사용하는 이유는 링크에 자세히 설명되어 있다.

volumes:
  - ./.docker/rabbitmq/etc/:/etc/rabbitmq/
  - ./.docker/rabbitmq/data/:/var/lib/rabbitmq/
  - ./.docker/rabbitmq/logs/:/var/log/rabbitmq/

volumes 는 도커 컨테이너는 실행 후 컨테이너를 삭제하면 존재하던 데이터가 모두 사라지게 되기 때문에 마운트를 해주는데, 이에 필요한 옵션이다.

만약 ./.docker/rabbitmq/etc/:/etc/rabbitmq/ 이라면 로컬의 ./.docker/rabbitmq/etc/디렉토리를 컨테이너의 /etc/rabbitmq/ 디렉토리와 마운트 하겠다는 의미이다.

한줄 한줄 마운트한 이유를 설명하자면

  • ./.docker/rabbitmq/etc/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf : RabbitMQ configuration 파일을 설정하기 위함
  • ./.docker/rabbitmq/data/:/var/lib/rabbitmq/ : RabbitMQ 데이터에 접근하기 위함
  • ./.docker/rabbitmq/logs/:/var/log/rabbitmq/ : RabbitMQ 로그에 접근하기 위함

environment:
  RABBITMQ_ERLANG_COOKIE: "RabbitMQ-My-Cookies"
  RABBITMQ_DEFAULT_USER: "admin"
  RABBITMQ_DEFAULT_PASS: "rabbitpassword"

컨테이너 내부의 환경 변수를 설정하는 옵션이다.

  • RABBITMQ_ERLANG_COOKIE : RabbitMQ 클러스터를 구성할 때 노드끼리 동일하게 맞춰줘야 하는 값
  • RABBITMQ_DEFAULT_USER : management UI 에서 로그인 username
  • RABBITMQ_DEFAULT_PASS : management UI 에서 로그인 password

ref.

https://zgadzaj.com/development/docker/docker-compose/containers/rabbitmq

6월 2일에 날라온걸 지금 쓰고있다...

친구가 지원하길래 재밌어 보여서 지원했던 우아한 테크캠프의 2차 코딩테스트 합격 이메일이 날라왔다.

 

문제는 API 명세서를 보고 간단한 로그인/로그아웃 구현하고 테이블을 만드는 예제였다.

우선 문제를 받자마자 언제나 그랬듯이 아키텍처에 대한 고민을 시작했다.

 

난생 처음 접해보는 프론트 코드를 짜야 해서 막연한 부분이 있긴 했지만 확장성DRY 두 가지만 생각하며 구조를 작성하고자 했다.

지금 이 문제를 받는다면 엔티티부터 정의하고 시작할 거 같다

 

두 가지중 확장성에 가장 큰 비중을 두고 구조를 작성했다.

 

users, posts, articles 와 같이 기능으로 폴더를 분리하기 보다 controller, models, views 처럼 역할로 폴더를 분리하는 것을 선호하는 스타일 (인터페이스를 만들어 다형성을 적용하기 편함) 이기에 역할로 폴더를 분리했다.

 

프로젝트 구조 고민에만 1시간은 잡아먹은듯 하다.

 

다음으로 과제에서 제공되는 API 명세서를 보고 fetch 혹은 axios로 요청해 검색 기능(간단한 Like 서치)이 있는 테이블을 구현해야 했는데, 익숙하지 않은 언어에 DRY를 지키려 노력하며 코딩하다 보니까 개발 속도가 나오지 않았다. 

 

프론트엔드 입장에서 개발하면서 느낌점이 몇가지 있는데, 아래와 같다.

  1. Pagination은 서버에서 무조건 제공해 줘야 하는구나 (너무 많은 정보를 bulk 조회 해버리면 로직 속도가 매우 느려질 것 같다.)
  2. 알고리즘을 의식의 흐름대로 작성할 수 있는 콜백함수가 마음에 들었다.
  3. 자바스크립트의 세미콜론은 왜 있는걸까
  4. 파이썬 하고싶다

 

내년에도 문제가 같다면 jQuery, fetch, JS의 각종 내장 함수 등은 숙지하고 테스트를 보는 편이 좋을 거 같다.


4시간동안 집중하먼서 풀었지만, 결국 필수 조건만 만족시키고 부가 조건은 만족시키지 못했다.

막상 끝나니 중간에 잠깐 쉬었던 시간이 아깝게 느껴졌다.

 

어찌보면 새로운 도전이었는데, 크게 느낀 것이 안 될거 같아도 일단 고민이라도 해보자는 것이다.

 

그래서 결국 2차 과제를 합격했는데 결과물은 마음에 들지 않았다.


최근에 클린 아키텍처에 관한 책을 읽으며 들은 생각인데, 내가 알고있던 아키텍처가 각 레이어 간의 종속성을 많이 위배한다는 사실을 알게 되었다. ex) 인터페이스 어댑터 레이어에서 로그인 로직을 처리함

또한 여러 에지케이스를 처리하기 위해 프로젝트 구조가 예기치 못한 많은 변경을 가졌는데 이에 대처하는 방법을 알고 싶어졌다.

import peewee

class MySQLModel(peewee.Model):
    @property
    @classmethod
    def unique_fields(cls) -> list:
        for field_name in cls._meta.fields.keys():
            field = getattr(cls, field_name)
            if field.unique == True or field.primary_key == True:
                yield field

peewee 모델의 unique한 field의 이름을 iterable한 객체로 반환하는 property 함수를 만들었다.

key in [field.name for field in MySQLModel.unique_fields]

그런데 위와 같이 사용 할 때 TypeError: 'property' object is not iterable 에러가 발생했다.

해결방법을 찾아보니 metaclass에서 property 함수를 만드는 방법, class decorator를 직접 만들어서 처리하는 방법 등이 있었다.

class decorator를 직접 만들어서 처리하는 방법은 아래와 같은데, 이게 마음에 들었다.


class classproperty(object):
    def __init__(self, function):
        self.function = function

    def __get__(self, owner_self, owner_cls):  # classproperty 객체에 접근할 때 inner_func 결과값을 반환하도록
        return self.function(owner_cls)


class MySQLModel(peewee.Model):
    @classproperty
    def unique_fields(cls) -> list:
        for field_name in cls._meta.fields.keys():
            field = getattr(cls, field_name)
            if field.unique == True or field.primary_key == True:
                yield field

docker-compose로 개발환경을 구축하기 위해 아래와 같이 docker-compose.dev.yml 파일을 작성했었다.

version: "2"

services:
  db:
    image: mysql:5.7.34
    container_name: dorandoran_db

    ports:
      - "7001:3306"
    environment:
      - MYSQL_DATABASE=dorandoran_dev_db
      - MYSQL_USER=devuser
      - MYSQL_PASSWORD=password
      - MYSQL_ROOT_PASSWORD=password
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      timeout: 20s
      retries: 10

  web:
    build:
      context: .
      dockerfile: ./compose/develop/Dockerfile-dev
    volumes:
      - ./:/app/
    command:
      - bash
      - -c
      - |
        python dorandoran/manage.py migrate
        python dorandoran/manage.py runserver 0.0.0.0:8000
    container_name: dorandoran_web
    env_file:
      - dev.env
    depends_on:
      db:
        condition: service_healthy
    restart: always
    ports:
      - "8000:8000"

분명 webdb에 의존하고 있음을 명시하고 healthcheck까지 붙여줬는데 왜 안되는지 이해가 안됐다.

문제는 dev.env 파일에 있었다.

DJANGO_DB_HOST=db
DJANGO_DB_PORT=7001  # 이곳
DJANGO_DB_NAME=dorandoran_dev_db
DJANGO_DB_USERNAME=root
DJANGO_DB_PASSWORD=password

web 컨테이너가 db 컨테이너에 접근 할 때의 포트를 외부 포트(7001)로 지정해 줬던 것이다.
내부 포트인 3306으로 바꾸니 제대로 동작하였다.

DJANGO_DB_HOST=db
DJANGO_DB_PORT=3306  # 잘 된다!
DJANGO_DB_NAME=dorandoran_dev_db
DJANGO_DB_USERNAME=root
DJANGO_DB_PASSWORD=password

depends_on

Express dependency between services. Service dependencies cause the following behaviors:

  • docker-compose upstarts services in dependency order. In the following example,dbandredisare started beforeweb.
  • docker-compose up SERVICEautomatically includesSERVICE’s dependencies. In the example below,docker-compose up webalso creates and startsdbandredis.
  • docker-compose stopstops services in dependency order. In the following example,webis stopped beforedbandredis.

docker-compose up 은 순서대로 서비스를 실행하는데, depends_on이 여기에 영향을 미친다.

version: "3.7"

services:
  web:
    build: .
    depends_on:
      - db

  db:
    image: mysql:8.0.22
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci

예를 들어 위와 같은 docker-compose 파일이 있다면, db 서비스가 먼저 실행되고, web 서비스가 그 다음으로 실행된다.

출처

Docker doDocker docscs

하위 디렉토리의 모든 테스트를 실행하고 싶을 때가 있다.

 

그럴때는

$ python -m unittest discover

를 입력하면 하위 디렉토리모든 테스트를 실행한다. (기본적으로 "test*.py" 포맷의 파일을 찾아 실행한다.)

 

$ python -m unittest discover 
F.
======================================================================
FAIL: test_failure (test2.HelloTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\workspace\study\TIL\test2.py", line 9, in test_failure
    self.assertEqual(0,1)
AssertionError: 0 != 1

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)
recursion = int(input())
print("어느 한 컴퓨터공학과 학생이 유명한 교수님을 찾아가 물었다.")

def q(recursion,cnt):
    print(cnt * "____" + "\"재귀함수가 뭔가요?\"")
    if recursion <= 0:
        print(cnt * "____" + "\"재귀함수는 자기 자신을 호출하는 함수라네\"")
    else:
        print(cnt * "____" + "\"잘 들어보게. 옛날옛날 한 산 꼭대기에 이세상 모든 지식을 통달한 선인이 있었어.")
        print(cnt * "____" + "마을 사람들은 모두 그 선인에게 수많은 질문을 했고, 모두 지혜롭게 대답해 주었지.")
        print(cnt * "____" +"그의 답은 대부분 옳았다고 하네. 그런데 어느 날, 그 선인에게 한 선비가 찾아와서 물었어.\"")
    
        q(recursion - 1, cnt + 1)
    print(cnt * "____" + "라고 답변하였지.")

q(recursion, 0)

 

+ Recent posts