우선 비즈니스 제약조건을 파악하고, 어떻게 불변조건으로 치환할 수 있는지
고민해야 한다.
그리고 본격적으로 DB 일관성을 유지하기 위한 애그리게이트 설계를 위해
유스 케이스
를 분석하여 경계(boundary)
를 정의해야 한다.
1️⃣ 불변조건
: 어떤 연산을 끝낼 때마다 항상 참이어야 하는 요소
2️⃣ 제약 조건
: 모델이 취할 수 있는 상태의 수 제한
<비즈니스 제약조건 1>
중복 매치 신청은 불허한다.
위 제약조건
은 하나의 Match
에는 한 계정의 MatchApply
만 가능하다는
불변조건
으로 치환할 수 있다.
<비즈니스 제약조건 2>
최대 신청 가능인원을 초과할 수 없다.
위 제약조건을 불변조건으로 바꾸면
match.max_player_cnt
보다 작거나 같은 값일 경우에만
매치 신청이 가능하도록 해야 한다.
시스템 상태를 업데이트할 때마다 불변조건을 validate해야 한다.
하지만, 동시성이라는 개념을 고려해야하기 시작하는 순간부터
문제 난이도가 올라간다.
3️⃣ 애그리게이트
: 객체 컬렉션 전체를 한꺼번에 다룰 수 있게 해주는 도메인 객체
"메모리상에서 예약을 섞는 동안 한 곳에 예약이 2개 이상 발생할 수도 있지만, 작업이 끝나면 도메인 모델은 모든 불변조건을 만족하는 일관성있는 최종상태로 끝난다는 사실을 보장해야 한다. 모든 고객이 만족하는 방법을 찾지 못하면 채 연산이 완료되면 안 되고 오류가 발생해야 한다."(p.149)
"단일 진입점을 만들어야 한다. 애그리게이트에 있는 객체를 변경하는 유일한 방법은 애그리게이트와 그 안의 객체 전체를 불러와서 애그리게이트 자체에 대해 메서드를 호출하는 것이다."(p.151)
Billing(결제)은 컬렉션이다.
이 루트 엔티티에는 하나의 작업 단위로 트랜잭션 처리 돼야할 주문들이 있다.
또한, 쿠폰, 캐시, 주차 처리 역시 이 루트 엔티티의 상태(status)에 종속적이다.
애그리게이트 패턴을 적용하기 위해 경계를 명확히 정의할 차례다.
그 기준은 유스 케이스가 된다.
모든 유저의 Billing들을 한 트랜잭션으로 처리하려는 유스 케이스는 없다.
각 유저의 Billing에 대해서만 불변조건을 유지하면 되므로, 모델 메소드에 정의내리는 것이 적절하다.
(참고)
유스케이스: 사용자와 시스템 간에 이뤄지는 상호작용 흐름을 텍스트로 정리한 것으로, 여러 시나리오들의 집합이다.
(출처: 객체지향의 사실과 오해)
class BaseBilling(models.Model):
(생략)
class Billing(BaseBilling):
(생략)
@transaction.atomic
def to_success(self):
"""결제 확정 단일 진입점"""
# 쿠폰 사용 처리
# 캐시 사용 처리
# 주차 사용 처리
@transaction.atomic
def to_cancel(self):
"""결제 취소 단일 진입점"""
# 쿠폰 환급 처리
# 캐시 환급 처리
# 주차 복구 처리
def order_success_kakaopay(requset, *args, **kwargs):
"""카카오 페이 결제 확정"""
try:
(중략)
payment = PaymentAction.set_pg(PaymentGateway.KAKAOPAY)
response = payment.approve(body)
if response.status_code != 200:
raise PaymentAPIException(response.text, response.status_code)
else:
(중략)
# order 애그리게이트
order.to_success()
# billing 애그리게이트
billing.to_success()
return render(request, 'order/order_success.html', context)
except PaymentAPIException as e:
(중략)
def test_update_user_profile(self):
'''Test updating the user profile for authenticated user'''
payload = {
'name': 'new_name',
'password': 'new_password'
}
res = self.client.patch(ME_URL, payload)
self.user.refresh_from_db()
self.assertEqual(self.user.name, payload['name'])
self.assertTrue(self.user.check_password(payload['password']))
self.assertEqual(res.status_code, status.HTTP_200_OK)
from blog.models import Article
a = Article.objects.get(pk=1)
Article.objects.filter(pk=1).update(title="New Title")
>> a
Article 1
>> a.refresh_from_db()
>> a
New Title
refresh_from_db() vs Foo.objects.get(id=obj.id)
# 락
billing = Billing.objects.get(pk=id).select_for_update()
# 값 업데이트
billing.status = PaymentStatus.SUCCESS_PAYMENT
# 영속화
billing.save()
하지만 위 방식을 적용하지 않은 이유는 다음과 같다.
# settings.py
TESTING = 'test' in sys.argv[1:]
if TESTING:
DATABASES['default'] = {'ENGINE': 'django.db.backends.sqlite3'}
print('=====================================================')
print('In TEST Mode - Disableling Migrations')
print('- For performance, changing DB engine from MySQL to SQLite')
print('=====================================================')
print()
class DisableMigrations(object):
def __contains__(self, item):
return True
def __getitem__(self, item):
return None
MIGRATION_MODULES = DisableMigrations()
필드의 값을 증,감소하는 것은 가능하지만,
상태명을 변경하려고 하는 기능에는 적합하지 않다.
reporter = Reporters.objects.filter(name='Tintin')
reporter.update(stories_filed=F('stories_filed') + 1)
그래서 나는
Billing 애그리게이트인 to_success
에서 refresh_from_db()
를 진행하고있다.
to_success에서만 billing 및 연관 객체들을 업데이트할 수 있고,
이곳에서만 billing.refresh_from_db()를 하기로 1차 결정을 내렸다.