728x90

명시적으로 api 작성하는것과 적절한 모델링을 통해 서버 개발하는것은 기본입니다. 그렇다면, 어떤 부분에서 성능을 개선할 수 있을까? 생각해봐야합니다.

 

추후 다른 주제로도 다루겠지만 이번 챕터에서는 쿼리에 대해 다뤄보겠습니다.

 

1. select_related와 prefetch_related(feat. N+1)

이건 많이 접했을것이라 생각듭니다. 먼저 select_related와 prefetch_related에 대해 알아보기전 django orm의 고질적인 문제인 N+1문제에 대해 알아봐야합니다.

from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=100)


class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)


from .models import Book


def book_list(request):
    books = Book.objects.all()
    for book in books:
        author_name = book.author.name  # 작가 정보를 가져오기 위해 추가 쿼리 발생
    return render(request, 'book_list.html', {'books': books})

위와 같은 기본적인 작과와 도서에 대한 간단한 모델의 각 도서 목록의 작간의 이름을 가져오는 방식을 볼때의 경우,

- Book.objects.all() 도서목록을 가져오는 쿼리 1회

- book.author.name을 통해 n개의 작가 이름을 가져오는 쿼리 n회

이러한 비효율적인 쿼리가 발생하게 됩니다.

 

이러한 문제를 해결하기 위해 select_related를 활용할 수 있습니다.

select_related란, Foreinkey 혹은 OneToOneField와 같은 관계 필드에 대해 사용하며 데이터베이스에서 필요한 연관 객체를 추가로 로드하게 되어 위와같은 N+1 쿼리 문제를 해결할 수 있게 됩니다.

books = Book.objects.select_related('author').all()
for book in books:
    author_name = book.author.name  # 추가 쿼리 없이 author를 미리 로드

- Book.objects.select_related('author').all() 1회의 쿼리에서 미리 작가의 정보까지 로드

이후, 반복문의 경우 미리 로드되어 있는 author의 정보로 인해 추가적인 쿼리가 발생하지 않게 됩니다.

 

그렇다면, prefetch_related는 무엇일까요? 이또한, N+1문제를 해결할 수 있으며 select_related와는 다르게 ManyToManyField 및 reverse ForeignKey/OneToOneField 관계와 같은 역참조 필드에 사용됩니다.

class Category(models.Model):
    name = models.CharField(max_length=100)
    books = models.ManyToManyField(Book)


from .models import Category

# prefetch_related를 사용하지 않은 경우
categories = Category.objects.all()
for category in categories:
    books = category.books.all()  # N+1 쿼리 발생

# prefetch_related를 사용한 경우
categories = Category.objects.prefetch_related('books').all()
for category in categories:
    books = category.books.all()  # 추가 쿼리 없이 books를 미리 로드

- Category.objects.all() 카테고리 목록을 가져오는 쿼리 1회의 쿼리에서 미리 도서 목록의 정보까지 로드

이후, 추가 쿼리에서는 미리 로드 되어있는 books로 인해 추가적인 쿼리가 발생하지 않게됩니다.

 

물론, 이 메서드들을 때에 맞지 않게 사용할시 오히려 역효과를 나을수 있겠지만 잘 사용만 한다면 쿼리 성능을 극대화 할 수 있는 가장 효율좋은 메서드 입니다.

 

2. bulk_create, bulk_update

예시를 들어봅시다. name과 price로 이뤄진 Product 모델이 있고 각각의 데이터 100개를 저장해야합니다.

from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)


from .models import Product

products_to_insert = [
        Product(name='Product 1', price=10.99),
        Product(name='Product 2', price=20.99),
        Product(name='Product 3', price=15.99),
        ...
        Product(name='Product 100', price=14.99),
    ]

for product in products_to_insert:
    product.save()

위와 같이 저장할시, 당연히 문제 없이 저장이 될것 입니다. 하지만, 100개의 쿼리가 발생하며 이말은 즉, 100개의 트랜잭션의 작업이 발생한다는 것입니다.

Product.objects.bulk_create(products_to_insert)

 bulk_create를 사용하면 저장할 Product 스키마에 맞는 데이터를 리스트에 담아 전달해주면 하나의 트랜잭션내에서 100개의 데이터를 생성해낼 수 있습니다.

