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

그동안 꿈꿔왔던 백엔드 개발자가 8월 1일부로 되었다!

 

사실 자세하게 말하면 원래는 6월 27일부터 다른 회사의 서버개발자로 들어가긴했지만ㅎ... 여러 사정으로인해 첫회사를 다닌지 15일? 만에 조금은 두려워도 퇴사를 마음먹고 다시 새롭게 출발하게 되었다.

 

간단히 말하자면 퇴사이유내가 생각했던 신입이 성장하기 좋은 회사가 아니라고 생각이 들었기 때문이다. 당장 1년뒤의 모습을 생각했을때 더 성장하기 좋은 회사로 빠르게 옮기는 것이 좋다고 생각되어 두려웠지만.. 퇴사를 했고 다시 취준을 하며 다행스럽게 합격하게 되었다ㅎㅎ

 

1,2차 취준간 총 50군데 정도 지원한것 같고 절반정도의 면접 기회 최종합격은 8곳 되었다. 이래저래 자존감도 많이 내려가고 퇴사후 이직이다보니 불안함도 많았는데 주위의 친구들 그리고 함께 공부해주는 일명 GODJANG팀원들 덕분에 견딜수 있었다!!

 

다행히? 회사에서는 내가 좋아하는 django프레임워크를 사용하고 있어서 당장의 코드 파악은 어렵지 않은데 스타트업 규모다 보니 데이터를 정제해서 bi툴도 만들고 인프라까지 관리를 해야할것 같아 이래저래 배울건 많지만 그래도 너무 만족하고 근무중이다. 무엇보다 같이 일하는 개발자 분들 모두 5년차~20년차까지 다양한 경력을 가지고 계셔서 배울점도 많다.(하지만 질문드리기 부담스럽...)

 

당장은 매번 orm만 사용하다 보니 잊어버린 sql문을 부랴부랴 다시 공부하고 대시보드를 제작하고 있어 바쁜 하루하루 지내고 있지만 다음주 새로운 기능 마이그레이션 회의에 참가할수 있는 기회가 생겨서 설레는 맘으로 자진해서 주말에도 코드리뷰 진행중이다ㅎㅎ

 

최근 큰 변화라면 주말에 따로 약속 없으면 그냥 카페에 노트북 들고가서 사이드 프로젝트 만지작만지작 했는데 요즘엔 그냥 회사로 출근해서 회사에서 작업하는 변화가 생겼고 회사끝나고 칼퇴보단 퇴근후엔 개인공부를 회사에서 하고 있어서 집 -> 회사 -> 운동 이패턴이 자리잡아 오히려 건강해진거 같은느낌이다. +) 7년간 못해오던 금연도 성공 했다!

 

대학교 3학년때까지는 컴공이지만 개발자가 될것은 생각도 안했고 영상, 영업, pd가 되지 않을까? 했는데 어찌저찌 열심히 다시 하니 개발자가 된것에 매우 만족한다ㅎㅎ 인턴 종료후 7개월만에 새로운 회사의 신입으로써 일원이 됐으니 인턴 처음 시작당시 먹었던 마음인 "회사에 실질적으로 도움이 되는 인재가 되자!"라는 마인드를 다시 갖추고 일할것이다~

728x90
728x90

올해 2월말 부터 드디어 연합 개발 동아리를 할수 있게 되었다!!

아무런 준비가 안된상태로 지원만 하다가 매번 떨어지기 일쑤였는데 어느정도 프로젝트를 할수 있겠다!라고 생각될때 지원을 하게 되었고 다행히 합격 하게 되었다!!


리쿠르팅 절차는 서류 -> 과제 -> 면접의 과정을 거쳤고 3주정도 소요된것 같다.

 

과제를 각자 지원한 node, django, react, ios, android, design파트에 맞게 서류를 통과하면 과제가 제시되었고, 나는 당연히 django 과제를 받았고 todolist를 만드는것이였는데 단순히 CRUD구현이 아니라 여러 생각해볼만한 조건에 맞게 API를 설계하는 과제였다. 그동안 생각지 못했던 조건이 있어서 과제를 하면서도 공부를 할수 있는 좋은 과제였다.

 

