[빌리지 프로젝트] DB 수정 및 투표 기능 구현

wodnr_P·2023년 8월 13일
0

오늘은 기본적인 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라는 리스트에 클라이언트에서 요청한 투표 항목 수 만큼 반복문을 활용하여 투표 항목을 생성하도록 로직을 구성했습니다.

📌 DB 설계 변경

다음 순서는 클라이언트 화면에서 '투표하기'를 눌렀을 때 필요한 투표하기 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 트랜잭션의 문제가 발생할 것 같아서 열심히 찾아보니 트랜잭션 데코레이터를 알게 되었습니다.

투표 기능을 어떻게 비동기적으로 구현 해볼 것이냐는 고민은 계속해서 하는 중 입니다. 다만, 어떤 방법이 더 효율적인지는 잘 모르겠어서 여러가지 시도를 해봐야겠습니다.

조금 더 효율적인 방법, 다른 생각을 가지신 분들이 있으시다면 언제나 질문, 댓글 환영입니다!

profile
발전하는 꿈나무 개발자 / 취준생

0개의 댓글