나만의 배당주 사이트 만들기) 2-10. 개발 단계 - 배포를 위한 준비단계!(pip list 정리, 도커 관련, postgresql 접속하기, 트러블 슈팅 등)

2025. 2. 12. 13:00프로젝트

이 전 글들이 궁금하다면 ?

 

0. 사이트를 만드려는 이유

https://hsjoo126.tistory.com/80

 

1-1. 프로젝트 가능성 보기

https://hsjoo126.tistory.com/81

 

pandas 와 jupyter 이용해서 테스트해보기 

https://hsjoo126.tistory.com/82

 

1-2. 기획 단계 - 디자인, 와이어 프레임, ERD 등

https://hsjoo126.tistory.com/83

 

2. 개발 단계 - 계획짜기, 구현해보기

https://hsjoo126.tistory.com/84

 

2-1. 개발 단계 - 배당지불일, 시장별 티커리스트 구하기

https://hsjoo126.tistory.com/85

 

2-2. 개발 단계 - 코드 정리

https://hsjoo126.tistory.com/86

 

2-3. 개발 단계 - 사이트에 적용하기(장고)

https://hsjoo126.tistory.com/87

 

2-4. 개발 단계 - 사이트 로딩 속도 줄이기

https://hsjoo126.tistory.com/88

 

2-5. 개발 단계 - 주식별 동적인 페이지 만들기

https://hsjoo126.tistory.com/89

 

2-6. 개발 단계 - 티커리스트 db에 저장해서 불러오기, 해결하지 못한 트러블슈팅

https://hsjoo126.tistory.com/91

 

2-7. 429에러를 해결하기위한 노력, 로직 대폭 수정하기 엉엉... 그리고 해냄...

https://hsjoo126.tistory.com/92

 

2-8. cron.py , view.py 작성하기, 디테일 잡기

https://hsjoo126.tistory.com/93

 

2-9. 개발 단계 - 주가그래프그리기(matplotlib), Non-GUI 백엔드? Agg가 뭐야!, 상세페이지 수정, 페이지네이션 등

https://hsjoo126.tistory.com/94


오늘은 배포를 위한 준비 단계를 할 것이다.

할 일

- Pip list 정리하기

- Docker 관련 설정

- nginx 관련

 

다음은 내가 생각해본... 배포를 위한 준비 순서이다!

  • .env
  • Dockerfile
  • docker-compose.yml
    • web
    • redis
    • db
    • nginX
  • nginx.conf
  • 배포
    • EC2
    • 도메인 연결
    • ssl 인증(https)

 


1. Pip list 정리하기

 

프로젝트를 진행하면서 pip 리스트가 굉장히 많이 쌓였다.

이 중에서 쓸모 없는 Pip 지우고 필요한 pip 만 requirements.txt에 넣을 예정이다.

 

제일 단순 무식한 방법은

새폴더에 깃클론을 받아다가 필요한 것들만 하나하나 설치하는 거다 ㅋㅋㅋㅋㅋㅋㅋ

 

 

그리고 그렇게 만든 requirements.txt

asgiref==3.8.1
async-timeout==5.0.1
beautifulsoup4==4.13.1
certifi==2025.1.31
charset-normalizer==3.4.1
contourpy==1.3.1
cycler==0.12.1
dj-database-url==2.3.0
Django==5.1.5
django-crontab==0.7.1
django-environ==0.12.0
django-redis==5.4.0
finance-datareader==0.9.94
fonttools==4.55.8
frozendict==2.4.6
html5lib==1.1
idna==3.10
kiwisolver==1.4.8
lxml==5.3.0
matplotlib==3.10.0
multitasking==0.0.11
narwhals==1.25.0
numpy==2.2.2
packaging==24.2
pandas==2.2.3
peewee==3.17.8
pillow==11.1.0
platformdirs==4.3.6
plotly==6.0.0
psycopg2-binary==2.9.10
pyparsing==3.2.1
python-dateutil==2.9.0.post0
pytz==2025.1
redis==5.2.1
requests==2.32.3
requests-file==2.1.0
six==1.17.0
soupsieve==2.6
sqlparse==0.5.3
tqdm==4.67.1
typing_extensions==4.12.2
tzdata==2025.1
urllib3==2.3.0
webencodings==0.5.1
yfinance==0.2.52

그 다음은

2. Docker 관련 설정

# Django Dockerfile
FROM python:3.10.11

# 프로젝트 작업 디렉터리
WORKDIR /app

# 프로젝트 요구사항 파일 복사 및 설치
COPY requirements.txt /app/
RUN apt-get update && apt-get install -y python3-pip cron && \
    pip install --no-cache-dir -r requirements.txt