면접같은경우, cs나 알고리즘 자료구조와 같은 지식을 확인하는 것이 아닌 제출한 과제에 대한 코드리뷰와 함께 인성면접?을 봤는데 비교적 편안한 분위기에서 볼수 있었다. 나중에 들어보니 여기는 회사 개발자를 채용하는 곳이 아닌 함께 프로젝트를 진행하며 매끄럽게 협업을 진행할수 있는 사람을 뽑는 자리이기 때문에 이런 분위기의 면접을 봤다고 했다!


아직 프로젝트 개발에 들어가지는 못했지만 그래도 1달정도 프로그라피를 하며 정말 좋은점이라고 느꼈던것은

1. django에 대해서 궁금한점을 질문하고 내코드의 개선점을 피드백해주시는 멘토가 생겼다.

프로그라피에 지원한 가장큰 이유중하나가 아무래도 node나 java기반의 개발자분들이 많다보니 내가 짜고 있는 코드가 현업에서 활용되는건지? 필드명은 맞게 짓는건지? 구현을 이렇게 하는것이 최선인지?등 모든것이 불확실했는데 1달여간의 세션을 장고 운영진 분이 해주셔서 정말 많이 배우고 그동안 내가 짯던 코드들의 허점을 많이 느낄수 있게 되었다. 정말 생각을 하며 개발을 하는 방법이 먼지 절실하게 배우고 있다.

2. IT업계에 종사하는 현업자분들과의 소통의 장을 얻을수 있게 되었다.

프로그라피에는 대학생들도 많지만 많은 분들이 현업에서 일하고 계시는 분들이 많다. 특히, 우리의 멘토가 되어주시는 운영진분들은 대부분 현업자분들이신데 개발 지식뿐만 아니라 다른 분야의 동료들과 소통을 할때 어떻게 해야 상대방이 기분이 나쁘지 않게 말할 수 있는지 협업은 어떤식으로 진행해야 하는지를 느끼게 되었다. 사실 지금까지 내가 협업을 할때는 대부분 회의의 준비부터 결과까지 내가 직접 다운영을 하고 대부분 내의견에 따라서 진행된 경험이 많았기 때문에 이부분은 따로 생각할 기회가 없었는데 덕분에 다른사람과의 협업을 어떻게 해야하는지? 많이 배우고 있는것 같다.

3. 기획을 폭넓게 생각할수 있게 되었다.

아무래도 포트폴리오 목적의 프로젝트를 많이 진행하고 별생각없이 항상 커뮤니티 위주의 프로젝트를 기획을하게 되었고 당연히 어플이라면 커뮤니티성 프로젝트가 인기가 많을거라고 생각했다. 하지만 이번에 기획을 뒤집게 될 상황이 생겼는데 도움을 주러오신 멘토 분께서 실질적으로 사용할 유저들의 폭을 꼭 한국의 유저 뿐만이 아닌 그리고 커뮤니티 앱이 아니더라도 간단한 유틸성 앱또한 사람들이 많이 사용한다는 것을 설명해주셨다. 항상 유저끼리의 소통이라는 곳에만 꽂혀 있던 나에게 약간은 충격이 된 기회였다. 아직 3개월여간 진행할 프로젝트의 기획을 확정짓지 못해 고민이 많긴 하지만 정말 유저가 사용을 할 매력있는 어플을 기획하고 개발에 진지하게 임할 생각이다.

 

암튼, 대학교 졸업전에 it연합동아리를 꼭 경험해보고 싶었는데 이런 기회가 생겨서 너무 좋고 처음 마음 먹었던 것처럼 한기수를 끝낼때 보람찰수 있도록 미루지말고 열심히 공부하고 참여해야 겠다~

728x90
728x90

