본문 바로가기
프로젝트

심부릉 앱 DB 설계

by hyunji00pj 2024. 12. 2.

mongoDB 설계

먼저 DB에 저장할 데이터를 아래에 정리해 보았다

 

로그인 로그아웃 회원가입 아이디 찾기 비밀번호 찾기

 

아이디 찾기는 이름과 휴대폰 인증을 통해 찾고 비밀번호는 이름과 아이디와 휴대폰 인증을 통해 찾는다

회원가입은 아이디와 비밀번호 비밀번호 확인 이름 생년월일 성별 휴대폰 인증 약관 동의를 해야한다

 

심부름 수행을 할 수 있는 파트너십 가입 은 신분증 전체 사진과 이름 주민등록번호 메일주소를 입력후 확인버튼을 누르고 프로필 등록을 위한 본인확인 정면 사진과 입금 계좌 정보(은행 계좌번호 예금주) 주로 수행할 심부름 종류 고르기, 필수정보인 배달 수단, 경력, 자격증,사업자 여부를 추가할 수 있다         

 

유저 프로필에는 파트너십 가입 여부에 따라 가입 전 가입 중 가입 후로 나뉘며 심부름 대행 활동 횟수 심부름 요청 횟수 나에대한 리뷰 평점, 자기소개,제공 심부름 종류, 이동수단,받은 거래 후기가 표기되며

 

내정보 메뉴에는 공지사항,찜목록,자주하는 질문,설정,회원 정보 조회 및 수정,약관 및 정책 페이지가있다

 

메인 화면에는 알림과 검색창이 있고 심부름 종류별 카테고리와 유형에 따라 심부름 요청 개시물을컨텐츠 영역에서 필터해서 볼 수 있다

 

각 게시물박스 컨텐츠에는 해당 게시물의 심부름 유형 아이콘과 제목,유저와의 거리 간격,위치,시간,가격,몇명이 댓글을 달았는지 몇명이 찜했는지 보여준다

 

게시물 상세페이지에는 해당 심부름을 요청한 유저 프로필과 유저 닉네임,요청사항,거리,찜 여부,세부 설정,Q&A 답글 단 유저 컨텐츠에는 해당 유저 프로필과 텍스트가 있으며 내 글에만 삭제하기 버튼이 활성화 된다

 

채팅은 요구와 수행 카테고리로 나누어져있으며 채팅은 소켓방식으로 데이터를 주고 받는다 채팅의 읽음 안읽음 표시도 해준다 채팅창 세부페이지 내에서는 심부름의status(거래중,거래완료),가격을 표기하며 아래엔 인풋 박스가 있다

Embedding


장점

  • 하나의 쿼리문으로 필요한 모든 데이터를 가져올 수 있습니다.
  • $lookup 과 같은 Join 동작을 수행하지 않고 데이터를 가져올 수 있습니다.

한계점

  • Document가 커지면 오버헤드 또한 함께 커진다 (문서의 크기를 제한하여 쿼리 성능도 챙길 수 있습니다.)
  • Document는 16mb 의 크기 제한을 갖고 있어서 Embedding방식을 계속 사용한다면 언젠간 한계에 도달할 수도 있습니다.

Referencing


참조형 같은 경우 각각의 Document들이 가지고 있는 특별한 id 인 object id  $lookup 을 이용해 데이터를 참조할 수 있습니다.

이는 관계형 데이터베이스에서의 JOIN  흡사하게 동작하며, 이러한 구조는 데이터를 효율적이고 관리하기 쉽게 나누면서도 데이터 간의 관계를 유지할 수 있습니다.

장점

  • Document를 쪼개어 더욱 작은 단위의 Document를 가질 수 있으며, 이를 통해 16mb 크기 제한을 피할 수 있습니다.
  • 필요하지 않은 정보에 대한 접근을 줄일 수 있습니다.
  • Document 를 참조하여 데이터를 가져오기에 Embedding 방식보다 데이터의 중복을 줄일 수 있습니다.
  • Document 를 나눠야 할 이유가 명확하지 않다면 Embedding 하여 사용하자.
  • Document 값 중 배열의 경우 제한 없이 데이터가 추가되다 보면 16mb 를 넘길 수 있기에 조심하자.
  • 원래는 Chat  One-to-Many 구조로 하려고 하였으나 글을 읽고나니 One-to-Squillions 구조로 설계해야하는 이유를 알게됐다.

One-to-One : key-value 선호

One-to-Few : Embedding 선호

One-to-Many : Embedding 선호

One-to-Squillions : Referencing 선호

Many-to-Many : Referencing 선호
참조 https://gamguma.dev/post/2022/04/mongodb_schema_design

 

