[강의 공부] 제미니의 개발실무 - 커머스 백엔드 기본편
강의 소개

현재 상황이 사용자가 10000명, 상품이 10000개 이상이고, 꾸준히 성장하고 있는 상황에서 개발자가 단 3명밖에 없다면 어떤 고민을 해야하고, 어떻게 개발을 진행해야 할까? 클린코드, 헥사고날 아키텍처도 좋지만 결국에는 일이 잘 되게 하는 것이 목적이다. 이 강의는 그러한 트레이드 오프속에 고민하고 문제해결을 위주로 생각하도록 도와주는 강의로써 좋은 강의입니다!
상품목록
ProductService 라는 클래스 안에 굳이 ProductFinder 클래스를 따로 만들어 둔 이유
- 조회(Query) 책임을 서비스에서 분리시키기 위해.
class ProductService {
fun findProduct(...) { ... }
fun findProductsByCategory(...) { ... }
fun createProduct(...) { ... }
fun updateProduct(...) { ... }
fun deleteProduct(...) { ... }
fun validateProduct(...) { ... }
fun calculatePrice(...) { ... }
}
- 조회 + 생성/수정 + 비즈니스 규칙 + 검증 모든 것이 한 클래스에 몰리면
- 읽기가 어렵고
- 테스트가 어려워지고
- 한 클래스에 책임이 많아진다
- 그래서 이러한 읽기(find) 계열은 따로 분리하여 은닉화시켜두고, ProductService 클래스에서는 ProductFinder의 메서드를 호출하도록 변경한다.
여기서 핵심은 한 클래스에 책임이 많다면 분리시키는 것이다. CQRS와 연관이 있다.
- 상품과 카테고리의 관계
- 상품 -> 카테고리에 의존하면 안된다.
- 카테고리의 정책이 변경되면 상품의 정책이 흔들리게 된다. (중요!)
- 매핑 테이블(ProductCategory)을 만들어 Category를 의존하게 하면 된다.
- Product는 ProductCategory을 통해 접근한다. 즉, Product는 Category를 모른다.
- 상품 -> 카테고리에 의존하면 안된다.
- 뭔가 다양한 분류를 생각해야될 것 같다면 Enum 클래스를 적극적으로 활용하자. 기능을 확장하는데에 도움이 된다.
도메인을 설계할 때는 DB 테이블을 먼저 생각하면 안된다. 개념적으로 접근하여 이 클래스가 어떤 역할을 수행하고, 어떤 메시지를 가지고 있는지를 고민한다. 그러면 저절로 어떤 클래스가 또 분리되어 필요한지 고민하게 된다.
리뷰
리뷰(Review)에도 정책이 다양하다. 여기서의 핵심도 책임을 분리하는 것이다. 여기서 마찬가지로 ReviewFinder 클랙스가 있을 것이고, 리뷰를 작성하면 포인트를 지급해주는 정책이 있다면 이러한 정책을 관리하는 클래스를 하나 또 만드는 것이다. (ReviewPolicyValidator) 만약 정책이 바뀐다면 ReviewPolicyValidator 클래스만 변경하면 되는 것이다. 하나의 ReviewService 이러한 모든게 담겨있게 된다면 변경해야 할 곳이 이곳저곳 정신없을 것이다. (테스트도 어려워질 것이다.)
만약 정책이 “하나의 주문에 하나의 리뷰을 달 수 있고, 포인트를 지급한다” 라고 되어 있다면
Review->Order의존Review->Point의존- 여전히
Product는Review를 모른다.
그 밖에 QnA, 포인트, 쿠폰, 장바구니, 결제 등등 다양한 도메인을 설계하기 전에 먼저 개념적으로 접근하는 것이 좋다. 실제 어떤 제약이 있고, 어떤 정책이 있다는 전제하에 어디까지 접근할 수 있는지 그러한 고민들을 해나가는 것이다. 이러한 고민들을 기획자, 프론트엔드 등 팀원들과 얘기하면서 문제를 해결해나가는 것이 중요하다. 아키텍처, 기술적인 얘기도 중요하지만, 개념적으로 어떻게 접근해서 문제들이 발생할 수 있는지, 그러면 그 문제들을 어떻게 해결해나갈지 깊이 있게 생각해보는 것이다.
Cart (장바구니)
도메인 설계하기 전에 DB테이블을 기준으로 설계하지 말고 개념적으로 설계하라는 이유가 있다. 만약 DB테이블을 기준으로 한다면 Cart 라는 테이블을 만들 것이다. 하지만 개념적으로 보면 Cart라는 것은 하나이고, Cart 안에 item들을 담을 수 있다. 그래서 CartItemEntity를 만들 수 있고, Cart라는 DB테이블과 관계없는 논리 클래스를 만들 수 있다. Cart라는 클래스는 CartItem들과 Product들을 담는 논리적인 클래스인 것이다. (사용자는 마트를 가서 상품들을 담을 때 장바구니 2~3씩 들고 다니지 않다. 한 사람당 하나씩 들고다닌다는 가정하에 설정)
그러면 CartEntity를 만들지 않은 이유는 결국 장바구니의 관점을 봤을 때 CartItem에 대한 정보들이 더 중요하다. Item의 수량이나 날짜 등의 중요한 정보가 포함되어 있기 때문에 CartItemEntity로 만드는 것이다. (CartItem으로 핸들링 하기에 더 적합하다.)
Cancel (취소)
결제(Payment)에서 취소 도메인이 있다. PaymentEntity에 취소 상태 컬럼을 추가해도 되지만, 이러면 취소 행위가 발생할 때마다 PaymentEntity에 Update를 발생시킨다. 그러면 PaymentEntity가 헤비해지고, PaymentEntity 자체에 부담이 가진다. 그리고 PaymentEntity에 의존성이 많아져 응집도가 떨어지게 된다. 그래서 CancelEntity를 만들어서 취소에 대한 정보를 자체적으로 관리하도록 만든다. 이러면 정산이나 데이터 분석시 좀 더 도움이 되고, 단순화된다. (그렇지만 규모에 따라 트레이드 오프를 고려해볼 필요가 있다.)
Settle (정산)
가맹점들에게 정산해주거나 돈 관련 비즈니스를 할 때 필요한 도메인이다. 돈 관련 도메인이나 보니 복잡하지만 그만큼 백엔드 개발자들이 도전하고 싶어하는 도메인이다. 정산은 보통 배치를 돌려서 관리한다. 정산에 필요한 도메인은 Payment, OrderItem, Cancel 데이터가 필요하다. 규모가 크지 않다면 대부분의 시간 베이스로 정산하는 것이 일반적이다. 다만 단점이 있는데,
- 환불/취소가 늦게 발생하면 보정 필요
- 실시간 수익 확인 어려움
규모가 크다면 이벤트 기반이나 상태 기반으로 개선할 수도 있다.
설계시 Settlement 클래스만 만들어도 되겠지만 Settlement의 의존성을 덜어주기 위해 SettlementTarget 클래스를 별도로 만든다. 그러면 Settlement -> SettlementTarget만 보면 된다. 결과적으로 어떠한 도메인의 데이터가 추가되어도 SettlementTarget이 처리해주고, Settlement는 아무런 영향을 받지 않는다.
정리
지금까지의 도메인들을 개념적으로 격벽을 사용하여 그림을 그려보면 어떤 도메인이 우리 서비스에서 중요하게 보고 있는지를 알 수 있다. 그래서 혹시 중요한 도메인이 다른 서브 도메인에 의존적인 관계가 있지 않은지, 개선할 수 없는지를 판별할 수 있을 것이다.
Comments