7. 애그리게이트와 일관성 경계

hyuckhoon.ko·2021년 11월 27일
0

🧐 도메인 모델은 영속적 저장소에서 일관성을 유지해야 한다.

우선 비즈니스 제약조건을 파악하고, 어떻게 불변조건으로 치환할 수 있는지
고민해야 한다.

그리고 본격적으로 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):
    	"""결제 취소 단일 진입점"""
    
    	# 쿠폰 환급 처리
        # 캐시 환급 처리
        # 주차 복구 처리
    	

결제 확정 처리 FBV

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:
    (중략)
        




🪁 일관성 유지를 위한 솔루션

① refresh_from_db

    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)

https://stackoverflow.com/questions/39458820/django-model-reload-from-db-vs-explicitly-recalling-from-db


② select_for_update

# 락
billing = Billing.objects.get(pk=id).select_for_update()
# 값 업데이트
billing.status = PaymentStatus.SUCCESS_PAYMENT
# 영속화
billing.save()

하지만 위 방식을 적용하지 않은 이유는 다음과 같다.

  • DB 엔진에 따라, select_for_update가 적용되지 않는다.
    ex) SQLite에서는 미지원
# 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()

③ F() expression

필드의 값을 증,감소하는 것은 가능하지만,
상태명을 변경하려고 하는 기능에는 적합하지 않다.

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차 결정을 내렸다.

0개의 댓글