어떻게 mongoDB 스키마 설계를 해야할까?

스포하자면 만들고자 하는 서비스에 알맞게 만들어야한다(?) 당연한 것 아닙니까?,,

gamguma.dev

one-to-one

 

User

{
    "_id": "ObjectId('mdkalsfmk2')",
    "nickname": "hyunji",
    "campus": "pizza school",
}

위와 같은 User document 가 있을 때, nickname 이나 campus 와 같이 하나만 존재하는 값이 1:1 관계며, key-value 로 모델링 할 수 있습니다.

one-to-few

{
    "_id": "ObjectId('mdkalsfmk2')",
    "nickname": "junseo",
    "campus": "42seoul",
    "projects": [
        { "name": "42WE", "isDone": false},
        { "name": "Decrypto", "isDone": true}
    ]
}

위와 같은 User document 에서 projects 의 타입은 배열이며 2개의 값을 가지고 있습니다. 이렇듯 User document 에서 projects  One-to-Few 관계입니다.

One-to-Few 처럼 몇 개의 데이터만 가지고 있을 경우엔 값을 Embedding 구조로 설계합니다.

 

one-to-many (자식 참조)

{
    "_id": "ObjectId('mdkalsfmk2')",
    "nickname": "junseo",
    "campus": "42seoul",
    "projects": [
        { "name": "42WE", "isDone": false},
        { "name": "Decrypto", "isDone": true}
    ],
    "creditCard": ["ObjectID('1234')", "ObjectID('4321')", "ObjectID('9399')"]
}
{
    "_id": "ObjectId('4321')",
    "bank": "kakao",
    "isActive": "true",
    "accountNumber": "1234-56-7891234"
}

위의 User document 에서 creditCard  Card 스키마로 생성된 한 Document 를 참조하는 중입니다.

이처럼 한 유저가 여러 카드를 갖고 있으며 따로 관리가 필요할 때, 관계가 필요할 때 One-to-Many 구조로 설계합니다.

 

one-to-squilions (부모참조)

 

{
    "_id": "ObjectId('4321')",
    "createdAt": ISODate("2021-04-28T09:42:41.382Z"),
    "users":"???",
}
{
    "_id": "ObjectId('328492489')",
    "chatRoom_id": "ObjectId('4321')", // 부모의 obj id 를 참조
    "createdAt": ISODate("2021-04-29T12:42:41.382Z"),
    "content":"피곤쓰",
}

이러한 채팅방, 채팅 스키마를 디자인할 때, One-to-Many 방식에서 Embedding 구조를 사용한다면, 채팅 데이터가 300개 넘어갔을 때 Document  16mb 제한을 초과해 버릴 것입니다. 더불어 Referencing 구조를 사용한다 하더라도 ObjectID 가 몇 천 개 쌓인다면 똑같이 용량 초과될 것입니다.

One-to-Squillions 구조를 사용한다면 채팅방의 ObjectID 로 해당 채팅방에서 주고받은 데이터를 쿼리 할 수 있게 되고, 용량 제한을 피하면서도 채팅방과 채팅의 관계를 유지시킬 수 있습니다.

 

MongoDB에서 심부릉 앱의 데이터베이스 설계에서 각 컬렉션 간의 관계를 One-to-One, One-to-Few, One-to-Many, One-to-Squillions, Many-to-Many 중 어떤 방식으로 설계할지 설명하겠습니다.


1. Users ↔ PartnershipDetails (One-to-One)

  • 관계 설명:
    • 한 사용자는 최대 하나의 파트너십 정보만 가질 수 있습니다.
    • 예: 파트너십 가입 정보(신분증 사진, 계좌 정보 등)는 사용자마다 하나만 존재.
  • MongoDB 방식:
    • User 컬렉션의 필드로 partnershipDetails를 포함하여 임베딩 처리.
    • 관계가 단순하고 항상 함께 조회되므로 One-to-One이 적합.

2. Users ↔ Tasks (One-to-Many)

  • 관계 설명:
    • 한 사용자는 여러 개의 심부름 요청(Task)을 생성할 수 있습니다.
    • 예: 사용자가 요청한 모든 심부름 내역을 가져와야 할 경우.
  • MongoDB 방식:
    • Tasks 컬렉션에서 requester 필드에 사용자 ID를 참조.
    • User는 여러 Task를 가질 수 있으므로 One-to-Many 관계.
  • 적용 이유:
    • 심부름 요청 데이터(Task)는 독립적으로 관리되며 사용자와 연결됩니다.

