Post

디스코드 뮤직 봇 개발 (4)

디스코드 뮤직 봇 개발 (4)

오늘은 문제가 많았던 출석관련 기능을 수정했다. 기존에 사용하던 SQLite 데이터베이스의 한계와 불편함을 느꼈다. 그럼 이번 수정사항과 여러 시행착오를 아래에서 자세히 설명해보겠다.

SQLite에서 PostgreSQL로 마이그레이션 및 Docker Compose 배포기

기존에 사용하던 SQLite 데이터베이스를 PostgreSQL로 전환하고, Docker Compose를 통해 배포 환경을 구축한 과정을 기록하려고 한다.

기존 SQLite로 출석을 관리할때의 문제점은 다음과 같았다.

  • 확장성 부족: SQLite는 단일 파일 기반의 데이터베이스로, 동시 접속이 많아지면 성능이 급격히 저하된다. 특히, 출석체크 기능을 사용하는 사용자가 많아지면, 데이터베이스 연결이 병목 현상이 발생할 수 있다.

  • 비효율적인 출석체크 로직: 매번 데이터베이스 연결을 열고 닫는 방식으로 인해 성능이 저하되었다. 또한, 연속 출석 체크 로직이 복잡하고 비효율적이었다.

  • 한국 시간대 미적용: datetime.now()가 UTC 기준으로 동작하여, 한국 시간대(KST)를 반영하지 못했다. 이로 인해 출석체크 시간이 매우 불편했다.

또한 극한의 함수형 프로그래밍에 의해 파일 구조 문제도 있었다.

  • 단일 파일(main.py)에 모든 로직 집중되었던 점이 문제였다. 모든 코드가 main.py 파일에 작성되어 있어, 유지보수가 어려웠으며, 특히 데이터베이스 연결 로직과 비즈니스 로직이 혼재되어 있어 코드의 재사용성이 떨어졌다.

  • 모듈화 부재로 출석체크, 음성 채널, 음악 재생 등의 기능이 모두 한 파일에 작성되어 있어, 코드도 매우 길고 기능 추가나 수정이 어려웠다.

이에 문제점을 개선하고자 목표를 잡았다.

수정사항

1. 데이터베이스 전환: SQLite → PostgreSQL

  • PostgreSQL은 확장성이 뛰어나고, 동시 접속 처리에 적합하다.

  • 비동기 처리를 위한 asyncpg 라이브러리 사용했다.

2. 출석체크 로직 개선

  • 한국 시간대(KST) 적용: pytz 라이브러리를 사용하여 한국 시간대를 반영했다

  • 연속 출석 체크 로직 최적화: 매번 전체 데이터를 조회하지 않고, 사용자별 통계를 별도의 테이블(user_stats)로 관리하여 성능을 개선했다.

3. 파일 구조 모듈화

  • main.py: 봇의 메인 로직을 담당. 봇 초기화 및 명령어 등록 처리.

  • database.py: 데이터베이스 연결 및 쿼리 실행을 담당.

  • cogs/attendance.py: 출석체크, 출석 정보 조회, 월간 출석 현황 등의 명령어를 구현

  • cogs/voice.py: 음성 채널 관련 명령어 모듈 파일

4. Docker Compose를 통한 배포

  • PostgreSQL과 봇을 Docker 컨테이너로 구성했다.

  • docker-compose.yml 파일을 작성하여 PostgreSQL과 봇을 컨테이너로 구성했다.


**위 4가지 변경사항을 하나씩 자세히 알아보자! **

1. 데이터베이스 전환: SQLite → PostgreSQL

먼저 asyncpg 라이브러리를 사용하여 비동기 데이터베이스 연결을 구현했다. requirements.txt에 추가해주고 라이브러리를 설치해주었다.

asyncpg==0.28.0
pytz==2023.3

db 연결 설정은 database.py 파일에서 PostgreSQL 연결을 설정했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import asyncpg
import os
from dotenv import load_dotenv

load_dotenv()

class Database:
    def __init__(self):
        self.pool = None

    async def init_db(self):
        self.pool = await asyncpg.create_pool(
            host=os.getenv('DB_HOST'),
            database=os.getenv('DB_NAME'),
            user=os.getenv('DB_USER'),
            password=os.getenv('DB_PASSWORD')
        )

테이블 구조는 다음과 같다.

  • attendance: 출석 기록 저장.
  • user_stats: 사용자별 출석 통계 저장.

2. 출석체크 로직 개선

기존의 문제점이었던 한국 시간대 미 반영을 드디어 적용했다. ptyz를 통해 다음과 같이 한국의 시간으로 설정할 수 있다.

1
2
3
import pytz
kst = pytz.timezone('Asia/Seoul')
today = datetime.now(kst).strftime('%Y-%m-%d')

또한 연속 출첵 로직도 어제 출석했는지 확인해 연속 출석일을 계산한다. 그 후 최대 연속 출석일수를 갱신한다. 스트릭이 이어지면 1을 더하고 아니면 1로 초기화한다.

