코인 가상 거래 만들어보기! (5) 모델 구조 개편 및 비즈니스 로직 구현

HEYDAY7·2021년 5월 23일
0
post-thumbnail

우선 모델 구조 개편에 앞서, 작성했던 코드에서 수정이 필요한 부분을 수정한다.

settings.py 추가

로그인 세션 관리 관련 코드가 빠져있어 추가한다.

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
    )
}

들어가며

여기부터가 이번 글 작성 내용의 시작이다.
만들 구조는 Transaction과 CoinHolding 두 개의 모델 조작을 통해서 거래를 발생/기록하고, 이에 따라 개인이 보유한 Coin의 양을 변경하게 된다.

Transaction model

Transaction model의 경우 거래의 내용을 담게 된다. 작성은 account app안에서 했고, 코드는 아래와 같다.

# account/models.py

    balance = models.PositiveIntegerField(default=100000000)


class Transaction(models.Model):
    account = models.ForeignKey(
        Account,
        on_delete=models.CASCADE,
        related_name='transactions',
        related_query_name='transaction'
    )
    type = models.CharField(max_length=10, default='buy')
    coin = models.ForeignKey('coin.Coin', on_delete=models.CASCADE)
    price = models.FloatField()
    total_price = models.FloatField()
    coin_amount = models.FloatField()

우선 맨 윗줄은 기존에 존재하는 Account 모델에서 저 부분을 수정해주자. 만들어졌을 때 기본으로 1억을 부여하도록 수정하는 것이다.

Transaction 모델은 account를 foreign key로 받고 Account 1:N Transaction의 관계가 된다. 다음 fields 설명은 아래와 같다.

  • type : sell or buy
  • coin : 거래 coin 종류 -> Coin model foreign key
  • price : 거래 당시 가격
  • total_price : 총 거래 가 (price * coin_amount)
  • coin_amount : 거래하는 coin 양

모두 직관적으로 이해할 수 있는 field들이다. 거래내역이라고 생각하면 편한다.


CoinHolding Model

다음은 CoinHolding Model이다. 이는 코인의 보유량을 나타낼 수 있는 모델이다.

#coin/models.py

class CoinHolding(models.Model):
    account = models.ForeignKey(
        'account.Account', 
        on_delete=models.CASCADE,
        related_name='coinholdings',
        related_query_name='coinholding')
    price = models.FloatField()
    total_price = models.FloatField()
    coin_amount = models.FloatField()
    coin = models.ForeignKey(Coin, on_delete=models.CASCADE)

coin/models에 위 model 코드를 추가한다.

  • account : foreign key field이다. 한 계좌가 coin을 보유하게 되는 것이므로 account와 연결해준다.
  • price : 매수평균단가가 된다.
  • total_price : 매수에 사용한 총 금액
  • coin_amount : 보유 coin 양
  • coin : foreign key field로 코인의 종류를 나타냄

여기까지는 단순 모델 생성이므로 간단하다.


Account serializer 수정

기존에 존재하던 Account serializer를 수정한다.

AccountSerializer

#account/serializers.py
class AccountSerializer(serializers.ModelSerializer):
	user = UserSerializer(required=True)
	balance = serializers.IntegerField(read_only=True)
	transactions = TransactionSerializer(many=True, read_only=True)
	coinholdings = CoinHoldingSerializer(many=True, read_only=True)

	class Meta:
		model = Account
		fields = ['user', 'balance']
		fields = ['user', 'balance', 'transactions', 'coinholdings']

	def validate(self, data):
		user_data = data['user']

수정된 점

  • balance : read_only field로 바꿔 생성시 주입되지 않고 그냥 생성시 1억을 갖게 된다.
  • transactions, coinholdings : (1:N) 관계인 두 모델의 추가에 따라서 해당 모델과의 관계를 나타내주기 위해 추가한다.

business flow