# 프로젝트 파일 복사
COPY . /app/


# 포트
EXPOSE 8000

# 장고 서버 실행
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
#docker-compose.yml
services:
  web:
    build: .
    command: >
      sh -c "python manage.py migrate && 
            python manage.py crontab add && 
            service cron start && 
            python manage.py runserver 0.0.0.0:8000"
    volumes:
      - .:/app
    ports:
      - "8000:8000"
    depends_on:
      - redis
      - db
  db:
    image: postgres:latest
    container_name: postgres-db
    env_file: 
      - ./dividend_stock/.env
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data

  redis:
    image: redis:latest
    ports:
      - "6379:6379"

volumes:
  postgres-data:

 

.env도 이렇게 바꿔주었다.

DJANGO_SECRET_KEY=
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1


DATABASE_URL=

POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=

REDIS_URL=redis://redis:6379/1


DJANGO_SETTINGS_MODULE=dividend_stock.settings

보안에 문제있을 수 있으니 문제가 될만한 건 빼고 올렸다!

 

그리고 .env에 맞춰서 settings.py 도 다음과 같이 바꿔주었다.

"""
Django settings for dividend_stock project.

Generated by 'django-admin startproject' using Django 5.1.4.

For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""

from pathlib import Path
import environ

# 환경변수 초기화
env = environ.Env()
environ.Env.read_env()

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('DJANGO_SECRET_KEY')

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = env('ALLOWED_HOSTS').split(',')


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'stocks',
    'django_crontab'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'dividend_stock.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'dividend_stock.wsgi.application'


# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases

DATABASES = {
    'default': env.db(),  # DATABASE_URL 환경 변수를 사용하여 자동 설정
}


# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/

STATIC_URL = 'static/'

# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

#django-crontab설정
CRONJOBS = [
    ('0 6 * * 1', 'stocks.cron.update_tickers', ">> /var/log/dividend_stock.log"),
    ('0 7 * * 1', 'stocks.cron.check_and_filter_dividends', ">> /var/log/dividend_stock.log"),
    ('0 9 * * *', 'stocks.cron.update_dividend_data', ">> /var/log/dividend_stock.log"),
    ('0 10 * * *', 'stocks.cron.update_last_dividend', ">> /var/log/dividend_stock.log"),
    ('0 * * * *', 'stocks.cron.update_redis_data', ">> /var/log/dividend_stock.log")
]

REDIS_URL = env("REDIS_URL")

#redis 설정
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': REDIS_URL
    }
}

 

2-1. Docker test

그리고 DB를 바꿔주었으니 업데이트도 할 겸 Postgesql에 새로운 주식 데이터를 넣어주었다.

먼저 도커 빌드랑 업 해서 오류 없는지 확인해주고!

docker-compose build
docker-compose up

 

새 터미널을 열어서 web 컨테이너에 접속하자.

docker-compose exec web bash

 

그 뒤로는 전 글에서 알려줬던 거랑 똑같다.

쉘에 들어간 다음, 함수 import 해주고

실행하면 된다

python manage.py shell
from 앱이름.cron import update_tickers
update_tickers()

이런 식으로 한 줄씩 입력하면 잘 실행된다!

 

cron.py 안에 있는 함수를 일일히 실행해서 postgresql안에 데이터를 새로 넣어주면 된다.

사실 함수 실행하면서 시행착오가 없었던 건 아닌데, 시행착오가 궁금한 사람들은? ⬇️⬇️⬇️⬇️

더보기

cron.py에는 총 5개의 함수가 있다.

1. 나스닥과 뉴욕증권 거래소에 있는 ticker 수집하는 함수
2. Ticker에 있는 것 중에서 배당관련 데이터가 있는 애들만 수집하는 함수
3. 배당관련 데이터 있는 주식을 가지고 원하는 데이터 수집하는 함수
4. update_dividend_data 실행시 last_dividend가 NULL로 나오는 문제가 생겨서  last_dividend만 따로 수집하는 함수

5. redis에 업데이트 하는 함수

 

일단 1번 함수는 진짜 너무 오래걸린다 ㅋㅋㅋㅋㅋㅋ

약 6천~7천개의 주식을 수집하는데 1시간 반 정도 걸린 거 같고..

2,3번도 다 오래걸린다. 

 

데이터들을 수집하면서 3번함수를 돌릴 때 오류가 많이 떴었는데,

3번 함수 코드는 다음과 같다.

def update_dividend_data():
    
    tickers = DividendTicker.objects.all()  # DividendTicker에서 티커 가져오기
    batch_size = 100  # 한 번에 처리할 티커 개수

    # 티커 배치별 처리
    for i in range(0, len(tickers), batch_size):
        batch = tickers[i:i + batch_size]  # 100개씩 배치 처리

        # 배치 내 티커 처리
        for ticker_obj in batch:
            ticker_symbol = ticker_obj.symbol
            try:
                ticker_data = yf.Ticker(ticker_symbol)

                # 종가 가져오기 (최근 1일 기준)
                data = ticker_data.history(period="1d")
                close = data['Close'].iloc[-1] if not data.empty else None  # 종가가 없으면 None

                # 배당금 정보
                dividends = ticker_data.dividends
                last_dividend = dividends.iloc[-1] if not dividends.empty else None

                # 배당일 처리 (calendar에서 'Dividend Date'를 추출)
                calendar = ticker_data.calendar
                dividend_date = None
                try:
                    # calendar가 dict인 경우에 'Dividend Date' 키 확인
                    if isinstance(calendar, dict) and 'Dividend Date' in calendar:
                        dividend_date = calendar['Dividend Date']
                    else:
                        print(f"No 'Dividend Date' for {ticker_symbol}, likely no dividends.")
                        dividend_date = "정보없음"
                except Exception as e:
                    print(f"Error parsing Dividend Date for {ticker_symbol}: {e}, Type of calendar: {type(calendar)}")
                # 배당률과 시가총액
                dividend_yield = ticker_data.info.get('dividendYield', '정보없음') * 100
                market_cap = ticker_data.info.get('marketCap','정보 없음')
                if isinstance(market_cap, (int, float)):
                    market_cap = "{:,}".format(market_cap)  # 숫자일 때 쉼표 추가
                else:
                    market_cap = market_cap  # '정보 없음' 그대로 사용

                # 저장할 데이터 생성
                dividend_yield_category = None
                if 4 <= dividend_yield <= 7:
                    dividend_yield_category = "4_to_7"
                elif dividend_yield > 7:
                    dividend_yield_category = "above_7"

                # CollectedDividendData에 데이터 저장
                if dividend_yield_category:
                    CollectedDividendData.objects.update_or_create(
                        ticker=ticker_symbol,
                        defaults={
                            "close": close,  # 종가 사용
                            "last_dividend": last_dividend,
                            "dividend_date": dividend_date,
                            "dividend_yield": round(dividend_yield, 2),
                            "market_cap": market_cap,
                            "dividend_yield_category": dividend_yield_category,
                        },
                    )
            except Exception as e:
                print(f"Error fetching data for {ticker_symbol}: {e}")

        # 각 배치마다 10초 대기 후, 처리된 티커 수 출력
        print(f"Processed {i + batch_size} tickers. Waiting for 10 seconds...")
        time.sleep(10)
    
    # Redis에 데이터 저장
    update_redis_data()
    print("배당 주식의 데이터 수집과 redis 저장이 완료되었습니다.")

 

 

다음은 뜬 오류메시지들과 왜 이런 메시지가 떴는지 주석으로 정리해보았다.

# 숫자가 아닌 문자열에 비교연산자를 써서 발생한 오류
Error fetching data for ADBE: '<=' not supported between instances of 'int' and 'str'

# 배당일이 없는 경우
No 'Dividend Date' for ADBE, likely no dividends.

# 종가 구할 때 뜨는 오류, yfinance에 없는 주식인 경우가 대부분이었음
$HTLF: possibly delisted; no price data found  (period=1d)

# info를 호출할 때 내부적으로 exchangeTimezoneName을 참조함, 
# 'exchangeTimezoneName' 필드가 없을 경우 오류 메시지가 출력될 수 있음
$HFWA: possibly delisted; no timezone found 

# 가격정보를 가져올 때 chart키를 사용해서 가져오는데, chart가 없을 경우
Could not get exchangeTimezoneName for ticker 'HFWA' reason: 'chart'

# ...ㅎ 데이터를 빠른시간안에 너무 많이 요청해서 생긴 에러 ..
Error fetching data for LINC: Too Many Requests. Rate limited. Try after a while.
401 Client Error: Unauthorized for url: https://query2.finance.yahoo.com/v10/finance/quoteSummary/SCI?modules=calendarEvents&corsDomain=finance.yahoo.com&formatted=false&symbol=SCI&crumb=Edge%3A+Too+Many+Requests

 

전에도 이 오류들이 떴었던 거 같은데,

오류를 정리했었는지 기억이 안나서 .. 그냥 이참에 다시 정리했다

 

첫 번째 오류는 배당률을 구하고, 배당률 기준으로 초고배당주 or 고중배당주로 나누는 과정에서 생긴 오류였고

마지막은 .... 원인을 알아도 해결할 수 없는 ... 오류이고

나머지는 yfinance에 데이터가 없어서 생긴 오류였다.

(실제 야후 파이낸스에 들어가 오류가 뜬 주식을 검색해보면 대부분의 데이터가 텅텅 비어있다.)

데이터가 거의 없는 모습을 볼 수 있음.

 

 

사실상 내가 해결할 수 있는 오류는 첫번째 오류이기 때문에 

가볍게 트러블 슈팅을 짚고 넘어가자

 

다음은 문제가 되었던 코드이다.

#배당률 구하는 함수
dividend_yield = ticker_data.info.get('dividendYield', '정보없음') * 100

# 배당률 4~7% 기준, 7%이상 기준으로 주식 나누기 
dividend_yield_category = None
    if 4 <= dividend_yield <= 7:
        dividend_yield_category = "4_to_7"
    elif dividend_yield > 7:
        dividend_yield_category = "above_7"

배당률 구하는 함수는

- 배당률이 없을 경우 정보없음으로 하고 있을 경우 그 숫자대로 *100을 하는 게 끝이다

배당률 카테고리 설정하는 함수는 

- 숫자와 비교해서 4_to_7 혹은 above_7 카테고리로 분류하는 함수인데

 

이 코드를 돌렸더니 다음과 같은 오류가 떴다.

Error fetching data for ADBE: '<=' not supported between instances of 'int' and 'str'

 

이 오류가 뜬 이유 ? 앞서 말했다시피 배당률이 없는 애들은 '정보없음'으로 설정해놨는데

카테고리 분류하는 과정에서 숫자랑 비교하니까 에러가 난 것이다...! (ㅋㅋ..멍청이슈..ㅠ)

 

해결방법은 그냥 간단하게, 숫자만 들어올 수 있도록 바꿔주었다.

# 배당률과 시가총액
dividend_yield = ticker_data.info.get('dividendYield', '정보없음')
if isinstance(dividend_yield, (int, float)):  # 숫자인 경우에만 처리
    dividend_yield = dividend_yield * 100
    
# 배당률 4~7% 기준, 7%이상 기준으로 주식 나누기 
dividend_yield_category = None
if isinstance(dividend_yield, (int, float)):
    if 4 <= dividend_yield <= 7:
        dividend_yield_category = "4_to_7"
    elif dividend_yield > 7:
        dividend_yield_category = "above_7"

 

 

안 궁금한 사람들은 ! 데이터가 postgresql에 잘 들어갔는지 한 번 확인해보자

 

 

docker 안에 있는 postgresql 접속하기

1. docker에 postgresql 컨테이너 접속하기

docker exec -it postgres-db bash

 

2. 유저 이름으로 접속

psql -U 설정해놓은 유저이름

 

3. 테이블 조회하기

#전체 테이블 조회
\dt

#개별 테이블 조회
SELECT * FROM 테이블이름

 

 

전체 주식들 중에 배당관련 데이터가 있는 주식만 수집하는 함수를 돌린 후,

데이터를 조회해봤다.

SELECT * FROM stocks_dividendticker;

사진처럼 잘 나오는 것을 확인할 수 있다.

 

터미널에서 db를 조회하다가 나가고 싶으면 다음 명령어를 쓰면 된다.

\q

 

matplotilb 폰트 변경하기

원래 폰트는 AppleGothic을 썼었다.

맥북에 있는 폰트여서 쓰기가 쉬웠기 때문이다.

 

근데 웬걸? ㅋㅋㅋㅋ 도커 설정 후 실행하니까 글자가 네모네모로 변해버렸다.

도커에 폰트 설정을 하고 지나가야한다는 걸 까먹고 있었다 .... ㅋㅋㅋㅋㅋㅋㅋ

그래서 나눔고딕으로 폰트를 변경해주었다.

#Dockerfile
# 프로젝트 요구사항 파일 복사 및 설치
COPY requirements.txt /app/
RUN apt-get update && apt-get install -y python3-pip cron fonts-nanum && \
    pip install --no-cache-dir -r requirements.txt
#views.py
#폰트 설정
matplotlib.rc('font', family='NanumGothic')

 

도커가 처음에 이거저거 설치할 때 폰트도 같이 설치할 수 있도록 코드를 추가하고

detail view에서 matplotlib 폰트를 나눔고딕으로 설정해주었다.

 

결과는? 사진처럼 네모네모가 사라지고 글씨가 정상적으로 나오는 것을 볼 수 있다.


 

 

어우 ... 원래는 nginx까지 하려고 했으나 길이 길어지는 거 같아 

다음글에서 해보자!