우선 모델 구조 개편에 앞서, 작성했던 코드에서 수정이 필요한 부분을 수정한다.
로그인 세션 관리 관련 코드가 빠져있어 추가한다.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
)
}
여기부터가 이번 글 작성 내용의 시작이다.
만들 구조는 Transaction과 CoinHolding 두 개의 모델 조작을 통해서 거래를 발생/기록하고, 이에 따라 개인이 보유한 Coin의 양을 변경하게 된다.
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 설명은 아래와 같다.
모두 직관적으로 이해할 수 있는 field들이다. 거래내역이라고 생각하면 편한다.
다음은 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 serializer를 수정한다.
#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']
수정된 점
이 부분에 들어가기 위해서는 먼저 로직의 이해가 우선이다. transaction이 만들어지는 flow는 다음과 같다.
이 flow를 어떻게 표현하는지가 관건인 것이다.
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의 경우 코드가 살짝 길다. 코드에 앞서 짚고 넘어갈 점은 두가지이다.
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
create에서 해주는 작업은 두가지이다.
1. 들어온 buy(여기서 sell의 경우 이미 instance가 있어야 하므로 create는 모두 buy 요청에만 작동한다.) 조건에 맞춰서 구매됐다고 하고, 해당 양 만큼의 CoinHolding 을 만들어준다.
2. 코인 구매가 이루어졌으므로 account의 balance를 구매에 사용한 비용만큼 차감시킨다.
update의 경우 create와 비슷하다. 다만 '매수'일때와 '매도'일때의 움직이만 달라질 뿐이다.
그리고서 마지막에는 만약 coin 보유량이 0이 된다면 해당 CoinHolding 모델을 delete 하도록 구현했다. 그렇지 않으면 보유하지 않은데 기록이 남아있게 된다.
모든것을 마무리 지얼 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)
위에서부터 차근차근 내려가며 정리 겸 마무리를 해보자.
이전에 만들었던 coin/utils.py의 함수를 통해서 거래 요청 순간의 가격을 받아온다.
거래요청을 한 coin의 모델이 있다면 가져오고, 없다면 생성한다.
serializer에 맡게 data를 재조작해서 만들어준다.
transaction serializer를 선언한다.
만약 request 요청한 account가 해당 coin에 대한 coin holding instance가 있는지 없는지를 확인하고 coinholding serializer를 선언한다.
transaction serializer로 요청 거래가 매수일 경우의 거래 가능한지를 확인한다.
바로 이어서 coinholding serializer를 통해요청 거래가 매도일 경우의 거래가 가능한지를 확인한다.
6,7에서 모두 exception이 일어나지 않았다는 것은 진행 가능한 거래이므로 두 serializer 모두 save() method를 실행시켜서 해당 Transaction과 CoinHolding 조작을 끝마쳐준다.
#account/urls.py
path('transaction', TransactionView.as_view(), name='transaction')
만 추가해주면 된다.
여기까지가 사실 백엔드 구성의 전부인 것 같다. 거래를 발생시키고 어떤 과정을 통해서 거래가 만들어지는지, 어떤 validation이 필요하고 어느 곳에서 그게 일어나는지를 작성하는게 포인트인 것이다. 다만 나도 내 딴에는 열심히 작성했으나, 이게 알맞게 구현되어 있다고 자신할 수는 없다.
그러나 이번 시도가 처음으로 서로 연관되는 모델 구조를 작성해본 것인데, 생각대로 작동이 되서 꽤나 기쁘다. 아마 다음 글에서는 직접 api를 통해서 거래를 해보는 것을 글로 남겨볼 것이다.