물론, 성능상 이점도 있겠지만 하나의 트랜잭션 내에서 실행되므로 일관성이 유지된다는 장점 또한 존재합니다.

 

bulk_update도 어렵지 않게 bulk_create와 동일한 방식으로 사용가능합니다.

products_to_update = [
        Product(name='Product 1', price=10.99),
        Product(name='Product 2', price=20.99),
        Product(name='Product 3', price=15.99),
        ...
        Product(name='Product 100', price=14.99),
    ]

Product.objects.bulk_update(products_to_update, ['price'])

bulk_create와 마찬가지로 스키마에 맞는 데이터를 리스트에 담아 전달 후, 업데이트할 데이터를 2번째 인자로 리스트에 담아 전달해주면 bulk_update가 완료됩니다.

 

추가로 insert_or_update 기능또한 존재합니다. 기본적으로 bulk_create 메서드를 이용하여 사용하는데 사용방식은 아래와 같습니다.

from django.db import models


class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)
	quantity = models.PositiveIntegerField()
    date = models.DateField()
    
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["name", "date"],
                name="unique name and date",
            )
        ]


from .models import Product


product_data = [
        Product(name='Product 1', price=10.99, quantity=10, date='2021-01-03'),
        Product(name='Product 2', price=20.99, quantity=6, date='2022-01-03'),
        Product(name='Product 3', price=15.99, quantity=3, date='2023-01-03'),
        ...
        Product(name='Product 100', price=14.99, quantity=2, date='2023-05-03'),
    ]

Product.objects.bulk_create(
    product_data,
    update_conflicts=True,
    unique_fields=["name", "date"],
    update_fields=["quantity", "price"],
)

bulk_create 메서드에서 update_conflicts=True로 설정뒤 고유값으로 설정한 필드와 업데이트할 필드를 입력해줍니다.

주의사항의 경우, 기본적으로 unique하게 선언되어 있지 않던 컬럼의 경우, model단에서, constraint로 선언해주어야합니다.

 

위의경우, 기존에 name=Product 1, date:2021-01-03이였던 데이터가 있었다면 price와 quantity가 업데이트 되며, 이외의 데이터의 경우, 새롭게 생성되게 됩니다.

 

물론, bulk_create, bulk_update방식이 위와같이 가장 이상적으로 담겨진다면 문제가 없겠지만 어쩔수 없이 로직상 불가능한 경우엔 사용이 어려운 부분이 있어 상황에 맞게 사용해야함은 당연합니다.

 

개인적으로, bulk 기능을 잘이용하기위한 팁은 순서를 보장하는 자료구조와 dataclass를 사용하여 미리 데이터 셋을 정의하고 bulk 기능을 사용하는것이 유용하니 참고 해보시길 바랍니다.

 

3. cached_property

class Travel(BaseAdminModel):
    user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="여행 리스트 방장")
    members = models.ManyToManyField(User, through="Member", related_name="travels", verbose_name="여행 멤버들")
    title = models.CharField(max_length=255, verbose_name="여행 제목")
    color = models.CharField(max_length=255, verbose_name="여행 색상")
    start_date = models.DateField(verbose_name="여행 시작 날짜")
    end_date = models.DateField(verbose_name="여행 끝나는 날짜")
    description = models.CharField(max_length=13, null=True, verbose_name="여행 메모")
    currency = models.CharField(max_length=15, choices=CurrencyType.CHOICES, default=CurrencyType.USD)

    def __str__(self):
        return self.title

    @property
    def total_amount(self):
        return sum(self.billings.all().values_list('total_amount', flat=True))

위의 코드의 경우, 쿼리를 할때마다 total_amount 연산을 수행하게 됩니다.

 

django에서 제공하는 decorator인 cached_property까 존재하는데 처음 호출되었을때 property 함수 결과값을 캐싱해 둔 뒤 그 이후에는 캐싱된 결과를 리턴합니다. 즉, 쿼리를 할때마다 total_amount로직을 수행하는 것이 아닌 해당 메서드를 호출할 때 캐싱된 값을 리턴하게 되어 불필요한 연산을 줄이게 됩니다.

