728x90

이렇게 글을 쓰게 된 이유는 사실 django에서 소셜로그인용 라이브러리를 사용하면 쉽게 구현이 가능합니다.

but, 아무래도 매번 이렇게 사용하다보니 내부 동작이 어떻게 이뤄지고 있는지 확인이 안되었기 때문에 이번에 middleware부터 authentication, jwt 발급까지 최대한 라이브러리 사용없이 글을 작성해보려합니다.

 

카카오 소셜로그인의 경우, 위와 같이 크게
1. 인가 코드 발급
2. 토큰(kakao)
3. 사용자 로그인 처리(server의 주된 로직)

과정을 거쳐 진행됩니다.

 

이번에 이 글에서 다룰 방식은 기본적으로 1,2 번의 경우, 프론트에서 모두 발급한 이후 토큰(kakao)을 서버에 전달하여 인증 처리하는것을 다룰 것입니다. 간단하게 2번단계의 결과물인 토큰(kakao)을 발급받는 방식은 아래의 방식을 따라 가면 됩니다.

https://kauth.kakao.com/oauth/authorize?client_id={kakao 서비스 restapi key}&redirect_uri=http://127.0.0.1:8000&response_type=code
위 url을 직접 접속하여 kakao id, pass를 입력 후 로그인합니다.

http://127.0.0.1:8000/?code={code}
당연하게도, 페이지는 404를 띄어 주겠지만 상단 주소창에 있는 code 값을 복사하여 줍니다.
이것이 바로 인가코드 입니다. 이 인가코드를 통해 kakao 인증용 토큰을 발급 받을 수 있습니다.

headers = {"Content-type": "application/x-www-form-urlencoded;charset=utf-8"}
data = {
    "grant_type": "authorization_code",
    "client_id": {kakao 서비스 restapi key},
    "redirect_uri":"http://127.0.0.1:8000",
    "code": {code},
}

response = requests.post("https://kauth.kakao.com" + f"/oauth/token", data=data, headers=headers)

print(response.json())

 

이후 response의 access_token값을 서버로 보내준다가 전제입니다.

이제 이 access_token을 통해 서버에서의 전반적인 처리 과정은

1. 클라이언트로 부터 kakao access_token을 전달 받아 이 토큰을 통해 kakao의 내서비스에 회원가입 or 로그인 시킨다.

    - 인증된 사용자라면 social_id값을 저장 한다.

    - 내 서비스 인증을 위한 jwt 토큰을 생성 및 발급해준다.

2. auth api외의 다른 api에서 접근시, 헤더의 jwt 토큰을 확인할 수 있도록 authentication 및 middleware를 설정해준다.

입니다.

 

1. 클라이언트로 부터 kakao access_token을 전달 받아 이 토큰을 통해 kakao의 내서비스에 회원가입 or 로그인 시킨다.

토큰을 받는 형식은 아래와 같습니다.

POST /auth/kakao
{"access_token": "~~"}

 

- models.py

from django.contrib.auth.base_user import AbstractBaseUser

class User(AbstractBaseUser):
    nickname = models.CharField(max_length=255)
    social_id = models.CharField(max_length=255)

- views.py

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from datetime import datetime
from datetime import timedelta
from django.conf import settings
import jwt


def generate_access_jwt(user_id):
    iat = datetime.now()
    expired_date = iat + timedelta(weeks=2)

    payload = {
        "user_id": user_id,
        "expired": expired_date.strftime("%Y-%m-%d %H:%M:%S"),
        "iat": iat.timestamp(),
    }

    return jwt.encode(payload, settings.SECRET_KEY, 'HS256')


@api_view(['POST'])
def kakao(self, request):
    data = request.data.copy()
    access_token = data.get('access_token')

    headers = {"Authorization": f"Bearer ${access_token}",
               "Content-type": "application/x-www-form-urlencoded;charset=utf-8"}
    response = requests.get("https://kapi.kakao.com/v2/user/me", headers=headers)
	
    if response.status_code == 200:
        kakao_data = response.json()
        kakao_id = kakao_data['id']
        nickname = kakao_data['kakao_account']['profile']['nickname']
        kakao_info = {"id": kakao_id, "nickname": nickname}
        
        try:
            user = User.objects.get(social_id=kakao_info["id"])
            status_code = status.HTTP_201_CREATED
        except User.DoesNotExist:
            user = User.objects.create(
            	nickname=kakao_info["nickname"],
            	social_id=kakao_info["id"],
        	)
            status_code = status.HTTP_200_OK
    	return Response({'message': '인증에 성공하였습니다.', 'access_token': generate_access_jwt(user.id)}, status=status_code)
        
    else:
        return Response({'message': '유효하지 않은 access_token입니다.'}, status=status.HTTP_400_BAD_REQUEST)

위 코드의 경우, 

1. https://kapi.kakao.com/v2/user/me api를 통해 전달받은 access_token을 body에 담아 request를 보냅니다.

2. 토큰이 유효했다면 아래와 같이 반환이 됩니다.(설정을 nickname만 받도록 해서 아래와 같이 반환되지만 추가적으로 정보 요청을 할경우, 유저의 정보가 더 추가되어 반환 됩니다.)

{
'id': 2819922431, 
'connected_at': '2023-06-04T15:43:11Z', 
'properties': {'nickname': '이형준'}, 
'kakao_account': {
'profile_nickname_needs_agreement': False, 
'profile': {'nickname': '이형준'}
}
}

3. 반환값의 id의 경우, 유일한 값이므로 이값이 만약 서비스의 social_id값과 일치하는 데이터가 존재한다면 "로그인" 아니라면, "회원가입"이 되게 됩니다.

4. 모든 인증 절차가 완료 되었다면 서비스내에서 jwt 토큰을 발급하여 반환합니다.

5. 액세스 토큰을 통한 kakao 인증 및 jwt 토큰 발급이 모두 마무리 됩니다.

이 단계를 통해 기본적으로 로그인, 회원가입이 완료 되었다면, 이젠 다른 api에서 정상적으로 유저를 식별 할 수 있는 방식을 준비해야합니다.

 

2.auth api외의 다른 api에서 접근시, 헤더의 jwt 토큰을 확인할 수 있도록 authentication 및 middleware를 설정해준다.

기본적으로 이 단계를 진행하기 전에 앞서 middleware와 authentication이 어떤것인지 간단히 알아봐야합니다.

미들웨어란 http 혹은 요청에 대한 응답에 있어 공통된 전후 처리 작업을 담당하는 역할을 합니다.

authentication의 경우, 쉽게 말해 등록한 인증 방법에 맞춰 유저를 식별해내는 방법이며, 더 쉽게 말해 기본적으로 django에서는 request.user를 통해 유저를 식별해낼수 있는 방법입니다.

자세한 내용은 아래의 내용을 참고해주시면 됩니다.

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

- views.py 추가

