지난 시간에는 대략적인 요구사항을 정리했었다. 이번 글에서는 제작하려는 서비스가 구체적으로 어떤 데이터 구조를 가져야 할지 스키마에 대해 고민해보았다.
백엔드는 눈에 보이지 않는다. 시각디자인 전공자에게는 마치 반대편 끝에 존재하는 분야처럼 느껴졌다. 실무를 진행하면 알쏭달쏭한 이야기들이 들려온다. 트랜잭션의 동시성 이슈를 어떤 식으로 방지한다 라던지, 어떤 식으로 구조를 짜야 부하 테스트를 통과하고 안정성이 높아지는지 등등 보이지 않는 것들에 대해 말하는 것은 매번 들어도 어렵다. 요즘은 아예 프론트엔드에서 BFF(Backend For FE)를 구축해서 필요한 데이터만 받는다고 하던데 그렇다면 그 백엔드는 프론트엔드 개발자가 구축해야 하나? 그렇다고 한다.
백엔드는 어쩔 수 없이 조금 덜 친근한 분야이긴 하지만 데이터 기반 예산수정 문제를 해결하려면 가장 깊이 알아야 한다. 먼저 데이터 구조에 대한 고민이 있다. 사실 스키마를 처음부터 짜본 경험이 없었다. 앞으로 계속 변하겠지만 그나마 처음부터 쓸 수 있게 만들어야 수정이 불가능해 작업물을 모두 버려야 하는 상황을 막을 수 있을 것 같았다. 따라서 형식에 제한이 없는 NoSQL로 기본 틀을 구성해 보기로 하였고 MongoDB의 Schema Design Best Practices를 참고했다.
MongoDB의 베스트 프랙티스는 일단 스키마를 잘 짜는 단 하나의 베스트 프랙티스는 없다고 한다. 그럼 어떻게 해야 될까? 다음과 같이 만들려는 서비스의 요구사항에 따라 달라진다.
그렇다면 MongoDB 스키마는 기존 관계형 DB 구조와 어떻게 다를까?
기존 관계형 스키마를 작성하기 위해선 데이터를 정규화(주로 제3정규화)해야 했다. 대략 이해한 내용은 데이터를 테이블들로 쪼개어 저장하기 위해선 정규화라는 처리가 필요하다. 이러한 처리 없이는 관계형 스키마에 데이터를 집어넣을 수 없다. 이렇게 쪼개어진 테이블은 Foreign Key를 통해 조인한다.
MongoDB 스키마는 이와 다르게 데이터를 쪼개지 않는다.
the only thing that matters is that you design a schema that will work well for your application.
제일 중요한 점은 당신의 어플리케이션에 잘 맞도록 스키마를 디자인하는 것입니다.
{
"first_name": "Paul",
"surname": "Miller",
"cell": "447557505611",
"city": "London",
"location": [45.123, 47.232],
"profession": ["banking", "finance", "trader"],
"cars": [
{
"model": "Bentley",
"year": 1973
},
{
"model": "Rolls Royce",
"year": 1965
}
]
}
위 예시와 같이 MongoDB 스키마는 하나의 object 형태로 저장할 수 있다. 그렇다면 데이터끼리 연결되는 경우는 어떤 식으로 가져와야 할까? 다음과 같이 두 가지 옵션이 있다.
Mongo DB에는 관계형 DB에서 사용하는 Join 개념이 없다. 대신 lookup 연산자를 사용할 수 있지만 이는 성능 문제로 권장되는 방법은 아니다. 언제 각 방법을 사용해야 할지는 다음의 지침을 참고했다.
다음의 rules of thumb도 참고하였다.
결론적으로는 'Embedding을 기본으로 하되, 지나치게 큰 크기의 데이터는 Referencing하라' 로 정리해 볼 수 있겠다.
그렇다면 위 내용을 기반으로 서비스의 스키마를 구성해 보자.
먼저 로그인 후엔 사용자라는 콜렉션에 관련 정보가 저장될 것이고, 사용자가 만들어지면 동시에 거래내역과 예산 리스트, 그리고 자산 리스트에 대한 콜렉션을 사용자의 id로 생성해 보려 한다(사용자와 연결시키기 위해). 사실 예산 관련 정보는 거의 모든 페이지에서 사용될 예정이라 사용자와 embedded 될 수도 있을 것 같지만 예산이 앞으로 계속 늘어난다면 massive array가 될 것 같아서 분리해 주었다. 거래내역은 엄청나게 양이 많을 것 같아서 콜렉션을 분리하였다.
// User
_id: 'objectId',
name: 'string',
createdAt: 'date',
// 모바일 앱 알림 관련 설정 - 어떤 구조로 잡아야 할지 아직 모르겠음
// 기타 금융정보?
// Budgets
_id: 'objectId', // = UserId
budgets: 'BudgetId[]',
createdAt: 'date',
// Transactions
_id: 'objectId', // = UserId
transactions: 'TransactionId[]',
createdAt: 'date',
고민중인 부분은 나의 자산 내역으로 사실 이 부분이 MVP에 속할지는 아직 불확실하다. 따라서 일단은 콜렉션만 존재하는 상태로 추후에 내용을 추가하기로 하였다.
// Assets
_id: 'objectId', // = UserId
// 어떻게 자산 내역을 가져올지 외부 API와 맞춰보아야 할 것 같다.
Assets:
createdAt: 'date',
이제 각 예산과 거래내역에 대한 콜렉션을 만들었다. 거래 내역같은 경우 외부 API의 형태에 맞춰 필요한 데이터만 다시 골라서 저장하면 어떨까 생각해보았다.
// Budget
_id: 'objectId',
name: 'string',
amount: 'number',
createdAt: 'date',
// Transaction
_id: 'objectId',
name: 'string',
// 금액
amount: 'number',
// 결제수단
paymentMethod: 'string',
// 카드결제인 경우 카드명
cardName: 'string',
// 카드결제인 경우 카드번호
cardNo: 'string',
// 할부 개월
InstallmentMonth: 'number',
// 가맹점 업종
storeType: 'string',
// 소비 카테고리
expenseCategoryType: 'string',
createdAt: 'date',
다음 글에선 홈 화면 UX에 대해 다시 고민해 보았습니다.
References
MongoDB Schema Design Best Practices
Embedded vs. Referenced Documents in MongoDB: How To Choose Correctly For Increased Performance