이 부분에 들어가기 위해서는 먼저 로직의 이해가 우선이다. transaction이 만들어지는 flow는 다음과 같다.
이 flow를 어떻게 표현하는지가 관건인 것이다.

Transaction serializer

class TransactionSerializer(serializers.ModelSerializer):

	class Meta:
		model = Transaction
		fields = ['id', 'account', 'type', 'coin', 'price', 'total_price', 'coin_amount']

	def validate(self, data):
		if data['type'] == 'buy' and data['account'].balance < data['total_price']:
			raise serializers.ValidationError('보유 금액이 부족합니다')
		
		return data

	def create(self, validated_data):
		transaction = Transaction.objects.create(**validated_data)

		return transaction

transaction serializer의 경우 위와 같다. 특별히 조작해줘야할 field는 없었고, validate를 통해서 '매수시의 잔고가 매수금액을 감당할 수 있는지'를 체크해준다. 필요한 field는 view에서 작성해서 넘겨줄 것이다. create의 경우에는 특별할게 없이 override 하지 않아도 되나, 추가 조작이 있을까봐 일단 작성만 해 두었다.

여기서 알아야할 점은 transaction이 create되는 시점은 단순히 위 validate만 통과할 때가 아니라는 것이다. 이어서 나올 CoinHolding Serializer의 validate도 통과해야 create()가 실행될 수 있다.

CoinHolding Serializer

coinHolding Serializer의 경우 코드가 살짝 길다. 코드에 앞서 짚고 넘어갈 점은 두가지이다.

  1. 여기서는 판매, 즉 매도가 가능한 상태인지 validate 한다.
  2. 이미 buy에 대해서는 Transaction serializer를 통해서 validate 된 이후에 불리게된다.
class CoinHoldingSerializer(serializers.ModelSerializer):
    type = serializers.CharField(write_only=True)
    
    class Meta:
        model = CoinHolding
        fields = '__all__'


    def validate(self, data):
        if data['type'] == 'sell' and not self.instance:
            raise serializers.ValidationError('!!1 오류')
        elif data['type'] == 'sell' and self.instance:
            if self.instance.coin_amount < data['coin_amount']:
                raise serializers.ValidationError('11111')

        return data

    def create(self, validated_data):
        type = validated_data.pop('type')
        coinHolding = CoinHolding.objects.create(**validated_data)
        account = validated_data['account']
        account.balance -= validated_data['total_price']
        account.save()

        return coinHolding

    def update(self, instance, validated_data):
        type = validated_data.pop('type')
        account = validated_data['account']

        if type == 'buy':
            instance.coin_amount += validated_data['coin_amount']
            instance.total_price += validated_data['total_price']
            instance.price = instance.total_price / instance.coin_amount 
            account.balance -= validated_data['total_price']

        elif type == 'sell':
            instance.coin_amount -= validated_data['coin_amount']
            instance.total_price = instance.coin_amount * instance.price

            account.balance += validated_data['total_price']

        if instance.coin_amount == 0:
            instance.delete()
        else:
            instance.save()
        account.save()

        return instance 

validate

  • 우선 매도인데 instance가 없다. 즉 coin을 팔겠다는데 보유한 coin이 없는 상태이니 validatation error를 뱉어준다.
  • 다음 매도일 때 instance는 있다. 즉 해당 coin을 보유하고는 있는데, 판매하려는 양이 보유하고 있는 양보다 많다면! 또한 validation error를 뱉어준다.

create

create에서 해주는 작업은 두가지이다.
1. 들어온 buy(여기서 sell의 경우 이미 instance가 있어야 하므로 create는 모두 buy 요청에만 작동한다.) 조건에 맞춰서 구매됐다고 하고, 해당 양 만큼의 CoinHolding 을 만들어준다.
2. 코인 구매가 이루어졌으므로 account의 balance를 구매에 사용한 비용만큼 차감시킨다.

update