from django.utils.functional import cached_property 


@cached_property 
def total_amount(self):
    return sum(self.billings.all().values_list('total_amount', flat=True))

캐싱된 데이터는 모델 인스턴스가 살아있는 동안만 캐싱되며, 모델인스턴스의 생명이 끝나게 되면 함께 초기화 되게 됩니다. 캐싱된 데이터이기 때문에 모델인스턴스에서 최초 1회가 아닌 연산에 변화가 생기게 되더라도 변화가 적용되지 않기 때문에 이부분을 주의하여 사용한다면, 성능 개선에 굉장한 도움이 될것입니다.

 

 

orm에서 제공하는 몇몇 메서드들만 활용하더라도, 성능 개선을 할수 있음에 많은 도움을 받을 수 있었습니다. 기본적인 orm 메서드 활용 뿐만 아니라 다른 자원을 좀 더 공격적으로 활용해 서버 개선을 할수 있으니 다음 챕터에서는 다른 자원을 활용한 서버 성능 개선에 대해 알아보도록 하겠습니다.

728x90
728x90

docker를 사용해서 프로젝트를 배포하는 방법에 대해 리뷰해보겠습니다.

크게 django, nginx, postgresql 컨테이너를 생성하여 튜토리얼을 진행해보겠습니다.

 

1. django 컨테이너

서버 컨테이너의 경우, docker hub의 오피셜한것이 아닌 직접 빌드를 해야하기 때문에 Dockerfile 작성이 선행되어야합니다.

dockerfile에서는 프로젝트 copy, 패키지 설치 및 collectstatic만 진행합니다. 

# /Dockerfile
FROM python:3.11.4

RUN apt-get -y update
RUN apt-get -y install vim

RUN pip install --upgrade pip

COPY . /server

WORKDIR /server

RUN pip install -r requirements.txt
RUN echo yes | poetry run python manage.py collectstatic

EXPOSE 8000

기존에 만약에 static 디렉터리가 존재할경우, django에서는 overwrite할지의 여부를 묻기 때문에 echo yes를 붙여주게 되었습니다.

--> 보통, aws s3를 사용하기 때문에 대부분 존재할 경우가 있을것입니다.

 

docker-compose.yml

version: '3'

services:
  server:
    build:
      context: .
    command: >
      bash -c "python3 manage.py makemigrations --settings=config.settings.deploy
      && gunicorn --bind 0.0.0.0:8000 config.wsgi.local:application"
    ports:
      - "8000:8000"
    volumes:
      - ./server:/server

migration 과정을 dockerfile이 아닌 docker-compose에서 빌드를 하는 이유는 현재의 경우, postgresql 컨테이너가 뜨기전 미리 docker image 생성 -> postgresql up -> server up의 과정을 거치게 됩니다. 때문에 순서상의 이유로 db가 올라오지 않은 상태로 migration을 진행하게 되어 제대로 빌드가 되질 않습니다.

 

2. postgresql 컨테이너

 

postgres - Official Image | Docker Hub

Note: the description for this image is longer than the Hub length limit of 25000, so has been trimmed. The full description can be found at https://github.com/docker-library/docs/tree/master/postgres/README.md. See also docker/hub-feedback#238 and docker/

hub.docker.com

postgresql의 경우, dockerhub에 있는 이미지를 가져와 컨테이너로 활용하기 때문에 별도의 dockerfile은 필요없이 compose에 업데이트 후 django의 세팅만 수정해주신 후 의존성만 명시해주면 쉽게 해결가능합니다.

 

- docker-compose 파일 수정

version: '3'

services:
  postgres:
    image: postgres
    hostname: postgres
    restart: always
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: "postgres"
      POSTGRES_USER: "postgres"
      POSTGRES_PASSWORD: "postgres"

  server:
    build:
      context: .
    command: >
      bash -c "python3 manage.py makemigrations --settings=config.settings.deploy
      && gunicorn --bind 0.0.0.0:8000 config.wsgi.local:application"
    ports:
      - "8000:8000"
    volumes:
      - ./server:/server
    depends_on:
      - postgres