프로젝트를 완성한후 좀더 발전시킬방법에 대해 학습중이다. 기능 관련해서는 물론 좀더 발전시킬부분이 있지만 내가 경험해보지 않은 부분은 어딜까? 생각하다 요즘은 선택이 아닌 거의 필수로 여겨지고 있는 TDD에 대해 알아보고 적용해보게 되었다.

 

TDD란?

테스트 주도개발(Test Driven Development)은 일종의 개발 방식 또는 개발 패턴을 말한다. 무언가를 개발할 때 바로 개발부터 하는 것이 아니라 개발하려는 항목에 대한 점검 사항을 테스트코드로 만들고 그 테스트를 통과시키는 방식으로 개발을 진행하는 방법이다.

 

-TDD 이전의 테스트 방법

API서버를 만들때 항상 그때그때마다 POSTMAN으로 API에 JSON형식으로 데이터를 전송하고 OK되면 바로 서버에 적용 하는 방법으로 테스트를 진행했다. 사실, 개인적 혹은 소규모의 팀프로젝트를 진행하는 나의 입장에서는 TDD가 그렇게 와닿지도 필요하지도 않았다. 그래서 테스트코드 작성이라는 귀찮은 개념을 다시학습하고 적용할 필요가 없었다.

 

하지만, 프로젝트 규모가 좀더 커지고 API가 다양해진다면? 말이 달라진다. 매번 POSTMAN으로 일일이 확인을 해야하고 복잡하게 얽혀있는 비즈니스 로직에 대해 하나하나 고려해가며 테스트 하기는 물론 쉽지 않고 누락하기 쉽다. 때문에 테스트코드를 작성함으로써 이런실수를 미연에 방지하고 추후 나아가 이걸기반으로 배포 자동화 즉, CI/CD를 적용할때의 기준점을 잡고 진행할수 있다. 물론, 다수의 개발자가 협업하는 회사에서의 TDD는 결국 필수라고 생각한다.

 

DRF에서의 TDD

사실 방법은 다양하다. 이미 파이썬 자체에서 제공하는 라이브러리도 있으며, 그밖의 다양한 방식또한 있겠지만 django라는 완벽한 프레임워크에는 자체적으로 test할수 있는 방법이있다. 매번 python manage.py startapp ~~를 통해 생성되는 test.py가 바로 그방법중 하나이다.

python manage.py test

Found 0 test(s).
System check identified no issues (0 silenced).

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

test.py를 전혀 건드리지 않고 위의 명령어를 입력한다면 당연히 아무런 에러가 뜨지 않는다.

하지만, 기존에 작성된 api에 잘못된 형식으로 데이터를 전송한다면? PTSD오는 수많은 에러창이 나온다.

 

대부분의 협업은 drf를 통해 api를 생성한후 클라이언트가 api를 이용하기 때문에 postman에서 테스트 하는것과 같은 방법으로 테스트 코드를 작성한다.

from django.test import TestCase
from rest_framework.test import APIRequestFactory

factory = APIRequestFactory()
request = factory.post('/api/board/', {
                    "title":"asdasdasd",
                    "category":"question",
                    "content":"asdasdasdasd",
                    "user":1,
                    "stack":["python", "java"],
                    "worker":["기획자","디자이너"]
                })

위와 같은 방식으로 테스트 코드를 작성한후 python manage.py test를 통해 테스트 통과유무를 확인하고 에러가 없는걸 확인 했을때 배포하는 식으로 진행하는 방법이다.

 

api 서버를 개발하는 우리 입장에서는 각각의 api에 적합한 데이터를 넣었을때 데이터가 제대로 저장되는지 확인을 하면 되는 부분이므로 아래의 공식문서의 내용을 통해 인증을 포함한 여러 테스트 코드를 작성하면 된다.

 

Testing - Django REST framework

 

www.django-rest-framework.org

TDD라는 말만 들어봤지 실제로 어떻게 작성해야하고 무엇을 테스트 해야하는걸까?라는걸 모르고 있었던 나에겐 사실 꼭필요했던 정보라 짧게나마 기록을 해두게 되었다ㅎㅎ..

728x90

+ Recent posts