1
2
3
4
5
6
7
async def mark_attendance(self, user_id, date_str):
    # 어제 출석했는지 확인
    yesterday = (date_obj - timedelta(days=1))
    if last_date == yesterday:
        current_streak += 1
    else:
        current_streak = 1

3. 파일 구조 모듈화

기존 main.py에 모든 함수를 담아두었던 함수형 프로그래밍에서 기능별 모듈화를 진행했다.

main.py: 봇 초기화 및 명령어 등록.

database.py: 데이터베이스 연결 및 쿼리 실행.

cogs/attendance.py: 출석체크, 출석 정보 조회, 월간 출석 현황 등의 명령어 구현.

cogs/voice.py: 음성채널 입출력 등의 명령어 구현.

4. Docker Compose 설정

docker-compose.yml을 다음과 같이 구성해주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
version: "3"

services:
  db:
    image: postgres:14
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_DB=${DB_NAME}
    ports:
      - "5432:5432"
    restart: always
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${DB_USER} -d $${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5

  bot:
    build: .
    depends_on:
      - db
    restart: always
    env_file:
      - .env

volumes:
  postgres_data:

여기서 $$는 찾아보니 다음과 같은 설명을 찾았다.

Docker Compose에서 환경 변수 탈출 처리

  • Docker Compose 2.x 버전에서는 이중 달러 기호($$)를 사용해 변수가 Docker Compose에 의해 평가되지 않도록 할 수 있습니다:

그리고 환경변수도 .env 에 업데이트해주었다

1
2
3
4
5
DISCORD_TOKEN = 디스코드 토큰값
DB_HOST=db
DB_NAME=meloD_db
DB_USER=nowalex322
DB_PASSWORD=db 비밀번호

트러블 슈팅

1. PostgreSQL 연결 오류

  • 문제: FATAL: role “nowalex322” does not exist

  • 원인: Docker Compose에서 환경 변수가 제대로 확장되지 않음

  • 해결: healthcheck에서 $${DB_USER}와 같이 이중 달러 기호를 사용하여 변수 확장

2. DataGrip 연결 실패

  • 문제: DataGrip에서 PostgreSQL에 접속할 수 없음

  • 원인: Docker 컨테이너 내부에서 Unix 도메인 소켓을 사용하려고 시도

  • 해결: TCP/IP를 통해 연결 (-h localhost 옵션 추가).

    • docker-compose exec db psql -U nowalex322 -d meloD_db -h localhost
    • 접속이 성공하면 다음과 같은 프롬프트가 나타납니다:

    psql (14.0 (Debian 14.0-1.pgdg110+1)) Type “help” for help.

    meloD_db=#

3. Docker 권한 문제

  • 문제: PermissionError: [Errno 13] Permission denied

  • 원인: 현재 사용자가 docker 그룹에 속해 있지 않음

  • 해결: 사용자를 docker 그룹에 추가하고 재로그인

    1. 사용자를 docker 그룹에 추가 다음 명령어로 현재 사용자를 docker 그룹에 추가 sudo usermod -aG docker $USER 이 명령어는 현재 사용자($USER)를 docker 그룹에 추가

    2. 변경 사항 적용 그룹 변경 사항을 적용하려면 로그아웃 후 다시 로그인하거나, 다음 명령어로 세션을 재시작 newgrp docker

    3. 변경 사항 확인 다음 명령어로 사용자가 docker 그룹에 추가되었는지 확인합니다: groups 출력 결과에 docker가 포함되어 있어야 합니다. 예를 들어

1
nowalex322 adm dialout cdrom floppy audio dip video plugdev netdev lxd ubuntu google-sudoers docker

이후 postgreSQL 잘 됐으면 Datagrip도 연결해볼 수 있다.

그 전에 방화벽 규칙을 만들어야한다.

여기서 방화벽 규칙 만들기를 누르고

다음과 같이 설정해준다.

  • 트래픽 방향: “인그레스” (외부에서 들어오는 트래픽)
  • 작업: “허용”
  • 소스 IP 범위: 내 IP 값 검색해서 넣기(특정 IP에서만 접속 허용하려면)
  • 프로토콜 및 포트: TCP 포트 (PostgreSQL은 기본적으로 TCP 5432 포트 사용)
  • PostgreSQL의 기본 포트 : 5432

이제 Datagrip에서 새로운 Data Source에서 PostgreSQL 선택하고 연결하기위한 정보 입력해주면 (외부 IP 주소는 VM 인스턴스에서 확인가능)

잘 연결된 것을 볼 수 있다!

빨리 음악 재생까지 완성하고싶다. 그리고 봇을 디벨롭 하면 할수록 욕심이 더 커져 생각보다 스케일이 커지는 것 같다.

This post is licensed under CC BY 4.0 by the author.