1) 사용자가 로그인 후, 임의의 제품을 찜 리스트에 넣는다.
2) 사용자가 탈퇴를 하면, 찜 리스트는 삭제하면 그만이다.
(사용자가직접
탈퇴를 했기 때문에, 문제의 소지가 없다.)3) 제품이 품절될 경우
--> (케이스1)on_delete=models.CASCADE
--> (케이스2)on_delete=models.DO_NOTHING
--> (케이스3)on_delete=models.SET_NULL, null=True
유저의 입장에서 생각해보자.
UI/UX 디자이너의 입장에서까지는 아니지만, 그래도 기본적인 역지사지를 할 필요가 있다.
예를들어, 쿠팡에서 맘에 드는 물건을 찜 리스트(==좋아요 리스트)에 넣어뒀다.
다음날 쿠팡앱을 확인해보니, 내가 찜 리스트한 품목이 사라져버렸다.
제품이름을 겨우 떠올려 검색해보니 해당 제품이 품절(혹은 단종, 절판)되었음을 확인했다.
내 입장에서는 조금은 납득하기 힘든 상황이다. 찜 리스트에서 해당 제품이 품절되어 구매할 수 없다는 식으로 알려주지 않고, 찜 리스트에서 없애버린 것이다.
다시 한번 강조하지만, 실제 쿠팡은 위와같이 하지 않는다.
실제 쿠팡에서는 품절 혹은 단종된 경우
찜 리스트에서 해당 제품 이미지가 회색 음영처리된 것을 확인할 수 있었다.
품절된 상품이라는 걸 단번에 알 수 있으며, 그럼에도 불구하고 여전히 내 찜 리스트에 있게 조치해 뒀다.
중고명품 거래 서비스를 제공하는 쿠돈(koodon)에서도
찜리스트 및 장바구니 기능을 구현해야 한다.
이런저런 테스트를 해보고 있다.
(실제 배포될 서비스이기에 이런저런 테스트를 스스로 해보고 있다. 너무 떨린다!!!!)
>>> UserProduct.objects.values()
<QuerySet [{'id': 1, 'user_id': 1, 'product_id': 1}]>
User와 Product의 중간테이블인 UserProduct 가 좋아요(찜) 리스트
인 상태다.
>>> Product.objects.get(id=1).delete()
품절 및 단종 등의 이유로 제품을 삭제했다고 가정해보자.
참고로, 제품 models.py는 아래와 같다.
실험을 위해 최대한 간략하게 작성했다.
from django.db import models
class UserProduct(models.Model):
user = models.ForeignKey('accounts.User', on_delete=models.CASCADE)
product = models.ForeignKey('Product', on_delete=models.SET_NULL, null=True)
class Meta:
db_table = 'users_products'
class Product(models.Model):
name = models.CharField(max_length=100)
is_valid = models.BooleanField()
다시, 좋아요(찜) 리스트를 보자.
>>> UserProduct.objects.values()
<QuerySet [{'id': 1, 'user_id': 1, 'product_id': None}]>
이는 Product 클래스를 정의할때,
product = models.ForeignKey('Product', on_delete=models.SET_NULL, null=True)
위와 같이 작성했기 때문이다.
그렇다면, on_delete=models.DO_NOTHING
을 해보자.
UserProduct 클래스를 부분수정후, Product를 삭제하려하니
Integrity 에러가 발생했다.
django.db.utils.IntegrityError: FOREIGN KEY constraint failed
외래키로 참조하는 객체가 삭제되었는데도, DO_NOTHING을 사용하는 것은 관계형 데이터베이스에서 비추천하는 방식이라고 한다.(비권장)
DO_NOTHING 지정후, 참조하는 객체를 삭제하려고 하면
Integrity에러를 일부러 발생시킨다고 한다.
다음 포스팅에서,
장고에서 그토록 비추천한다는 DO_NOTHING을 한번 진행해보려 한다.
이번엔 유저가 탈퇴한 상황을 가정해보자.
예측이 가능한 결과다. 그래도 해보는 거다.
User id가 5번인 유저를 삭제했다.
예상결과는 UserProduct에 해당 row(데이터)가 삭제될 것이다.
>>> User.objects.get(id=5).delete()
좋아요(찜) 리스트를 확인해보자.
>>> UserProduct.objects.values()
<QuerySet [{'id': 1, 'user_id': 1, 'product_id': None}]>
역시, 유저 아이디가 5번과 관련된 데이터가 삭제됐다.
(위에 보이는 쿼리셋 객체 하나는 이전 테스트때 저장된 객체다.)
product_id가 None과 같이 변경되면
발생하는 문제가 하나 있다.
맨 처음 언급한 시나리오처럼 다음날 나의 찜리스트에서
품절된 제품이 더이상 보이지 않는다는 것이다.
이 방식은 문제가 있어 보인다.
장바구니 및 결제 정보를 다루는 테이블이라면,
로그 테이블을 통해 유저가 삭제되더라도
로그 정보는 남아있어야 한다.
아니, 로그는 유저의 발자취이자 흔적이다.
유저의 기록을 전부 기록하고 삭제되어서는 안된다.
" 유저가 삭제되서, 유저 id(pk)가 User테이블에 없다면??????
그런데 로그 기록엔
예를들어 user_id가 None이 된 기록이 남아있다.
유저가 한 두명도 아니고 None으로 바뀐 탈퇴 유저를 어떻게 찾아야 할까?"
즉, 예를들어 134번이었던 유저가 회원탈퇴를 하니,
-> 유저 테이블에서 삭제되고
-> 해당 유저를 참조하는 테이블의 데이터가 None으로 된 상황!!!!
(SET_NULL, null=True 설정시)
결론부터 말하면SET_NULL, null=True
방식을 채택하게 됐다.
이와 같이 채택하게 된 과정을
아래의 테스트들을 통해 설명해보려 한다.
class Log(models.Model):
create_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey('accounts.User', on_delete=models.CASCADE)
user_name = models.CharField(max_length=100)
user_phone_number = models.CharField(max_length=100)
class Meta:
db_table = 'logs'
당연한 결과가 예상된다.
로그의 경우, 절대 위와 같이 코드를 작성하면 안 된다.
유저가 탈퇴를 할 경우, 로그 역시 삭제되기 때문이다.
결과를 보자.
>>> Log.objects.create(user_id=4, user_name=User.objects.get(id=4).user_name, user_phone_number=User.objects.get(id=4).phone_number)
<Log: Log object (2)>
이제 유저를 삭제시켜보자.
>>> User.objects.get(id=4).delete()
로그를 확인해보자.
>>> Log.objects.values()
<QuerySet []>
예상했던 대로, 유저가 삭제되니 로그 역시 삭제됐다.
즉, 이러한 방식으로 사용하면 안된다.!
>>> Log.objects.values()
<QuerySet [{'id': 1, 'create_at': datetime.datetime(2020, 7, 26, 6, 58, 44, 577096, tzinfo=<UTC>), 'user_id': 5, 'user_name': 'test_staff', 'user_phone_number': '010-1233-4567'}]>
유저를 삭제해보자.
>>> User.objects.get(id=5).delete()
다음과 같은 에러 메시지가 출력됐다.
django.db.utils.IntegrityError: FOREIGN KEY constraint failed
역시나......
장고에서 DO_NOTHING은 on_delete 옵션에서 1000000% 배제하자.
(다음 포스팅에서 DO_NOTHING에 대해 다뤄보려 한다.)
그렇다면, 로그 models.py를 수정해보자.(SET_NULL, null=True)
class Log(models.Model):
create_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True)
user_name = models.CharField(max_length=100)
user_phone_number = models.CharField(max_length=100)
class Meta:
db_table = 'logs'
다시, 유저를 삭제해보자.
>>> User.objects.get(id=5).delete()
삭제가 되었다.
로그는 어떻게 됐을까?
로그를 확인해보자.
>>> Log.objects.values()
<QuerySet [{'id': 1, 'create_at': datetime.datetime(2020, 7, 26, 6, 58, 44, 577096, tzinfo=<UTC>), 'user_id': None, 'user_name': 'test_staff', 'user_phone_number': '010-1233-4567'}]>
user_id 가 기존 5에서 None 으로 바꼈다.
하지만,
유저의 이름
과 유저의 전화번호
는 남아있으므로 로그의 역할을 충실히 하고 있다.
즉, 유저가 삭제되면 유저 id(pk) 정보로는 추적을 할 수가 없다.
(너무나 당연하다. 유저 테이블에서 User 정보가 사라지기 때문)
다만, 이것은 큰 문제가 될 것처럼 보이지 않는다.
왜냐하면,
회원가입시의 아이디 (또는 휴대폰 번호 등을 unique=True)
역시 로그에 남기면,
탈퇴한 회원마저 로그에서 추적이 가능하기 때문이다.
(즉, log 클래스의 attribute에
등과 같이 유저를 식별할 수 있는 필드들을 추가하였다.)