지난 시간까지 Domain 계층과 저장소 패턴에 대해 배웠다.
실세계의 웹 애플리케이션에서의 아키텍처는 어떻게 달라야 할까?
1) 우선 API 방식으로 프로덕트가 움직인다. 장고, 플라스크 등의 API 계층이 추가될 것이다.
2) 서비스 계층이라는 추상화 계층이 필요하다.
따라서, API와 테스트 계층은 서비스 계층의 클라이언트가 되는 꼴이다.
3) 실제 환경에서 API는 SQLAlchemy Repository를 사용한다.
테스트는 FakeRepository를 사용한다.
엔드투엔드란?
end-to-end, E2E 테스트는 영어 표현대로 끝과 끝까지의 테스트다.
API호출부터 DB 저장까지의 테스트다.
아키텍처 우주인
"사람들이 워드프로세스나 PDF 파일을 주고받는구나"
통찰: 파일 전송이라는 추상화 계층을 두자!
(시간이 흐르고)
"requests를 주고받는 행위도 결국 같은 맥락아닌가? 메시지를 주고받는거잖아"
통찰: 메시징 추상화 계층을 두면 다 해결할 수 있구나!
하지만 점점 개념의 범위가 넓어지면서 의미전달이 모호해진다.
다른 부서의 실무자들은 파일 전송에 대한 이야기를 할 때, 그건 사실 메시징을 주고받는 행위라고 외쳐대는 단계에 다다른다.
우주비행사가 지구로부터 점점 멀어지면서, 언제 멈춰야할지를 모른다.
계속 하이 레벨 수준에서의 세계만 떠들고 bottom line에 대한 이야기를 하지 않는다.
"high-level pictures of the universe that are all good and fine, but don’t actually mean anything at all."
https://www.joelonsoftware.com/2001/04/21/dont-let-architecture-astronauts-scare-you/
조엘 온 소프트웨어: http://www.yes24.com/Product/Goods/1469763
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APIClient
from core import models
# 찜하기URL
LIST_URL = reverse("shopping:main")
def create_sample_product(**param):
"""샘플 제품 생성"""
return models.Product.objects.create(**param)
class PrivateUserEnrollmentProductApiTest(TestCase):
"""유저 찜 기능 테스트"""
def setUp(self):
self.client = APIClient()
self.user = get_user_model().objects.create(
email="like-create@user.com",
password="like-create"
)
self.token = create_token(self.user)
self.client.cookies = SimpleCookie({"access_token": self.token})
self.brand = models.Brand.objects.create(
kor_name="샤넬",
eng_name="Chanel"
)
self.product_status_1 = models.ProductStatus.objects.create(
id=1,
name="신청 목록"
)
self.product_status_5 = models.ProductStatus.objects.create(
id=5,
name="판매 대기"
)
self.product_status_6 = models.ProductStatus.objects.create(
id=6,
name="판매 중"
)
def test_empty_user_product_list_post_success(self):
"""찜 리스트 비어있는 상태에서 제품 추가 케이스"""
# valid 제품 1개
product_param = {
"user": self.user,
"brand": self.brand,
"product_status": self.product_status_6
}
sample_product = create_sample_product(**product_param)
payload = {"product_id": sample_product.id}
res = self.client.post(LIKE_CREATE_URL, payload)
num_of_like = models.UserProduct.objects.filter(user=self.user).count()
self.assertEqual(res.status_code, 201)
self.assertEqual(res.data, None)
self.assertEqual(num_of_like, 1)
def test_fulled_user_product_list_post_success(self):
"""찜 리스트 채워져 있는 상태에서 제품 추가 케이스"""
product_param = {
"user": self.user,
"brand": self.brand,
"product_status": self.product_status_6
}
# valid 제품 4개 추가
for i in range(4):
sample_product = create_sample_product(**product_param)
payload = {"product_id": sample_product.id}
self.client.post(LIKE_CREATE_URL, payload)
# valid 제품 1개 추가
new_product = create_sample_product(**product_param)
payload = {"product_id": new_product.id}
res = self.client.post(LIKE_CREATE_URL, payload)
num_of_like = models.UserProduct.objects.filter(user=self.user).count()
self.assertEqual(res.status_code, 201)
self.assertEqual(res.data, None)
self.assertEqual(num_of_like, 5)
def test_empty_user_product_post_fail_invalid_product_status(self):
"""비어있는 찜 리스트에 판매 중(status_id=6)이 아닌 제품 추가 케이스"""
# invalid 제품 1개 추가
product_param = {
"user": self.user,
"brand": self.brand,
"product_status": self.product_status_5
}
invalid_product = create_sample_product(**product_param)
payload = {"product_id": invalid_product.id}
res = self.client.post(LIKE_CREATE_URL, payload)
num_of_like = models.UserProduct.objects.filter(user=self.user).count()
self.assertEqual(res.status_code, 400)
self.assertEqual(res.data, {'MESSAGE': 'invalid product'})
self.assertEqual(num_of_like, 0)
def test_fulled_user_product_post_fail_invalid_product_status(self):
"""채워져있는 찜 리스트에 판매 중(status_id=6)이 아닌 제품 추가 케이스"""
product_param = {
"user": self.user,
"brand": self.brand,
"product_status": self.product_status_6
}
# valid 제품 4개 찜
for i in range(4):
sample_product = create_sample_product(**product_param)
payload = {"product_id": sample_product.id}
self.client.post(LIKE_CREATE_URL, payload)
# invalid 제품 1개 추가
product_param = {
"user": self.user,
"brand": self.brand,
"product_status": self.product_status_5
}
invalid_product = create_sample_product(**product_param)
payload = {"product_id": invalid_product.id}
res = self.client.post(LIKE_CREATE_URL, payload)
num_of_like = models.UserProduct.objects.filter(user=self.user).count()
self.assertEqual(res.status_code, 400)
self.assertEqual(res.data, {'MESSAGE': 'invalid product'})
self.assertEqual(num_of_like, 4)
def test_fulled_user_product_post_already_registered_product(self):
"""채워져있는 찜 리스트에 이미 등록된 제품 추가 케이스"""
product_param = {
"user": self.user,
"brand": self.brand,
"product_status": self.product_status_6
}
# valid 제품 4개 찜 추가
for i in range(4):
sample_product = create_sample_product(**product_param)
payload = {"product_id": sample_product.id}
self.client.post(LIKE_CREATE_URL, payload)
# 이미 추가된 제품 1개 추가
already_registered = models.UserProduct.objects.filter(
user=self.user).first().product
payload = {"product_id": already_registered.id}
res = self.client.post(LIKE_CREATE_URL, payload)
num_of_like = models.UserProduct.objects.filter(user=self.user).count()
self.assertEqual(res.status_code, 400)
self.assertEqual(res.data, {'MESSAGE': 'duplicated product'})
self.assertEqual(num_of_like, 4)
def tearDown(self):
models.Brand.objects.all().delete()
models.ProductStatus.objects.all().delete()
models.Product.objects.all().delete()
models.UserProduct.objects.all().delete()
서비스 계층 == 오케스트레이션 == 오케스트레이션 계층 == 유스케이스 계층 != (클라우드)오케스트레이션 (계층)
오케스트레이션은 E2E 테스트에서 실제로 테스트해야 하는 대상이 아니다.
def create_matches(**param):
"""매치 생성"""
# 파라미터 검증 및 생성
return Match.objects.create(**param)
장고에서는 유닛테스트 모듈에서 테스트용 데이터베이스가 제공된다.
따라서, 코드 내에 FakeRepository layer가 생성될 필요가 없고 사실 고려할 필요도 없다.
즉, DB의 클라이언트는 API와 테스트로써, 우리는 테스트용 DB와 deploy용 DB를 구분하여 생각할 필요가 없다.
서비스 계층: 애플리케이션 서비스
도메인 서비스: 엔티티나 값 객체와 무관한 로직 담당
매치 시작 시간 별 참가비가 차등 부여되는 것은 무엇일까?
매치 시작 시간이라는 인스턴스의 엔티티를 알아야 하기 때문이다.
반면, 오늘의 날씨를 파악하여 그 매치가 우천 매치인지, 아닌지를 판단하는 것은 도메인 서비스다. 오늘의 시간별 강수량을 파악후 수치(단위: mm)에 따라 우천 상황인지 아닌지를 판단하기 때문이다.
장고 앱 생성 시, services.py를 생성하여 도메인 로직을 전부 이곳에 작성하면,
빈약한 도메인
이라는 부작용이 생길 수도 있다.
웹 프레임워크에서 views.py라는 컨트롤러에 사실상 모든 유스 케이스가 있다.
또한, 장고의 모델 메소드에서 서비스 계층과 도메인 서비스까지 만들어 낼 수 있다.
Fat Model을 지향하는 장고에서 모델과 뷰 사이에 굳이 추상화 계층을 만들 필요는 없다고 생각한다.