3. Tasks ↔ Comments (One-to-Many)

  • 관계 설명:
    • 하나의 심부름 요청(Task)에는 여러 개의 댓글(Comment)이 작성될 수 있습니다.
    • 예: 한 게시물(Task)에 댓글이 여러 개 달리는 경우.
  • MongoDB 방식:
    • Comments 컬렉션에서 taskId 필드로 심부름 ID를 참조.
    • 댓글이 많을 수 있으므로 One-to-Many 관계.
  • 적용 이유:
    • 댓글(Comment)은 독립적으로 관리되며, 특정 Task에만 속합니다.

4. Users ↔ Comments (One-to-Many)

  • 관계 설명:
    • 한 사용자는 여러 개의 댓글을 작성할 수 있습니다.
    • 예: 사용자가 작성한 모든 댓글을 가져와야 할 경우.
  • MongoDB 방식:
    • Comments 컬렉션에서 author 필드로 사용자 ID를 참조.
    • One-to-Many 관계.

5. Tasks ↔ Chats (One-to-One or One-to-Few)

  • 관계 설명:
    • 하나의 심부름 요청(Task)에 대해 하나의 채팅방(Chat)을 생성하거나, 특정 조건에서 몇 개의 채팅방만 연관될 수 있습니다.
    • 예: Task 수행자와 요청자 간 1:1 채팅.
  • MongoDB 방식:
    • Chats 컬렉션에서 roomId로 Task ID를 참조.
    • 기본적으로 One-to-One 관계이지만, 경우에 따라 One-to-Few로 확장 가능.

6. Chats ↔ Messages (One-to-Many or One-to-Squillions)

  • 관계 설명:
    • 하나의 채팅방(Chat)에 여러 메시지(Message)가 포함될 수 있습니다.
    • 메시지의 개수가 많아질 수 있으므로, 잠재적으로 One-to-Squillions 관계.
  • MongoDB 방식:
    • Chats 컬렉션에 메시지 배열을 임베딩하거나, Messages 컬렉션에서 chatId를 참조.
    • 메시지가 많아질 가능성이 크므로 One-to-Squillions 방식.

7. Users ↔ Chats (Many-to-Many)

  • 관계 설명:
    • 한 사용자는 여러 채팅방에 참여할 수 있고, 한 채팅방에는 여러 사용자가 포함될 수 있습니다.
    • 예: 심부름 요청자와 수행자 모두 같은 채팅방에서 대화.
  • MongoDB 방식:
    • Chats 컬렉션에서 users 필드에 사용자 ID 배열을 임베딩.
    • 관계를 직접적으로 표현할 필요가 있는 경우에는 중간 컬렉션 생성 가능.
    • Many-to-Many 관계.

8. Users ↔ Notifications (One-to-Many)

  • 관계 설명:
    • 한 사용자는 여러 알림(Notification)을 받을 수 있습니다.
    • 예: 요청이 수락되거나 완료될 때 알림 전송.
  • MongoDB 방식:
    • Notifications 컬렉션에서 userId 필드로 사용자 ID를 참조.
    • One-to-Many 관계.

9. Tasks ↔ Likes & Reviews (One-to-Many)

  • 관계 설명:
    • 하나의 심부름 요청(Task)에 여러 사용자가 "찜"하거나 리뷰를 작성할 수 있습니다.
  • MongoDB 방식:
    • Likes와 Reviews를 별도의 컬렉션으로 분리하고 taskId로 참조.
    • One-to-Many 관계.

최종 관계 요약

Users: 사용자 정보 저장

사용자 프로필, 로그인 정보, 회원가입 관련 데이터를 저장.

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
    username: { type: String, required: true, unique: true }, // 아이디
    password: { type: String, required: true }, // 암호화된 비밀번호
    name: { type: String, required: true }, // 이름
    birthdate: { type: Date, required: true }, // 생년월일
    gender: { type: String, enum: ['male', 'female', 'other'], required: true }, // 성별
    phone: { type: String, required: true }, // 휴대폰 번호
    email: { type: String }, // 이메일 주소
    agreements: {
        terms: { type: Boolean, required: true }, // 약관 동의 여부
        marketing: { type: Boolean, required: false }, // 마케팅 동의 여부
    },
    partnershipStatus: {
        type: String,
        enum: ['none', 'pending', 'approved'], // 파트너십 상태: 가입 전/가입 중/가입 후
        default: 'none',
    },
    partnershipDetails: {
        idCardPhoto: { type: String }, // 신분증 사진 URL
        personalPhoto: { type: String }, // 본인확인 사진 URL
        account: {
            bank: { type: String }, // 은행 이름
            accountNumber: { type: String }, // 계좌번호
            accountHolder: { type: String }, // 예금주
        },
        taskPreferences: [String], // 주로 수행할 심부름 종류
        deliveryMethod: { type: String }, // 배달 수단
        experience: { type: String }, // 경력
        certificates: [String], // 자격증
        businessOwner: { type: Boolean, default: false }, // 사업자 여부
    },
    profile: {
        tasksCompleted: { type: Number, default: 0 }, // 심부름 대행 활동 횟수
        tasksRequested: { type: Number, default: 0 }, // 심부름 요청 횟수
        reviews: {
            rating: { type: Number, default: 0 }, // 평점
            count: { type: Number, default: 0 }, // 리뷰 개수
        },
        bio: { type: String }, // 자기소개
        services: [String], // 제공 심부름 종류
        transport: { type: String }, // 이동수단
        feedback: [String], // 받은 거래 후기
    },
    createdAt: { type: Date, default: Date.now }, // 가입 날짜
});