update의 경우 create와 비슷하다. 다만 '매수'일때와 '매도'일때의 움직이만 달라질 뿐이다.

  • 매수시에는
  1. 기존에 CoinHolding의 코인 보유양 증가
  2. 기존 CoinHolding의 총 매수금액 증가
  3. 1,2를 통해서 매수평단가 구해서 update
  4. 구매에 따른 잔고 금액 차감
  • 매도시에는
  1. 코인 보유량 감소
  2. 총 매수 금액을 구매단가 * 남은 보유량으로 update 시켜준다
    (여기서 이렇게 계산하는 이유는 구매 단가는 바뀌지 않았고, 보유량만 줄었기 때문이 이렇게 계산해준다.)
  3. 판매에 따른 잔고 금액 증가

그리고서 마지막에는 만약 coin 보유량이 0이 된다면 해당 CoinHolding 모델을 delete 하도록 구현했다. 그렇지 않으면 보유하지 않은데 기록이 남아있게 된다.

Transaction View

모든것을 마무리 지얼 transaction view이다. 나중에는 거래 목록을 확인하기 위해 get method를 허용해야 할 수 있지만, 일단은 post method만을 작성한다.

class TransactionView(views.APIView):
    def post(self, request, *args, **kwargs):
        price = get_coin_price(request.data['ticker'])['data']['closing_price']
        coin, create = Coin.objects.get_or_create(ticker=request.data['ticker'])
        data = {**request.data, 
            "price":price, 
            "coin":coin.id,
            "account" : Account.objects.get(user=self.request.user).id,
            "total_price": float(price) * request.data['coin_amount']
        }
        tran_seri = TransactionSerializer(data=data)

        try:
            instance = get_object_or_404(CoinHolding, account=data['account'], coin=data['coin'])
            coin_hold_seri = CoinHoldingSerializer(instance=instance, data=data)
        except:
            coin_hold_seri = CoinHoldingSerializer(data=data)

        tran_seri.is_valid(raise_exception=True)
        coin_hold_seri.is_valid(raise_exception=True)
        tran_seri.save()
        coin_hold_seri.save()

        return Response(tran_seri.data) 

위에서부터 차근차근 내려가며 정리 겸 마무리를 해보자.

  1. 이전에 만들었던 coin/utils.py의 함수를 통해서 거래 요청 순간의 가격을 받아온다.

  2. 거래요청을 한 coin의 모델이 있다면 가져오고, 없다면 생성한다.

  3. serializer에 맡게 data를 재조작해서 만들어준다.

  4. transaction serializer를 선언한다.

  5. 만약 request 요청한 account가 해당 coin에 대한 coin holding instance가 있는지 없는지를 확인하고 coinholding serializer를 선언한다.

  6. transaction serializer로 요청 거래가 매수일 경우의 거래 가능한지를 확인한다.

  7. 바로 이어서 coinholding serializer를 통해요청 거래가 매도일 경우의 거래가 가능한지를 확인한다.

  8. 6,7에서 모두 exception이 일어나지 않았다는 것은 진행 가능한 거래이므로 두 serializer 모두 save() method를 실행시켜서 해당 Transaction과 CoinHolding 조작을 끝마쳐준다.

url 연결

#account/urls.py
path('transaction', TransactionView.as_view(), name='transaction')

만 추가해주면 된다.


정리

여기까지가 사실 백엔드 구성의 전부인 것 같다. 거래를 발생시키고 어떤 과정을 통해서 거래가 만들어지는지, 어떤 validation이 필요하고 어느 곳에서 그게 일어나는지를 작성하는게 포인트인 것이다. 다만 나도 내 딴에는 열심히 작성했으나, 이게 알맞게 구현되어 있다고 자신할 수는 없다.

그러나 이번 시도가 처음으로 서로 연관되는 모델 구조를 작성해본 것인데, 생각대로 작동이 되서 꽤나 기쁘다. 아마 다음 글에서는 직접 api를 통해서 거래를 해보는 것을 글로 남겨볼 것이다.

profile
(전) Junior Android Developer (현) Backend 이직 준비생

0개의 댓글