def decode_jwt(token):
    try:
        return jwt.decode(token, settings.SECRET_KEY, 'HS256')
    except:
        return None


def check_jwt_expired_date(now_date, expired_date):
    now_date = datetime.strptime(now_date, "%Y-%m-%d %H:%M:%S")
    expired_date = datetime.strptime(expired_date, "%Y-%m-%d %H:%M:%S")

    return True if expired_date <= now_date else False

 

- middleware.py

from datetime import datetime

from jwt import ExpiredSignatureError
from rest_framework.exceptions import PermissionDenied

from .views import decode_jwt, check_jwt_expired_date


class JsonWebTokenMiddleWare(object):

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        try:
            if (
                request.path != "/auth/kakao"
                and "admin" not in request.path
                and "swagger" not in request.path
            ):
                access_token = request.headers.get("Authorization", None)
                if not access_token:
                    raise PermissionDenied()

                auth_type, token = access_token.split(' ')
                if auth_type == "Bearer":
                    payload = decode_jwt(token)
                    if not payload:
                        raise PermissionDenied()

                    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                    token_expired = payload.get('expired')

                    if check_jwt_expired_date(now, token_expired):
                        raise ExpiredSignatureError()

                    user_id = payload.get("user_id", None)
                    if not user_id:
                        raise PermissionDenied()

                else:
                    raise PermissionDenied()

            response = self.get_response(request)
            return response

        except (PermissionDenied, User.DoesNotExist):
            return JsonResponse(
                {"error": "Authorization Error"}, status=status.HTTP_401_UNAUTHORIZED
            )

        except ExpiredSignatureError:
            return JsonResponse(
                {"error": "Expired token. Please log in again."},
                status=status.HTTP_403_FORBIDDEN,
            )

1. 기본적으로 인증을 진행해야하는 /auth/kakao와 swagger, admin 페이지의 경우, token이 없어도 접속이 가능해야함을 전제로 하여 middleware에서 바로 통과하도록 설정하였습니다.

2. header의 Autorization에 담겨있는 token을 가져와 유효성을 증명하고 문제없다면 통과 아니라면 middleware단에서 오류 반환 합니다.

 

- authentication.py

from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed

from .views import decode_jwt
from .models import User


class JsonWebTokenAuthentication(BaseAuthentication):

    def authenticate(self, request):
        access_token = request.headers.get("Authorization", None)
        if not access_token:
            return None

        auth_type, token = access_token.split(' ')
        payload = decode_jwt(token)

        user_id = payload.get("user_id", None)
        try:
            user = User.objects.get(id=user_id)
            return user, None
        except User.DoesNotExist:
            raise AuthenticationFailed({"message": "INVALID_TOKEN"})

1. 기본적으로 이미 middleware를 통해 token이 유효하다는것이 증명 되었으므로 authentication에서는 다른 유효성 검증 절차를 진행하지 않습니다.

2. access_token에 해당하는 user값을 반환해줍니다.

 

위의 과정을 거쳐 인증이 진행되며 작성한 코드의 경우, settings.py에서 등록해줘야합니다.

- settings.py

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        '<app_name>.<file_name>.<class_name>',
    ]
}

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',
    '<app_name>.<file_name>.<class_name>',
]

 

모든 인증 관련 로직 준비가 완료되었습니다.

추가로 생각해봐야할점은

1. 클라이언트와 서버의 인증에 있어 역할의 분리를 클라이언트 = access_token까지 발급으로 나눴는데 과연이게 정답일지? 아무래도 키값을 클라이언트에서 가지고 있다보니 탈취의 위험도가 있는데 이것이 과연 괜찮은 방법일지는 고려를 해봐야합니다.

2. middleware단에서 로직을 처리할때 물론 개발에 있어 편의성을 굉장히 늘어나지만, 일부 로직에서는 의미 없는 리소스가 소모 된다는 단점 또한 존재합니다. 이것이 middleware에서 처리하기 적합한 과정일지는 요구사항에 맞춰 결정해야합니다.

 

이글의 경우, 글 작성의 편의성을 위해 대부분의 코드가 분리 되어있지않아 있으며 최소한의 예외처리가 진행되었지만 전체적인 흐름 파악의 의미에서 참고를 한후 개발을 진행하면 좋을것 같습니다.

감사합니다 :)

728x90
728x90

이번에 우연찮게 동아리에서 오랜만에 django로 서버개발을 맡게 되며 초기 세팅을 하고 있는데 오랜만에 하게되다보니 길을 굉장히 헤멧다ㅎ... 그래서 리마인드겸 아래의 작업들에 대해 포스팅할 생각이다.

1. 환경 분리

2. django rds s3 연동

3. 소셜로그인(kakao) 연동, jwt 토큰 발급 및 middleware, authentication 세팅(라이브러리 사용x)

- django settings 파일 분리

이 settings 파일을 왜분리해야 하나?라는 생각이 들수 있다.

Django의 경우, settings.py에서 기본적인 설정들을 명시해두기 때문에 테스트와 실 운영 설정은 다르게 가져가야한다. 기본적으로 분리해야하는 것같은경우 아래와 같다. 

1. DEBUG

2. SECRET_KEY

3. DATABASE

4. STATIC FILE, MEDIA FILE --> 이것들은 선택사항일수도 있지만 필자의 경우 운영서버의경우, S3연동을 주로합니다!

 

settings파일은 크게 공통사항은 settings/__init__.py에 명시하고, deploy와 develop으로 나눠진행할예정입니다.

django-admin startproject config .

위의 명령어를 통해 config 폴더가 생성되면 내부에 settings, wsgi 폴더를 생성해줍니다.

기본적으로 settings 내용은 아래와 같습니다.

__init__.py (공통적으로 사용할 설정)

