오늘은 기본적인 jwt를 활용한 사용자 로직 구현과 공지 게시판 로직, 댓글 관련 API의 구현을 마무리 하고, 공지 게시판에 투표 기능을 구현하기 위해 고민을 해보았습니다.
투표 기능에 대해 고민한 결과 핵심을 다음과 같이 정리했습니다.
- 투표 생성시 Vote 모델에서는 하나의 투표 테이블과 Choice 모델에서는 N개의 테이블이 생성 및 저장 되어야 한다.
- 투표는 중복 투표를 방지해야 하고, 사용자가 항목을 선택할 때 마다 Choice의 count가 증가해야 한다.
다음은 이를 바탕으로 작성한 투표 생성 로직 입니다.
class QuestionAPIView(APIView):
# 투표 생성
def post(self, request, board_id):
board = get_object_or_404(Board, pk=board_id)
vote = Vote.objects.create(board=board, title=request.data["title"])
# 선택지 생성
choice_data = request.data.get('choices', [])
if not choice_data:
return Response({'Message': '선택지를 입력해주세요.'}, status=status.HTTP_400_BAD_REQUEST)
# 요청된 투표 항목 수 만큼 choices에 담아서 저장
choices = []
for data in choice_data:
choice = Choice.objects.create(content=data.get('content', ''), vote=vote)
choices.append(choice)
return Response({'Message': 'Success'}, status=status.HTTP_201_CREATED)
해당 게시글에 하나의 투표를 생성하기 위해 게시글 id 기준으로 투표 모델을 생성했습니다.
그리고 투표 항목이 없는 경우를 대비해 선택지를 입력해 달라는 에러 처리를 하고, choices라는 리스트에 클라이언트에서 요청한 투표 항목 수 만큼 반복문을 활용하여 투표 항목을 생성하도록 로직을 구성했습니다.
다음 순서는 클라이언트 화면에서 '투표하기'를 눌렀을 때 필요한 투표하기 API를 구성하려 했습니다.
하지만 현재 DB 모델에서는 [투표 - 사용자]간의 관계를 정의하지 않았다는 것을 알게 되었고, 여러명의 사용자는 여러 투표 항목을 선택할 수 있고 투표 항목도 여러명의 사용자에게 선택 받을 수 있기 때문에 투표 항목과사용자간의 N:M 관계를 추가 해주었습니다.
변경된 DB 모델과 ERD는 다음과 같습니다.
# vote/models.py
from django.db import models
from board.models import Board
from user.models import CustomAbstractBaseUser
# Create your models here.
class Vote(models.Model):
title = models.CharField(max_length=100)
create_time = models.DateTimeField(auto_now_add=True)
board = models.OneToOneField(Board, on_delete=models.CASCADE)
class Choice(models.Model):
count = models.IntegerField(default=0)
content = models.CharField(max_length=100)
vote = models.ForeignKey(Vote, on_delete=models.CASCADE)
voter = models.ManyToManyField(CustomAbstractBaseUser, related_name='voter', blank=True)
# voter라는 필드를 추가하여 사용자와 투표항목 간 다대다 관계를 추가
USer 모델과 Choice 모델간 N:M 관계가 추가 된 것을 확인 할 수 있습니다.
DB모델을 수정 한 후, 로직은 다음과 같이 구성했습니다.
# vote/views.py
from django.db import transaction
class VoteAPIView(APIView):
# 투표 하기
@transaction.atomic
def post(self, request, board_id, choice_id):
auth = get_authorization_header(request).split()
if auth and len(auth) == 2:
board = get_object_or_404(Board, pk=board_id)
vote = Vote.objects.get_or_create(board=board)[0]
choice = get_object_or_404(Choice, pk=choice_id, vote=vote)
# 중복 투표 방지
user = decode_access_token(auth[1])
if choice.voter.filter(pk=user).exists():
return Response({'Message': '이미 투표하셨습니다.'}, status=status.HTTP_400_BAD_REQUEST)
# 투표 수 증가
choice.count += 1
choice.voter.add(user)
choice.save()
return Response({'Message': 'Success'}, status=status.HTTP_201_CREATED)
@transaction.atomic은 django에서 지원하는 트랜잭션 데코레이터 입니다.
- 데이터베이스 트랜잭션에서 모든 작업을 실행하거나 모두 롤백하는 것처럼, 한번에 모든 작업이 수행되는 것이 원자성(atomic)이므로 이를 활용하면 데이터베이스 관련 오류에 대한 예외 처리나 중단 등의 문제를 방지할 수 있습니다.
❓why
예를 들어 여러 사용자가 동시에 하나의 투표를 제출한다고 가정 했을 때, 여러 사용자가 동시에 같은 데이터를 변경하기 때문에 데이터 충돌이 발생할 수 있습니다.
이를 방지하기 위해 하나의 트랜잭션으로 DB 작업을 수행해야 하므로
@transaction.atomic
을 사용 했습니다.
로직의 구성은 먼저 사용자가 토큰으로 인증된 상태인지 확인 후 게시글에 해당하는 투표 정보를 불러 옵니다.
그리고 토큰에 저장된 사용자 id를 decoding
하여 해당 사용자가 투표를 한 사용자인지 확인하고, 투표를 하지 않았다면 투표 항목에 대한 선택 수 를 증가 시키고, 투표한 사용자에 추가한 후 저장하게 됩니다.
해당 투표 기능은 비동기적으로 구현을 해볼 수 있을 것 같아서 조금 더 고민해보고 변경 해봐야겠습니다.
ORM으로 여러 개의 DB를 조회하고 생성하는 과정에서 DB 트랜잭션의 문제가 발생할 것 같아서 열심히 찾아보니 트랜잭션 데코레이터를 알게 되었습니다.
투표 기능을 어떻게 비동기적으로 구현 해볼 것이냐는 고민은 계속해서 하는 중 입니다. 다만, 어떤 방법이 더 효율적인지는 잘 모르겠어서 여러가지 시도를 해봐야겠습니다.
조금 더 효율적인 방법, 다른 생각을 가지신 분들이 있으시다면 언제나 질문, 댓글 환영입니다!