environment 값의 경우, 만약 실제로 사용하신다면 시스템 환경변수를 이용해 설정해주시기 바랍니다.

 

- django settings.py 변경

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "postgres",
        "HOST": "postgres",
        "USER": "postgres",
        "PASSWORD": "postgres",
        "PORT": 5432,
    }
}

docker-compose파일에 명시해준 내용과 매칭을 하게 된다면 POSTGRES_DB=NAME, POSTGRES_USER=USER, POSTGRES_PASSWORD=PASSWORD, hostname=HOST가 됩니다. 기본적으로 django에서 postgresql을 사용하기 위해선 psycopg2 설치가 선행되어야합니다.

docker에서는 기본적으로 service명=host 입니다. 위의 docker-compose에서 명시적으로 hostname을 명시하기는 했지만 만약 안했어도 기본적으로 postgres로 설정 되게 됩니다.

 

3. nginx 컨테이너

- docker-compose 수정

version: '3'

services:
  postgres:
    image: postgres
    hostname: postgres
    restart: always
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: "postgres"
      POSTGRES_USER: "postgres"
      POSTGRES_PASSWORD: "postgres"

  server:
    build:
      context: .
    command: >
      bash -c "python3 manage.py makemigrations --settings=config.settings.deploy
      && gunicorn --bind 0.0.0.0:8000 config.wsgi.local:application"
    ports:
      - "8000:8000"
    volumes:
      - ./server:/server
    depends_on:
      - postgres
 
  nginx:
    image: nginx
    depends_on:
      - server
    volumes:
      - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf
    ports:
      - "80:80"

- ./nginx/conf.d/default.conf

server {
        listen 80;
        server_name 본인 서버 호스트;
        charset utf-8;

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        location / {
                proxy_pass http://server:80/;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        location /static/ {
                autoindex on;
                alias /static/;
        }
}

nginx의 기본 설정을 바라보는 경로인 /etc/nginx/conf.d/default.conf와 로컬 경로를 docker volume을 활용하여 공유 하기 때문에 미리 위와같은 default.conf 파일이 작업된상태로 존재해야합니다.

nginx의 경우, 서버가 정상적으로 실행된 이후 마지막에 웹서버가 떠있어야하므로 의존성을 server로 설정해주었고 위에서 말했듯이 docker에서는 서비스명 = 호스트가 되므로 proxy_pass를 http://server:80으로 설정해주었습니다.

 

이후엔 이전 과정의 설정이 문제없었다면 아래 명령어를 통해 배포를 진행해주시면 됩니다.

docker compose up -d --build

 

사실 db의 경우, 실제 배포시엔 따로 데이터베이스 호스트가 이미 존재해있을것이기 때문에 요구사항에 맞춰 참고용으로 봐주시면 감사드리겠습니다 :) 

 

도커를 활용하면서 느끼는점이 물론 배포시에도 큰 이점이 있지만 초기 세팅할때 정말 편하게 활용 가능하다는점이 있다고 생각합니다.

최근, 회사에 신규 입사자가 들어오시면서 도커로 빌드가 안되어있던 프로젝트와 되어 있던 프로젝트 세팅을 하며 확실히 그차이를 느낄수 있었습니다. 초기 환경 설정에 있어서 귀찮고 힘들수도 있지만 이것이 나중을 생각했을때 불러올 스노우볼을 생각해보면 정말 좋은 오픈소스 프로젝트라고 생각합니다!!

728x90
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

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
728x90

1. 서버란 무엇인가?

  • 일반인의 관점: 인터넷, 맨날 터지는 짜증나는 곳
  • IT관련직 관점: 서비스가 동작하게 만드는 원천, 데이터가 다뤄지는곳
  • FE 개발자 관점
    • 정해진대로(API) 요청(request)을 보내면, 정대진 대로 응답(response)이 돌아오는 곳
    • 모델링, 데이터베이스는 잘모르겠고 내가 클라이언트 개발하게 받고싶은 데이터 내놓고, 내가 넣으려는 데이터 알맞게 넣어줘!
    • 요구하지만 매번 핑계되며 늦게 주는 곳
  • BE 개발자 관점
    • 효율적으로 클라이언트의 요구에 따라 데이터를 건네주고 저장하는 곳
    • 데이터를 처리하는 곳
    • 부하 분산, 인증, 쿼리 최적화, 보안(CSRF)등 조심해서 다뤄야 하는 예민한 아이
    • 한번의 결정이 추후 확장할때 많은 리소스를 소비할수 있으니 신중히 결정!
    • 알면알수록 점점 발전할수 있는 양파같은 매력적인 아이
    • etc...