"""
Django settings for config project.

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

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

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

from pathlib import Path
import environ

import os


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

# django-environ 라이브러리를 통해 환경변수 세팅을 진행했는데 다른방식을 사용해도 무방합니다!
env = environ.Env(DEBUG=(bool, True))
environ.Env.read_env(
    env_file=os.path.join(BASE_DIR, '.env')
)

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

DJANGO_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

PROJECT_APPS = [
]

THIRD_PARTY_APPS = [
    'rest_framework',
    'drf_yasg',
]

INSTALLED_APPS = DJANGO_APPS + PROJECT_APPS + THIRD_PARTY_APPS

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 = 'config.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',
            ],
        },
    },
]


# Password validation
# https://docs.djangoproject.com/en/4.2/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/4.2/topics/i18n/

LANGUAGE_CODE = 'ko-kr'

TIME_ZONE = 'Asia/Seoul'

USE_I18N = True

USE_TZ = True

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

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

deploy.py(실 운영 서버에서 사용할 settings) - 추후 S3와 RDS를 사용한다는 가정하에 세팅한겁니다.

from config.settings import *

DEBUG = False

SECRET_KEY = env('SECRET_KEY')

ALLOWED_HOSTS = ['*']

INSTALLED_APPS += [
    'storages',
]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': env('DATABASE_NAME'),
        'HOST': env('DATABASE_HOST'),
        'USER': env('DATABASE_USER'),
        'PASSWORD': env('DATABASE_PASSWORD'),
        'PORT': env('DATABASE_PORT'),
    }
}

WSGI_APPLICATION = 'config.wsgi.deploy.application'

AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY')
AWS_REGION = 'ap-northeast-2'

AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME')
AWS_S3_CUSTOM_DOMAIN = '%s.s3.%s.amazonaws.com' % (AWS_STORAGE_BUCKET_NAME, AWS_REGION)
AWS_S3_OBJECT_PARAMETERS = {
    'CacheControl': 'max-age=86400',
}

STATIC_URL = "https://%s/" % AWS_S3_CUSTOM_DOMAIN
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'

develop.py(로컬환경에서 사용할 settings파일)

from config.settings import *

DEBUG = True

SECRET_KEY = 'django-insecure-sb!sadasdasdasdasdasd'

ALLOWED_HOSTS = []

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

WSGI_APPLICATION = 'config.wsgi.develop.application'

STATIC_URL = 'static/'

wsgi/deploy.py

import os
from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.deploy')

application = get_wsgi_application()

wsgi/develop.py

import os
from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.develop')

application = get_wsgi_application()

이후 develop와 deploy환경 파일을 사용환경에 따라 아래와 같이 분리하여 실행시켜주면됩니다.

# 테스트환경
python3 manage.py runserver --settings=config.settings.develop
# 운영환경
python3 manage.py runserver --settings=config.settings.deploy

그리고 환경변수 import시 가끔 실수를 하는 부분이 특정 settings파일을 명시해서 가져오는경우가 있는데 이런실수를 저지르면 안됩니다!

# 이경우, 환경 분리를했더라도 deploy가아닌 develop의 값을 가져옵니다.
from config.settings.develop import SECRET_KEY
value = SECRET_KEY

# 이경우, 실행환환경 세팅을 불러오니 이방법으로 불러오길 추천드립니다.
from django.conf import settings
value = settings.SECRET_KEY

 

728x90
728x90

요즘 많은 쿼리 성능과 같은 많은 이점 때문에 데이터 분석시 google BigQuery로 데이터를 이전하는 경우가 많다.

이때 테이블 하나하나 생성후 데이터를 옮기는 작업 자체는 어렵지 않지만 생각보다 고된 작업이 이어진다.

물론 제가 겪은 일입니다ㅎㅎ... 데이터를 안전하고 쉽게 옮기기 위한 tip을 공유합니다.

 

과정은 아래와 같습니다.

1. 옮기자 하는 대상인 rdbms 테이블 정보를 모두 가져옵니다.

2. 원하는 target을 선택합니다.

3. bigquery api client를 연결한뒤 이전을 시작합니다.

 

1단계

from sqlalchemy import create_engine
# sqlalchemy의 create_engine 메서드를 사용하여 db를 연결시킵니다.
RDS_ENGINE = create_engine('database_engine://ID:PASSWORD@host:port/database명')

# 모든 테이블 목록 조회
query = '''
SHOW TABLES;
'''
tables = pd.read_sql(query, con=RDS_ENGINE)

# 본인이 옮기고자 하는 테이블의 index를 제외한 목록을 모두 drop 합니다.
# 이건 방법이 여러가지라 target_tables에 target들만 남기면됩니다.
target_tables = tables.drop(index)

 

2단계

import google
# 이작업을 하기 위해서는 google_account_key가 필요합니다.
# 작업 환경의 환경변수에서 path를 설정해줄수 있습니다.
# GOOGLE_APPLICATION_CREDENTIALS=/Users/~~/google_secret.json
# 위와 같이 GOOGLE_APPLICATION_CREDENTIALS에 본인이 원하는 path를 설정해 키를 위치해줍니다.

credentials, project = google.auth.default(
    scopes=["https://www.googleapis.com/auth/bigquery"]
)
client = bigquery.Client(project=project, credentials=credentials)

 

3단계

for i in target_tables.index:
    target = target_tables[i]
    schema = f'''
    SELECT *
    FROM {target}
    '''
    try:
        for chunk in pd.read_sql(schema, con=engine, chunksize=50000):
            print(target, len(chunk))
            df = pd.DataFrame(chunk)
            df.to_gbq(f'dataset이름.{target_ph}', project_id=project, credentials=credentials, if_exists='append', chunksize=50000)
    except Exception as e:
        print(target)
        print(e)
        continue

한숨자고 일어나면 됩니다. 여기서 알아야하는것은 주의해야할것은 if_exists인데 ['replace', 'fail', 'append']가 있습니다.

셋다 모두 테이블이 없다면 생성후 문제가 없지만 

1. append: 기존의 테이블의 정보를 변경하지 않고 dataframe을 테이블에 append 즉 추가만 진행합니다.

2. replace: 기존에 테이블이 있다면 dataframe으로 모두 대체해버립니다.

3. fail: 기존에 테이블이 있다면 실패로 처리됩니다.

위와 같이 동작하기 때문에 반드시 주의해야합니다.

 

물론 모두 완벽하게 테이블 생성뒤 이관된다면 좋겠지만 특수한 사항에 에러가 발생하므로 주의해야합니다.

403 Quota exceeded: Your table exceeded quota for imports or query appends per table.

이에러 주의해야합니다...

 

insert만을 고려한다면 bigquery아주 유용하겠지만 중간중간 데이터의 정보를 변경해줘야 하며 increment_id와 같은 정보가 순차적인 정보가 없다면 테이블이라면 bigquery의 도입을 안하는것을 고려해야합니다.

 

read_sql, to_gbq 메서드를 사용할때 chunksize를 사용한이유는 아래 글을 참조해주세요... 데이터가 많을때는 왠만하면 꼭사용해주셔야 합니다.

 

Loading SQL data into Pandas without running out of memory

Pandas can load data from a SQL query, but the result may use too much memory. Learn how to process data in batches, and reduce memory usage even further.

pythonspeed.com

 

728x90

'기술 > database' 카테고리의 다른 글

postgres table join을 이용한 data update  (0) 2023.02.06
데이터베이스 실행(MYSQL)  (0) 2020.11.16
데이터베이스 정의_2(MYSQL)  (0) 2020.11.16
데이터베이스의 정의_1(MYSQL)  (0) 2020.11.16
728x90

pandas dataframe을 이용하여 db 업데이트 작업을 하다 문제가 생겼다.

id primaykey
tracking_number int
created_at date

위와 같은 fulfillment table 에서 특정 id 값들의 created_at값을 일괄적으로 변경한다 했을때 targets에 해당하는 id 값들을 transaction 한번에 모두 처리할수 있다.

engine = LOCAL_DB
targets = (1,7,15,3000,4000,...13000)

update_query = f'''
UPDATE fulfillment
SET
created = now()
WHERE
id in {targets}
'''

with engine.connect() as conn:
    conn.execute(update_query)

하지만 id값마다 다른 tracking_number를 넣는다고 할때 루프를 태우는 방법외엔 따로 없었다.

만약 insert라면 dataframe 자체를 to_sql메서드를 사용하여 일괄적으로 insert하기 때문에 문제가 없다.

insert_dataframe # fulfillment테이블과 동일한 포맷의 target dataframe이 있다고 가정한다.
insert_dataframe.to_sql('fulfillment', con=ENGINE_LOCAL, if_exists='append', index=False, method='multi', chunksize=50000)

여기서 문제가 발생하였다. to_sql 메서드를 사용하여 특정 키값에 해당하는 필드를 업데이트 하고 중복된다면 생성하지 않고 넘어가야했는데 to_sql메서드에서는 이러한 기능을 제공하지 않는걸로 확인이 됐다(물론 제가 못찾았을수도...).

물론 for루프를 태워서 해결은 가능하다.

insert_dataframe
with engine.connect() as conn:
    for i in insert_dataframe.index:
        update_query = f'''
        UPDATE fulfillment
        SET
        tracking_number insert_dataframe['tracking_number'][i]
        WHERE
        id = insert_dataframe['id'][i]
        '''

        conn.execute(update_query)

위와 같이 단건으로 트랜잭션을 발생시키고 네트워크를 태우는 성능 최저의 방식을 사용한다했을때 데이터 1만개 기준 1시간이 넘어가는 성능을 보여줬다. 물론 안정성은 보장되고 데이터의 수가 적다면 위의 인덱스 하나하나 처리하는 방식이 맞다고 할수 있다.

하지만, 데이터 20만개정도를 업데이트한다고 했을때는 말도 안되는 저성능을 보여준다.

 

이때 사용한것이 temp 테이블을 이용한 join 업데이트다. 물론, 애플리케이션 단에서 모두 처리할 수 있는 방법이 있을것이라 생각하는데 찾아내지는 못했다. 여기서 조건은 업데이트할 테이블이 속해있는 database에 temp 테이블이 있어야한다. 방법은 아래와같다.

 

1. fulfillment_table과 동일한 포맷인 fulfillment_temp_table을 create한다.

2. fulfillment_table에 target데이터인 insert_dataframe을 to_sql메서드를 통해 insert한다.

3. left join을 통해 일괄적으로 fulfillment_table_tracking_number를 update한다.

insert_dataframe # to_sql로 insert할 target_data

insert_dataframe.to_sql('fulfillment_temp', con=ENGINE_LOCAL, if_exists='append', index=False, method='multi', chunksize=50000)

update_query = '''
UPDATE
fulfillmet as a
SET
tracking_number = b.tracking_number,
created_at = b.created_at,
FROM
fulfillment_temp as b 
WHERE
a.id = b.id
'''
with engine.connect() as conn:
    conn.execute(update_query)

위와 같은 방식으로 join을 이용한 대량의 update를 할수 있다.

 

bulk_update 방식으로 업데이트 데이터들의 query를 일일이 생성하여 대량 업데이트하는 방식도 있지만 이방식이 가장 간단했다고 생각이 든다.

728x90

'기술 > database' 카테고리의 다른 글

RDB에서 Bigquery로 데이터 이전하기  (0) 2023.02.22
데이터베이스 실행(MYSQL)  (0) 2020.11.16
데이터베이스 정의_2(MYSQL)  (0) 2020.11.16
데이터베이스의 정의_1(MYSQL)  (0) 2020.11.16
728x90

행운의 사진.. 쓸 썸넬이 없다면 무조건 이놈이다

그토록 원하던 서버개발자가 되고 힘들었지만 어느덧 100일차가 넘어가고 있는 시점이 되었다.

100일간 어떻게든 1인분은 하기 위해서 노력했고 물론 1인분은 못하지만 나름 도움이 되고 있지 않나?혼자서 위로할 수있는정도는 된것 같다. 

 

100일간 크게 서버 개발자로써 나름 여러가지 경험과 성과를 이룬것 같다.

1. ORM만 사용하다 SQL의 묘미를 알아내다!

솔직히, 취업 전까지 sql을 직접 쓰는 일이 없었다. 애초에 orm만 사용하다 편리함에 잊혀졌고 sql 사용하는 방법도 가물가물했다. 그러다 json 필드에 데이터를 막넣어서 꺼내쓰던 비정규화의 끝판왕이던 테이블을 정규화하는 작업을 하게 되었고 기존의 데이터를 정규화한 필드에 알맞게 넣어야하는 일이 생기며... sql을 쓸수 밖에 없게 되었다. 하지만 이게 뭐람 python 스크립트로 django 임포트후 for문을 돌려 하나하나 넣어야 하지않나? 밖에 생각못하던 것과 달리 postgres의 문법은 update에 특화 되어있지 않나? 생각했다. 물론 sql의 묘미는 이것이 아니라 group by를 탑재하신 Read에서 성과를 드러냈다. 여러 이슈가 생기는 실서비스에서는 원인이 무엇인지 데이터베이스에서 조금만 찾아보면 다나온다 그리고 다른팀에서 데이터를 요청했을때 sql을 사용하면 쭈르륵 나오게 된다ㅎㅎ 매번 print문으로 뽑아서 주던 이전의 나의 모습은 잊게 되었다.

2. 성능을 끌어올리는 법을 알아 가고 있다.

1번의 경험과 이어지는데 기본적으로 정규화 vs 비정규화를 비교한다면, 항상 그렇지는 않지만 조회시 정규화가 잘된 구조가 성능이 더 좋다는걸 몸소 알게 되었다. 물론 생성 삭제, 수정시에는 안그럴수도 있겠지만 기존에 비정규화되어있던 테이블에서 데이터를 가져와 2중 반복문 구조를 벗어던지고 쿼리 한번으로 해결하게 되어 속도가 기존과 비교해 굉장히 빨라졌다. 이작업을 하며 몸에 익힌 팁은 아래와같다.

 

- 서버에서 데이터베이스 호출은 최대한 줄여야한다. 비용이 상당히 높다.

- 데이터호출 2번 vs 데이터 호출 1번 + for문 이라면 예외 상황을 제외하고 보통 후자를 선택해야한다. 네트워크 호출 한번이후 메모리에 데이터를 올리고 작업하는것이 확실히 빠르다.

- 결과가 true, false로 구현되는 구조라면 exists를 애용하자! 모든 데이터를 끝까지 조회하는것은 굉장히 비효율적이다. exists면 해당 조건에 걸리는순간 결과를 반환한다. 물론 false라면 끝까지 가겠지만? 조금이라도!!데이터가 많으면 많을수록 개선효과가 두둑할꺼다.

- 언어의 내장함수를 애용하자! 사실 내장함수는 내가 그냥 구현하면 되는거잖아?라는 생각을 가지고 있었고 필수적인 것을 제외하고는 일부러 익히려고 하지 않았다. 하지만, 내장함수의 내부구조를 까보면(물론 python에만 해당할수도있는얘기!) 그건 아니였다. 내가 구현한 방법보다 훨씬 최적화되어 구현되어있고 또 알아가다보면 python comprehension처럼 일반 적인 for append구조보다 성능을 배로 띄어나게 만들수 있는 좋은 함수들이 있다.

 

말이 너무길어지는데 이번 챕터는 따로 블로그에 게시를 해야겠다.

 

3. 테스트와 문서화는 선택이 아닌 필수다. 

사실 나는 이두가지가 필요없는것이라 생각했다. 테스트 자체는 예를 들면 3과5를 더했을때 8인가요?라는걸 확인 하는것과 같이 느껴왔고 문서화는 내가 코드를 가독성 좋게 짜고 폴더 및 파일 구조도 잘배치 하면 해결된다 생각했다. 내생각이 먼저 바뀐 건 문서화부터였다. 이미 몇년간 서비스를 지속하며 여러명의 개발자들이 거쳐간 서비스는 대체로 나같은 신입 뿐만 아니라 경력직분들도 파악하는데 굉장한 어려움을 느끼며 초반에 먼저해야하는 작업이다. 해당 서비스의 목적과 히스토리 그리고 배포 및 빌드 방법과 같은 문서가 제대로 정리 되어있다면 후에 오는 개발자들을 위해 도움이 될뿐만아니라 시간이 지나고 다시 유지보수를 해야할때 자신에게도 굉장한 시간절약이 될것이다. 

 

테스트의 경우, 코드 리팩터링 과정을 거치고 당연히 내가 만진 부분만 테스트 후 커밋해서 서비스 배포를 진행 했는데 주문을 하는과정에서 내가 고친 코드의 오류로 인해 다른 증정 상품이 나가게된 상황이 발생했다.... 이때 굉장한 멘붕이 왔고 정신을 차리고 난후 테스트의 중요성이 여기서 나오는구나라고 느끼게 되었다. 3+5=8이라는 당연한 결과를 테스트 하지 않는것은 수백 수천개의 함수와 클래스로 이뤄진서비스에서 하나가 바뀌게 되면 3+5=300이라는 결과를 초래할수 있다. 이는 내가 고친 부분이 다른곳에도 영향을 끼치기 때문이며 시간을 생각했을때도 매번 고칠때마다 직접 하나하나 확인하는것이 아닌 똑같은 상황을 테스트 코드로 미리 설정해둠으로써 우리가 예상한 3+5=8이라는 결과가 커밋시에 매번 서비스 전체적으로 동일해야함을 확인하는 과정을 거침으로써 문제발생을 대비할수 있다.

 

 

주저리 주저리 말이 많았는데 100일간 나도 모르는 사이에 실력이 많이 늘어난것 같다는 느낌을 받고 있고 새로운것을 습득하는 재미를 느낀다. 사실 나는 도움이 안되고 그냥 민폐만 끼치는거 같다라는 생각때문에 조급해하며 회사일에만 메달려 있었는데 매번 조급해하지 않아도 된다는 좋은 동료분들이 있어서 다행이고 그런 조급함이 오히려 나의 성장에 도움을 줬다는 생각도 한다. 이제는 워라벨을 찾아 보려 하는데 취미중 하나로 기술 블로그를 활성화 시키는걸 해볼까 한다. 암튼! 다음 마음가짐 카테고리는 200일차에 오늘걸루!!! 그때 까지 화이팅이다~

728x90
728x90

7. 쿼리와 데이터베이스 레이어

7-1. 단일 객체에서 get_object_or_404()사용하기

- 단일 객체를 가져오는 세부 페이지와 같은 뷰에서는 get()대신 get_object_or_404()를 사용해준다.

- 해당 객체가 없을경우, 예외처리를 동시에 해주는 유용한 함수 이므로 애용해줘야한다.

- 꼭 뷰에서만 사용하는 걸 권장한다. 뷰를 제외한 곳에서의 사용은 최대한 지양하도록 해야한다.

    - 예전의 사례에서 보면, 뷰가 아닌 여러곳에서 애용하다가 특정 데이터를 지웠을때 모든 페이지에 문제가 생기는 결과가 나타났으므로 굳이 위험한 행동을 사서 하지 않길 바란다.

 

7-2. 예외를 일으킬 수 있는 쿼리를 주의하자

- get_object_or_404를 이용하면 예외처리가 필요없지만 그 외의 모든 경우엔 try-except로 예외처리를 할 필요가 있다.

- ObjectDoesNotExist와 DoesNotExist

    - ObjectDoesNotExist는 어떤 모델에서도 사용가능함

    - DoesNotExist는 특정 모델에서만 사용가능함

def list_flavor_line_item(sku):
    try:
        return Flavor.objects.get(sku=sku, quantity__gt=0)
    excepy Flavor.DoesNotExist:
        msg = "We are out of {0}".format(sku)
        raise OutOfStock(msg)
        
def list_any_line_item(model, sku):
    try:
        return model.objects.get(sku=sku, quantity__gt=0)
    except ObjectDoesNotExist:
        msg = "We are out of {0}".format(sku)
        raise OutOfStock(msg)

-MultipleObjectsReturned

    -여러개의 객체가  반환되었을때 사용

def list_flavor_line_item(sku):
    try:
        return Flavor.objects.get(sku=sku, quantity__gt=0)
    except Flavor.DoesNotExist:
        msg = "We are out of {0}".format(sku)
        raise OutOfStock(msg)
    except Flavor.MultipleObjectReturned:
        msg = "Multiple items have SKU {}. Please fix!".format(sku)
        raise CorruptedDatabase(msg)

7-3. 쿼리를 좀 더 명확하게 하기 위해 지연 연산 이용하기

- 복잡한 쿼리의 경우 한줄에 모두 표현하는것은 최대한 지양해야한다.

- 지연 연산(lazy evaluation): 정말로 데이터가 필요하기 전까지는 장고가 sql을 호출하지 않는 특징

- 지연 연산을 이용해서 여러 쿼리를 여러줄에 나누어 좀더 간결하고 명확하게 구성한 코드 작성이 중요하다.

 

7-4. 필수 불가결한 상황이 아니라면 로우 sql 이용은 지양하자

 

7-5. 필요에 따라 인덱스를 이용하자

- 모델에 index=True만으로 추가가 가능하다

- 하지만 언제 사용을 해야하는지를 결정하고 적절히 사용하는것이 인덱스사용에 가장 중요하다.

 

7-6. 트랜잭션

- 장고는 기본적으로 orm이 모든 쿼리를 호출할 때마다 자동으로 커밋을 진행한다.

    - 매번 .create()나 .update()가 수행 될때마다 sql 값들이 변함을 뜻함

    - 단순 토이 프로젝트 수준에서는 문제없이 편하다고 생각할 수 있겠지만 만약 데이터 베이스상의 충돌이 발생했을때부터는 위험성이 존재하게 되었다는 것을 뜻한다.

- 트랜잭션의 기본 특성 ACID를 이해하고 적용해야 한다.

 

7-6-1. 각각의 HTTP요청을 트랜잭션으로 처리하라

#settings/base.py

DATABASES = {
    'default': {
        ...
        'ATOMIC_REQUESTS': TRUE,
    },
}

- 위와 같이 처리하면 읽기 데이터를 포함한 각요청마다 트랜잭션으로 처리할 수 있게 된다. 이는 데이터베이스 쿼리가 보호되는 안정성을 얻을 수 있지만 단점은 성능 저하를 가져올 가능성이 굉장히 높아질 수 있다는것 이다.

- 이를 해결하기 위해서는 transaction.non_atomic_requests()데코레이터를 선택한다.

    - 모든 요청에 트랜잭션이 아닌 데이터베이스의 영향이 가는작업(create, update, delete)시에만 트랜잭션을 선택적으로 적용해주는 것이다.

 

7-6-2. 명시적인 트랜잭션 선언

-트랜잭션 가이드라인

    1. 데이터베이스에 변경이 생기지 않는 데이터베이스 작업은 트랜잭션으로 처리하지 않는다.

    2. 데이터베이스에 변경이 생기는 데이터베이스 작업은 반드시 트랜잭션으로 처리한다.

- django orm에 기반한 가이드라인

목적 orm 메서드 트랜잭션을 이용할 것인가?
데이터 생성 create(), bulk_create(), get_or_create() yes
데이터 가져오기 get(), filter(), count(), iterate(), exist(), exclude(), in_bulk() no
데이터 수정하기 update() yes
데이터 삭제하기 delete() yes

 

정리

- 장고로 crud를 만드는걸 넘어서서 한단계 더 나아가려면 바로 이런 트랜잭션, 인덱스, 캐시 사용과 같은 것을 다룰수 있어야 한다고 생각이 든다. 신입이라면, 회사의 코드를 확인하며 어떻게 적용하고 있는지에 대해서 알아볼 수 있는 기회가 있을것이라 생각된다!

728x90
728x90

6. 장고에서 모델 이용하기

- 모델은 장고 프로젝트에서 토대가 되는 부분이다. 성급하게 모델을 작성하게 되면 어느 순간 프로젝트가 내생각과 다르게 흘러가는 경험을 하게 될것이다.

- 그렇기 때문에 모델은 신중히 미래를 고려하여 설계해야만 한다.

 

6-1. 앱을 분리하자

- 모델이 너무 많으면 앱을 나눠야 한다. 하나의 앱에 모델이 20개 이상있으면 너무 많은 일을 한다는것을 반증하는 것이다. 책에서는 모델 5개가 넘지 않길 권장한다.

6-2. 모델 상속에 주의하자

모델의 상속 스타일 장점  단점
상속을 이용하지 않는 경우: 모델들 사이에 공통 필드가 존재할 경우, 두 모델에 전부 해당 필드를 만들어 준다. 테이블에 어떤 식으로 매필되는지 상관 없이 장고 모델을 한 눈에 이해하기 쉽다. 모델들 사이에 서로 중복되는 테이블이 많을 경우 이를 지속적으로 관리하는 데 어려움이 따른다.
추상화 기초 클래스: 오직 상속받아 생성된 모델들의 테이블만 생성된다. 추상화된 클래스에 공통적인 부분을 추려 놓음으로써 한 번만 타이핑을 하면 된다. 
추가테이블이 생성되지 않고 여러 테이블에 걸쳐 조인을 함으로써 발생하는 성능 저하도 없다.
부모 클래스를 독립적으로 이용할 수 없다.
멀티테이블 상속: 부모와 자식 모델에 대해서도 모두 테이블이 생성된다. onetoonefield는 부모와 자식간 적용된다. 각 모델에 대해 매칭되는 테이블이 생성된다. 따라서 부모 또는 자식 모델 어디로든지 쿼리를 할 수 있다. 부모 객체로부터 자식 객체를 호출 하는 것이 가능하다: parent.child 자식 테이블에 대한 각 쿼리에 대해 부모 테이블로의 조인이 필요하므로 이에 따른 상당한 부하가 발생한다.
프록시 모델: 원래 모델에 대해서만 테이블이 생성된다.  각기 다른 파이썬 작용을 하는 모델들의 별칭을 가질 수 있다.  모델의 필드를 변경할 수 없다.

6-3. 장고 모델 디자인

- 정규화 하기 -> 데이터베이스 자체의 정규화에 대한 지식습득이 먼저다! 이 원칙에 맞춰 디자인을 해야만 한다.

- 캐시와 비정규화

    -적절한 위치에서 캐시를 세팅하는 것은 모델을 비정규화 할 때 발생하는 문제점들을 상당부분 해결해준다.

- 반드시 꼭 필요할때만 비정규화를 하도록한다.

-언제 널을 쓰고 언제 공백을 쓸것인가?

필드 타입  null=True blank=true
charfield, textfield, slugfield, emailfield, uuidfield, filefield, imagefield 이용하지 않는다. 이용한다.
booleanfield 이용하지 않는다. 이용하지 않는다.
integerfield, floatfield, decimalfield 해당값이 null로 들어가도 문제가 없다면 이용한다. 해당값이 빈값을 받아도 문제가 없다면 이용한다.
datetimefield, datefield null값으로 설정 가능하다면 이용한다. null=true와 같이 이용한다.
foreignkey, manytomanyfield, onetoonefield null값으로 설정 가능하다면 이용한다. 이용한다.

 

-정리

이외에도 모델에 관한 부분은 수많이 있으며 또한 two scoops of django에도 굉장히 자세히 다루고 있다. 제필요성에 의해서 필요한 부분만 적어둔거기 때문에 따로 웹서핑이든 도서를 통해 계속해서 학습을 해야하는 파트라고 생각됩니다!

728x90
728x90

5. Settings와 requirements 파일

장고 세팅 모듈에서는 수많은 항목의 설정을 제공하고 있다. 이것이 장고의 장점이자 어려움이 될 수 있다.

책에서 추천하는 장고 최적의 설정 방법은 아래와 같다.

- 버전 컨트롤 시스템으로 모든 설정 파일을 관리해야 한다.

- 반복되는 설정들을 없애야 한다.

- 암호나 비밀 키 등은 안전하게 보관해야 한다.

 

5-1.버전 관리되지 않는 로컬 세팅은 피하도록 한다.

- django의 시크릿키와 같은 것들은 공개 되서는 안되기 때문에 감춰줘야한다.

- 디버깅 툴과 같은 것은 운영 서버에서는 활용 되지 않고 개발 환경에서만 활용 되기 때문에 제외해야 한다.

- 즉, settings모듈을 분리해냄으로써 해결 가능한 부분이다.

 

5-2. 여러 개의 settings 파일 이용하기

- 한개의 settigns파일만 사용하는 것이 아닌 용도에 맞게 settings 파일을 분리해낸다.

settings/
    __init__.py
    base.py # 프로젝트의 모든 인스턴스에 적용되는 공통 세팅 파일
    local.py # 로컬환경에서 작업할때 사용(디버그, 로그 레벨, etc...)
    staging.py # 실운영 환경에 넘어가기전 반 운영 환경용 세팅 파일
    test.py # 테스트 러너, 로그 세팅을 위한 파일
    production.py # 실 운영 서버를 위한 세팅 파일

- 각 settings 파일 실행 명령어는 아래와 같다.(config는 프로젝트 명이다)

python manage.py runserver --settings=config.settings.local

- 각 세팅파일의 들어가는 코드의 차이는 개인적인 것이지만 공통적으로 DEBUG는 분리해줘야한다. 개발=True, 배포=False는 기본적으로 지켜줘야하는 분명한 사실이다.

- 그외의 실질적인 분리방식은 아래 링크를 참고 해도 될듯싶다!(깨알 깃허브 홍보ㅎ)

 

GitHub - DreamIn-Developer/server: Dreamin server repo

Dreamin server repo. Contribute to DreamIn-Developer/server development by creating an account on GitHub.

github.com

 

5-3. 코드에서 설정 분리하기

- secret key, aws key, api key와 같은 다른 사람들이 알아선 안되는 키값같은 경우, 분리해서 감춰야만 한다.

- 이를 해결하는 방식은 json, .env파일과 같은 형식으로 관리할 수있다.

- 하지만, 환경변수도 한가지 방식이고 확장에 용이하므로 알아보면 좋다.

- 이부분은 너무나 방식이 다양하여 책을 구입하여 참고하거나 구글에서 검색해보길 권장한다.

 

5-4. 여러개의 requirements파일 이용하기

- 환경이 여러개로 분리되면 각각에 해당하는 requirements를 생성해야한다.

- respository_root 아래에 아래와 같은 구조로 관리할 수 있다.

requirements/
    base.txt
    local.txt
    staging.txt
    production.txt

- 각 settings 파일과 동일한 이름으로 생성해준다.

- 설치는 아래와 같이 한다. -> 그에 맞는 가상환경 생성은 기본이다!!

pip install -r requirements/local.txt

 

728x90
728x90

 

3. 어떻게 장고 프로젝트를 구성할 것인가

3-1. 장고의 기본 프로젝트 구성

- 장고의 기본명령어를 실행하면 그리고 대부분의 나와같은 토이프로젝트만 만들던 신입 개발자들은 아래와 같은 프로젝트 구성이 되어 있을것이다.

mysite/
	manage.py
    app/
    	__init__.py
        admin.py
        urls.py
        views.py
        models.py
        tests.py
    mysite/
    	__init__.py
        settings.py
        urls.py
        wsgi.py
        asgi.py

-하지만, 이는 여러 소스코드가 이곳저곳 존재할 실무에서는 혼동을 주기 쉬운구조로 이책에서는 아래와 같은 방식을 제안한다.

mysite_project/
    .gitignore
    Makefile
    Dockerfile
    docs/
    README.md
    requirements.txt
    mysite/
    	manage.py
        media/ #개발전용
        static/
        templates/
        app/
        config/
            __init__.py
            settings/
            	base.py
                deployment.py
                development.py
            urls.py
            asgi.py
            wsgi.py

- 총 3단의 구성으로 최상위레벨, 중간 레벨, 최하위 레벨로 나뉜다. 책에 정리된 것들을 적었으며 이것들 외에 성격이 비슷한 파일 및 디렉터리를 자신의 생각에 맞게 배치해두면 될것 같다!

- 최상위레벨: 저장소 루트

    -djnago의 실질적인 동작을 이루는 프로젝트 소스외에 배포 및 문서에 관련된 것들로 이루어진다.

파일/디렉터리 설명
.gitignore 깃이 처리하지 않을 파일과 디렉터리를 정의한것
README.md와 docs 나외의 다른 개발자가 쉽게 프로젝트를 파악할 수 있도록 문서화한것
Makefile과 Dockerfile docker와 같은 배포시 필요한 작업을 정리한 파일 및 이미지
requirements.txt 장고의 패키지 버전 목록을 정리해둔파일
mysite/ 프로젝트 소스

- 중간 레벨: 프로젝트 루트

    -장고 프로젝트 소스들이 위치하는 디렉터리로 모든 파이썬 코드들이 위치한 곳이다.

파일/디렉터리 설명
config/ 프로젝트의 설정 파일 즉, settings파일이 모여있는 디렉터리
manage.py django를 동작시키는 명령어 파일(왠만하면 수정하지 않길 권장)
media/ 개발 용도로 이용되는 이미지 관련 디렉터리다
app/ 개발간 확장할 users, post와 같은 app들을 의미한다.
static/ css, js, image등 사용자가 올리는 것 이외의 정적파일들
templates/ 시스템 통합 템플릿 파일 저장 장소

-최하위레벨: 설정 루트

    -django의 최종 url의 정의 및 설정 파일 wsgi설정 파일등 초기 프로젝트 생성시에 나오는 루트 디렉터리이다.

 

3-2.장고 쿠키커터

- 책에서는 쿠키커터를 추천해주는데 개인적으로 현재 많은 버전 업데이트가 이뤄지며 장고 자체로도 문제없다 생각되어 잘정리된 링크를 추천드립니다!!(two scoops of django책을 구매하길 추천해요!)

 

4. 장고 앱 디자인의 기본

4-1. 장고의 용어 정의

- 장고 개발을 할때 용어에 대해 간단히 정리하고 가야한다.

    -장고 프로젝트: 장고 웹프레임워크를 기반으로 한 웹 애플리케이션을 지칭한다.

    -장고 앱: 프로젝트의 한 기능을 표현하기 위해 디자인된 작은 라이브러리를 의미한다. 아래 명령어로 생성되는 디렉터리를 말한다.

python manage.py startapp <app_name>

    -INSTALLED_APPS: 프로젝트에서 이용하려고 settings.py에 설정한 장고 앱들을 지칭한다.

    -서티 파티 장고 패키지: 파이썬 패키지 도구들에 의해 패키지화된, 재사용 가능한 플러그인 형태의 장고앱을 지칭한다.

    -코린이 TIPS! -> 책에 나오진 않았는데 나름 편한 방식이라 팁 전수하고 갑니다.

    -네뭐.. 그렇다구요 나중에 앱이 많아질것 대비해서 미리 분리하고 가는것 매우 좋습니다 애용해주셔요ㅎㅎ

DJANGO_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

PROJECT_APPS = [
    'accounts',
    'posts',
    'notifications',
    'teams',
    'images',
]

THIRD_PARTY_APPS = [
    'rest_framework',
    'corsheaders',

    'drf_yasg',
    'django_extensions',

]

INSTALLED_APPS = DJANGO_APPS + PROJECT_APPS + THIRD_PARTY_APPS

 

4-2. 장고 앱 디자인의 황금률

- 장고 앱 디자인 즉, 정의는 각 앱이 그 앱의 주어진 임무에만 집중할 수 있도록 정의해야한다는 것이다.

- 아래와 같은 방식으로 앱은 그자체로 기능을 설명할 수 있어야합니다.

# 유저의 정보를 기록하고 저장하며 조회할수 있는 앱
users/
# 게시글을 기록하고 저장하며 조회할수 있는 앱
posts/
# 팀을 관리할수 있는 앱
teams/

- 추가로 개인적인 의견을 덧붙이자면 app을 분리했을때 독립적으로 존재할 수 기능을 할수 있는 단위가 분리의 단위라고 생각합니다.

    -즉, post는 따로 제약사항 없이 생성이 가능하지만 comment의경우, post가 없다면 존재할 수 없는 기능이기 때문에 분리하지 않고 post내부에 comment를 속하게 개발합니다. -> 이것은 개인적인 개발 스타일 일뿐 꼭 따르지 않아도 됩니다!!(책내용 아니에요!)

 

4-3 장고 앱 이름 정하기

- flavors, animals, polls, dreams와 같이 앱 이름은 명료하게 지어야한다.

- 앱의 이름은 복수의 형태로 지어야한다.

- pep8의 규약에 맞게 앱 이름을 짓도록 하자. 소문자로 구성된 숫자, 대시(-), 마침표(.), 스페이스와 같은것들은 왠만해서는 넣지말자

- 밑줄(_)의 경우 가독성을 높이기 위해서 꼭 사용해야 한다면 사용하도록 하자

 

4.4 앱 안에는 어떤 모듈이 위치하는가?

- 공통 앱 모듈: 이미 오랜 시간 장고를 개발하며 장고 앱을 구성하는 오피셜한? 앱이름들이다.(이건 따르도록하자!)

app/
    __init__.py
    admin.py
    management/
    forms.py
    migrations/
    models.py
    templatetags/
    test/
    urls.py
    views.py

 

-비공통 앱 모듈

app/
    behaviors.py
    constants.py
    context_processors.py
    decorators.py
    db/
    exceptions
    fields.py
    factories.py
    helpers.py
    managers.py

- 간단히 용어 정의를 하자면

앱 이름 설명
behaviors.py 모델 믹스인의 위치에 대한 옵션
constants.py 앱레벨에서 이용되는 세팅을 저장하는 장소의 이름
decorators.py 데코레이터가 존재하는 곳
db/ 여러 프로젝트에서 이용되는 커스텀 모델이나 컴포넌트
fields.py 폼 필드 이용에 사용되는 파일
factories.py 테스트 데이터 팩토리 파일
helpers.py 헬퍼 함수. 뷰와 모델을 가볍게 하기위한
managers.py models.py가 너무 커질경우, 커스텀 모델 매니저를 옮기는 용도
signals.py 커스텀 시그널 함수 저장소

 

728x90
728x90

1. 코딩스타일

1-1. 읽기 쉬운 코드를 만드는것이 왜 중요한가

- 코드의 특서은 한번 작성되면 여러번 읽힌다. 몇 분에서 몇십년 뒤까지 재활용될수가 있는데 이것을 이곳저곳 막 만들어두면 언젠가 이를 참고하고 수정하기는 더더욱 어려워지기 대문에 처음 코드를 만들때 읽기 쉬운 코드를 만들어야한다.

-어떻게 읽기 쉽게 만드는가?

- 축약적이거나 함축적인 변수명은 피한다.

- 함수 인자의 이름들은 꼭 써준다.

- 클래스와 메서드를 문서화한다.

- 코드에 주석은 꼭 달도록 한다.

- 여러 함수 내에서 반복되는 코드들을 리팩토링해서 반복을 최소화한다.

- 함수와 메서드는 가능한한 작은 크기를 유지한다.

 

1-2. PEP8

- 파이썬 공식 스타일 가이드은 PEP8을 여러번 읽어보며 스타일을 맞추기 위해 최대한 노력한다.

- 만약 이미 다른 관례를 따르고 있는 프로젝트는 함부로 바꾸지 않도록 해야한다.

 

1-3. import *은 피하자

- 우리의 코드 대부분은 각 모듈을 개별적으로 임포트해야한다.

from django.forms import *
from django.db.models import *

- 위와 같이 모두를 임포트하지 말고 개별적으로 함수나 클래스를 임포트해야한다.

- 이외에도 표준 PEP8에 임포트 관련한 내용은 많으니 꼭 참고하길 바란다!

 

1-4. 장고 코딩 스타일

- URL 패턴 이름에는 대시(-) 대신 밑줄(_)을 사용한다.

# 잘못된 예시
patterns = [
	url('corin/', index, name='home-index')
]

# 좋은 예시
patterns = [
	url('corin/', index, name='home_index')
]

- 여기서 유의할껀 패턴이름은 엔드포인트 주소가 아닌 django의 name부분이라것이다.

- rest를 따르는 엔드포인트에서는 밑줄이 아닌 대시를 사용하여 명시하여야한다 -> parser renderer와 관련되어 보안이슈가 있었다고 한다.

 

2. 최적화된 장고 환경 꾸미기

2-1. 같은 데이터베이스를 이용해라

- 일반적으로 개발환경은 sqlite3, 배포 즉 실서버환경은 postgresql를 이용하는 경우가 많다.

- 이는 굉장히 위험한 방식으로 물론 sqlite3가 편하긴 하겠지만 다른 종류의 데이터베이스 사이에는 다른 성격의 필드 타입과 제약 조건이 존재한다. 이를 굳이 편하다는 이유만으로 다르게 유지하면 안된다. 반드시 실서버에서 사용하는 데이터베이스를 개발환경에서도 동일하게 사용해줘야한다.

- django orm은 강력하지만 무적이 아니다. 

 

2-2. pip와 virtualenv 사용하기

- 사실상, 현재는 pip와 virtualenv는 관행처럼 사용되고 있다.

- pip는 그렇다치고 virtualenv같은경우, 다른 대안또한 많이 있다 -> 여기서 말하는것을 바로 가상환경을 사용하라는것이다. 가끔가다 로컬 전역에 설치를 해버리는 경우가 있는데 이는 추후 다른 프로젝트를 진행할때 버전관리가 굉장히 꼬여 개발시작전부터 어려움을 겪게 될테니 각프로젝트별 가상환경 및 requirements.txt를 사용하여 사용 라이브러리 버전을 맞춰줘야한다!

728x90

+ Recent posts