module.exports = mongoose.model('User', userSchema);

Tasks (심부름 요청): 심부름 요청 정보

심부름 게시물 데이터를 저장합니다.

const mongoose = require('mongoose');

const taskSchema = new mongoose.Schema({
    requester: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, // 요청자 ID
    title: { type: String, required: true }, // 제목
    description: { type: String, required: true }, // 요청 사항
    location: {
        address: { type: String }, // 위치
        distance: { type: Number }, // 요청자와의 거리
    },
    price: { type: Number, required: true }, // 심부름 가격
    category: { type: String, required: true }, // 심부름 유형
    likes: { type: Number, default: 0 }, // 찜 수
    comments: { type: Number, default: 0 }, // 댓글 수
    status: {
        type: String,
        enum: ['open', 'in_progress', 'completed', 'cancelled'], // 심부름 상태
        default: 'open',
    },
    createdAt: { type: Date, default: Date.now }, // 생성 시간
});

module.exports = mongoose.model('Task', taskSchema);

Comments (댓글): 심부름 게시물에 달린 댓글

심부름 게시물과 연관된 댓글 데이터를 저장합니다.

const mongoose = require('mongoose');

const commentSchema = new mongoose.Schema({
    taskId: { type: mongoose.Schema.Types.ObjectId, ref: 'Task', required: true }, // 심부름 ID
    author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, // 댓글 작성자
    content: { type: String, required: true }, // 댓글 내용
    createdAt: { type: Date, default: Date.now }, // 작성 시간
});

module.exports = mongoose.model('Comment', commentSchema);

Chats (채팅): 채팅 데이터

소켓 기반의 채팅 데이터를 저장합니다.

const mongoose = require('mongoose');

const chatSchema = new mongoose.Schema({
    roomId: { type: String, required: true }, // 채팅방 ID
    users: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], // 참여자들
    messages: [
        {
            sender: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, // 메시지 발신자
            content: { type: String, required: true }, // 메시지 내용
            read: { type: Boolean, default: false }, // 읽음 여부
            createdAt: { type: Date, default: Date.now }, // 보낸 시간
        },
    ],
});

module.exports = mongoose.model('Chat', chatSchema);

Notifications (알림): 알림 데이터

사용자 알림 데이터를 저장합니다.

const mongoose = require('mongoose');

const notificationSchema = new mongoose.Schema({
    userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, // 알림 대상 사용자
    message: { type: String, required: true }, // 알림 메시지
    read: { type: Boolean, default: false }, // 읽음 여부
    createdAt: { type: Date, default: Date.now }, // 생성 시간
});

module.exports = mongoose.model('Notification', notificationSchema);

FAQ & Settings: 자주 묻는 질문, 약관 및 설정

FAQ와 정책 등은 정적 데이터로 관리하거나 CMS를 통해 별도 관리.

컬렉션 간의 관계

 

  • Users ↔ Tasks:
    • 사용자는 심부름 요청(Task)을 생성하고, 관련 데이터를 연관.
  • Users ↔ Comments:
    • 사용자는 게시물(Task)에 댓글(Comment)을 작성.
  • Users ↔ Chats:
    • 사용자는 여러 채팅(Chat)에 참여 가능.
  • Users ↔ Notifications:
    • 사용자는 다양한 알림(Notification)을 수신.
  • 회원가입:
    • POST /api/users/register
    • 필수 필드: 아이디, 비밀번호, 이름, 생년월일, 성별, 휴대폰 인증.

예시 요청 흐름

  • 심부름 요청:
    • POST /api/tasks
    • 필수 필드: 제목, 설명, 위치, 가격, 유형.
  • 댓글 추가:
    • POST /api/comments
    • 필수 필드: 심부름 ID, 댓글 내용.
  • 채팅 전송:
    • POST /api/chats/send
    • 필수 필드: 채팅방 ID, 메시지 내용.