REST API가 뭔가요?
라고 물으신다면
https://thankyou-ddabong-dochi.tistory.com/51
REST API 장고로 구현하기 - 1 (웹 기본지식)
API 란? Application Programming Interface 어플리케이션이 어떤 프로그램이 제공하는 기능을 사용할 수 있도록 연결하는 매개체 라고 하는데 잘 이해가 안된다. 대충 프로그램과 프로그램을 연결하는 통
thankyou-ddabong-dochi.tistory.com
예전에 써놓은 글이 있으니까 참고하시기 바랍니다.
아무튼 저 글 이후에 쓰던 글에서 만들던 칵테일 레시피 알려주는 사이트의 백엔드 api의 코드를 리팩토링하고 또 신기능을 추가해야 하는 상황이 생겼다.
저 글 쓰던 시절은 그냥 만들면서 배우자 라던 시절이었고, 또 웹사이트의 기능들이 떠오를때마다 즉시 새롭게 추가해주어야 상황이었기 때문에 지금와서 코드를 다시 읽어봤더니 아주 개판이었다.
오랫동안 웹 개발 공부를 놨었기 때문에 다시 재활치료도 할 겸 이 api 코드를 리팩토링하기로 결정했다.
1. 주요 기능 기획
다음과 같은 기능들이 필요하고 1~4는 이미 구현된 기능, 5,6은 앞으로 구현해야 할 기능이다.
1~4까지의 기능이 구현이 됐지만 아까 말했듯 코드 리팩토링 또한 해야했다.
2. DB Schema 설계
GitMind - 무료 온라인 마인드 맵 도구
GitMind는 브레인 스토밍, 프로젝트 계획, 개발, 행동 및 기타 창의적인 작업을위한 무료 온라인 마인드 맵 메이커입니다. 이 도구를 사용하면 수많은 사용자와 마인드 맵을 공유하고 공동 작업 할
gitmind.com
구현해야 할 기능을 기획했으므로 GitMind를 이용해 DB Schema를 시각화해봤다.
물론 기획한 내용을 그대로 구현하진 않았지만 변수 이름 정도만 바뀌고 큰 구조는 동일하게 구현했다.
사실 db구조는 이미 만들어진 구조에서 몇가지만 수정해서 사용했다.
가장 큰 차이점은 JSONField의 삭제다.
예전 api에서 칵테일 재료와 양을 표현할때
{"잭 다니엘스" : 30.0}
과 같은 식으로 JSON을 보내고 받아야 했다.
이 과정에서 레시피 테이블에 join한 후 잭 다니엘스라는 재료 이름과, 30.0ml라는 재료양을 찾은 후
다시 dict형태로 직렬화 하고 주고 받는게 귀찮아서 json필드로 통채로 만들어서 저장하고 조회했다.
https://vixxcode.tistory.com/229
[Database][Postgresql] JSONField VS JOIN 에 대한 성능 체크(jsonb vs table)
공부하면서 json field을 쓰는 것을 보고 과연 join과 비교했을 때 얼마나 성능에 대한 이점이 있을까 고민을 하다가 한 번 실험해보기로 했습니다. framework: django database: postgresql 13 python version : 3.9 co
vixxcode.tistory.com
하지만 조회 과정에선 큰 성능 차이를 보이지 못했고 데이터 수정시 상당히 로직이 더 복잡해지는 결과를 초래해
그냥 Forigen Key를 이용해 두 테이블을 연결하는 방식으로 전환했다.
3. api 명세 기획
다음으론 API 명세서를 기획했다.
원래는 API 명세를 먼저 짜고 그에 맞춰서 개발을 하는 게 프론트와의 협업 과정에서 좋다.
그리고 이미 기존의 api 명세가 존재했다.
하지만 프론트도 코드 개편이 필요한 상황이므로 api 명세를 내 입맛대로 수정해도 된다고 OK 사인을 받았다.
사실 그래서 코드를 먼저 짜버리고 api 명세를 내가 편한대로 나중에 구현했는데
이게 상당히 큰 실수였다. (추후 설명 예정)
앞으론 시간을 좀 들여서라도 API 명세를 신중히 기획하고 거기에 맞춰서 구현하자.
4. 코드 구현
1) Models.py
from django.db import models
class Glass(models.Model): # 서빙 글라스 모델
name = models.CharField(primary_key = True,max_length=255)
def __str__(self):
return self.name
class Cocktail(models.Model): # 칵테일 레시피 모델
name = models.CharField(primary_key = True,max_length=255)
recipe = models.TextField()
img_url = models.URLField()
glass = models.ForeignKey(Glass, on_delete=models.CASCADE, null=True, related_name = 'cocktail')
def __str__(self):
return self.name
class Base(models.Model): # Base 재료 모델
name = models.CharField(primary_key = True,max_length=255)
alcohol_degree = models.FloatField(default = 0.0)
cocktail = models.ManyToManyField(Cocktail,through = 'CocktailBase',related_name = 'base')
class CocktailBase(models.Model): # Cocktail - Base 중간 테이블 모델
cocktail = models.ForeignKey(Cocktail, on_delete=models.CASCADE, related_name = 'cocktail_base')
name = models.ForeignKey(Base, on_delete=models.CASCADE, related_name = 'cocktail_base') # base-name
amount = models.FloatField(default = 0.0)
# ...이하 생략 ...
Cocktail 모델에 Glass 와 Base 모델이 연결된 형태다.
이때 Cocktail - Glass 모델은 1:N 관계 이고, Cocktail - Base 모델은 M:N 관계이다.
장고에선 두 관계를 models.ForeignKey, 와 models.ManyToManyField를 이용해 구현한다.
구현할때 까다로웠던 점은 무슨 Base 재료가 들어가는지 뿐 아니라 해당 재료가 그 칵테일에서 몇 ml 정도 들어가는지도 조회할 수 있어야 한다.
블루 하와이완과 블루 사파이어를 예로 들어보자.
두 칵테일 모두 "말리부" 라는 재료가 들어간다.
하지만 블루 하와이안에는 말리부가 15ml, 사파이어에는 30ml가 들어간다고 가정해보자
이때 Cocktail의 "블루 하와이안" 이란 모델과 Base 의 "말리부" 라는 모델을 연결하고 또 "블루 사파이어"와 "말리부"를 연결한다.
그렇다면 15ml와 30ml라는 재료양의 정보는 어느 테이블에 저장하는것이 좋을까?
이렇게 두 모델을 연결하면서 추가적인 정보를 저장해야 할때는 중간 테이블 모델을 만들어 사용하면 된다.
CocktailBase라는 모델을 만든 후 다음과 같이 through 옵션을 이용해 사용하면 된다.
cocktail = models.ManyToManyField(Cocktail,through = 'CocktailBase',related_name = 'base')
2) serializers.py 구현
serializer가 뭔가요?
라고 물으신다면
이 글을 읽고 오시는 걸 추천합니다.
https://thankyou-ddabong-dochi.tistory.com/53
REST API 장고로 구현하기 - 3 (연습 프로젝트 구현)
개발환경 Mac + Visual Studio Code + Python 3.10.5 django REST framework를 사용해 구현할 때 핵심은 Serializer 이다. django 내부의 복잡한 로직을 Serializer가 간단하게 구현해준다. 백엔드 API를 구현할 때 구현한
thankyou-ddabong-dochi.tistory.com
class BaseSerializer(serializers.ModelSerializer):
class Meta:
model = Base
fields = ('name','alcohol_degree')
class CocktailBaseSerializer(serializers.ModelSerializer):
alcohol_degree = serializers.SerializerMethodField(read_only = True)
class Meta:
model = CocktailBase
fields = ('name','amount','alcohol_degree')
def get_alcohol_degree(self,obj):
return obj.name.alcohol_degree
class CocktailSerializer(serializers.ModelSerializer):
glass = serializers.StringRelatedField(read_only = True)
base = CocktailBaseSerializer(many = True,read_only = True, source ='cocktail_base')
sub = CocktailSubSerializer(many = True, read_only = True, source = 'cocktail_sub')
juice = CocktailJuiceSerializer(many = True,read_only = True, source = 'cocktail_juice')
other = CocktailOtherSerializer(many = True,read_only = True, source ='cocktail_other')
class Meta:
model = Cocktail
fields = ('name','base','sub','juice','other','recipe','img_url','glass')
다음과 같은 방식으로 serializer를 구현했다.
DRF는 ManyToManyField를 직렬화하는 serializer를 구현할때
대부분 읽기 전용으로만 구현이 가능하고 생성시엔 create함수를 오버라이딩 하거나 views에서 직접 모델을 건드려야 한다.
그래서 나는 후자 방식을 선택했다.
CocktailSerializer의 필드를 보면 대부분 필드에 read_only = True로 설정되어 있는 것을 볼 수 있다.
그리고 읽을 때는 마찬가지로 중간 테이블의 정보를 같이 읽어야 하므로 CocktailBaseSerializer을 같이 만들어야 한다.
(이 방식 외에도 다른 방식이 있는데 DRF 공식 문서의 NestedSerializer 부분을 읽고 오는 것을 추천한다.)
가장 중요한 것은 source = 'cocktail_base'(중간 테이블 모델의 related_name 부분) 를 입력해주지 않으면
Serializer가 자동으로 중간테이블을 건너 뛰고 바로 Base 모델을 가져온다.
3) views.py
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from .models import *
from .serializers import *
def check_cocktail_data(model,query_set): #유효성 검사 함수 - 재료가 존재하는지 확인
for data in query_set:
if not model.objects.filter(name = data['name']).exists():
return False
return True
# 중간 테이블에 칵테일과 재료 연결하는 함수
def create_mapping_table(mapping_table_model,query_set,cocktail,ingredient_model):
for data in query_set:
name = ingredient_model.objects.get(name = data['name'])
mapping_table_model.objects.create(cocktail = cocktail, name = name, amount = data['amount'])
@csrf_exempt
def cocktails(request):
if request.method == 'GET': # 칵테일 레시피 전체 요청
cocktails = Cocktail.objects.all()
serializer = CocktailSerializer(cocktails, many=True)
return JsonResponse(serializer.data,safe = False)
elif request.method == 'POST': # 칵테일 레시피 등록
data = JSONParser().parse(request)
base_data = data.pop('base') #외래키 등록 필요한 항목들
sub_data = data.pop('sub')
juice_data = data.pop('juice')
other_data = data.pop('other')
glass_data = data.pop('glass')
serializer = CocktailSerializer(data = data)
# ---유효성 검사---
if (serializer.is_valid()
and Glass.objects.filter(name = glass_data).exists()
and check_cocktail_data(Base,base_data)
and check_cocktail_data(Sub,sub_data)
and check_cocktail_data(Juice,juice_data)
and check_cocktail_data(Other,other_data)):
# --- 유효성 검사 통과 ---
cocktail = serializer.save()
glass = get_object_or_404(Glass, name = glass_data) #Glass 연결
cocktail.glass = glass
# --- 중간 테이블 연결 ---
create_mapping_table(CocktailBase,base_data,cocktail,Base)
create_mapping_table(CocktailSub,sub_data,cocktail,Sub)
create_mapping_table(CocktailJuice,juice_data,cocktail,Juice)
create_mapping_table(CocktailOther,other_data,cocktail,Other)
cocktail.save()
return JsonResponse(serializer.data, status=status.HTTP_201_CREATED)
# error_data = {"error_code":400, "error_message":f"invalid data"}
return JsonResponse(serializer.errors, status = 400)
ManyToManyField 연결 을 views.py에서 진행했다.
POST method와 함께 request body로 데이터가 들어오면 올바른 데이터인지 유효성 검사를 진행한 후
외래키 테이블을 제외한 정보들(칵테일 이름, 조합법, 이미지 등)은 serializer을 통해 만들고
create_mapping_table함수를 통해 중간 테이블을 하나씩 연결해주는 방식이다.
5. 마무리하며
사실 일주일이라는 시간동안 리팩토링을 진행했는데 5일 정도는 시간을 날리기만 하고 진짜 개발은 마지막 2일에 다 한 것 같다.
평일에 컴퓨터를 할 수 있는 시간이 적어서 그런것도 같지만 그냥 api 명세를 제대로 안 짜고 맘대로 만들다가
이게 더 좋을 거 같은데..? 하고 다시 만들고.. 만들다가 막혀서 검색해봤더니 더 좋은 방식이 생각나서 또 갈아엎고..
이 과정을 반복하는데 5일을 썼다.
물론 그 과정에서 잊어버렸던 python 문법과 django, git, 노션 사용법, 에 관해서 다시 기억해낼 수 있는 시간이었다.
다음주 할 일
- 프론트와 상의 후 error_message 및 예외처리 구현
- 수정된 api 명세 검토
- 수정사항 main branch merge
- Hashtag, 및 좋아요 기능 구현 준비
'장고는 못말려' 카테고리의 다른 글
사지방에서 웹 백엔드 공부하기 #4 git, github 관리하기 (0) | 2023.02.27 |
---|---|
사지방에서 웹 백엔드 공부하기 #3 CRON 사용법 (2) | 2023.02.20 |
사지방에서 웹 백엔드 공부하기 #1 개발환경 설정 (Goorm IDE) (2) | 2023.02.11 |
장고로 REST API 구현하기 - 4 (Serializer 응용) (3) | 2022.07.01 |
REST API 장고로 구현하기 - 3 (연습 프로젝트 구현) (2) | 2022.06.22 |