사전적 정의 같은건 이미 많이 나와있지만 각자의 관점에 따라서는 위와같다!

 

2. 서버는 어떻게 만드는가?

  • python, java, js, php등 많은 프로그래밍 언어를 통해 직접 서버를 만들수 있지만 각각의 언어엔 python-django, java-spring, js-express, php-lalavel과 같이 유명한 서버 프레임워크가 있다. 이 프레임워크를 활용하여 서버를 만들수 있다.
  • full-featured framework (drf) vs not full-featured framework (express)
    • full-featured framework는 이미 서버 개발에 필요한 대부분의 기능이 구현 되어있어서 crud 구현이 간단하며 그저 이용만 하면되지만 이에 맞게 해당 프레임워크에 대한 공부또한 많이 해야한다.
    • not full-featured framework는 서버 개발에 필요한 최소한의 기능만 주어져있어서 아무래도 crud 구현에 비교적 시간을 할애해야 하겠지만 필요한대로 입맛에 맞게 개발할수 있는 장점이 있다.
    • 즉, drf 잘하려면 공식문서 자세히 들여다 봐야한다!!!

3.ORM 이란?

  • Object Relation Mapping
  • SQL(Structured Query Language) 데이터베이스 언어를 python의 환경에 맞게 객체로 쉽게 가져올수 있도록 하는것
  • Table -> Class, Column -> Property, Row -> Instance
#SQL
SELECT * FROM user WHERE ~~

#ORM
User.objects.filter(~~)
  • SQL문에서도 마찬가지지만 drf의 ORM에서는 쿼리를 가져올때 최적화하는 작업이 중요하다!
  • 캐싱, transaction활용, selected_related, prefetch_related등을 활용하여 최적화 작업을 진행한다

4. RESTAPI란?

 

프로젝트(1)프론트, 백엔드 통신 방법(feat.django, react)

새롭게 프로젝트를 시작하며 개발전 1주일간 3파트로 프로젝트 사전 준비를 하였다. (1) 프론트, 백엔드 통신방법 (2) 기술스택 선정, 협업 노하우 (3) token, django user, 소셜로그인 위의 3가지를 사전

leeceo97.tistory.com

위글에 정리를 해뒀다

  • 한마디로, METHOD로 행동을 구분하고, URL에 가져올 자원과 이를 식별할 기준을 나타내며 행동에 대한 응답은 상태코드와 메시지로 응답하는 방식이다.
request  GET(method) https://corin2.com/category(가져올 자원)/:id(식별자)
response  200(행동에 대한 상태코드)  OK(메시지) {"Data": "Oh My God"} (데이터)

5. drf에서의 서버 동작은?

  • request -> middleware -> router -> parser -> viewset(view) -> permission -> serializer -> model -> DB -> model -> serializer -> viewset(view) -> renderer -> response의 과정을 거친다
  • request: 말그대로 클라이언트가 서버로 요청을 보내는과정
  • middleware: 요청에 대한 전/후처리를 하고 싶은것이 있다면 미리 만들어 둔다(ex.인증)
  • router: urls.py 요청을 view로 연결해줌
  • parser: request의 content-type에따라 request data/ request FILES등을 처리(ex. iAmBabo-> i_am_babo)
  • viewset(view): 정확하게 말하면 viewset=view의 집합이며 요청에 따른 알맞은 로직을 수행하는 곳
  • permission: has_permission은 view요청이 들어오기전, has_object_permission은 view요청후 인증 확인
  • serializer: model 직렬화/역직렬화
  • renderer: parser와 반대되는 개념
  • response: request와 반대로 서버가 클라이언트의 요청에 따른 응답을 보내는 과정
728x90

+ Recent posts