[SOPT 세미나] 서버 파트 3차 세미나 회고, MongoDB를 연결하고 API를 뚝딱뚝딱

SSO·2022년 5월 17일
0

SOPT 30기

목록 보기
4/9

👩‍💻 3차 세미나 회고

3차 세미나에서는 mongoDB와 CRUD API를 만들어 연결하는 것을 배웠다. 벌써 5차 세미나까지 했지만 이제야 3차 회고 적는 것은 안 비밀~.~ 다음 주에 솝커톤 한다고 복습 겸 부랴부랴 켠 것도 안 비밀 ㅎㅎㅎㅎ...

3차부터는 수업량도 많아지고 속도도 빨라졌다@! 중간에 오류나면 속도 못 따라가서 멘탈이 와장창..! 될 뻔 했지만 OB분이 도와주셔서 겨우 따라갔다 ㅋㅋ큐ㅠㅠ 그래도 어려웠던 만큼 어찌됐건 세미나 끝까지 무사히 따라 가서 뿌듯했다 :) 복습하자 복습~


💜 NoSQL vs SQL

SQL
RDBMS(관계형 데이터베이스 관리 시스템)을 위해 설계된 프로그래밍 언어

NoSQL
비관계형 데이터베이스로 비정형 데이터를 처리할 수 있는 유연한 스키마 제공

SQL은 말 그대로 쿼리 언어이다. 컴공 쪽으로 공부를 해보았다면 들어보았을 테이블 간 join, 테이블(Table), 로우(Row), 칼럼(Column)의 용어를 포함하고 있는 언어이다. 대표적으로 Select문, From절, Where절 등이 있다. (SQLD 자격증 후기도 얼른 써야겠다 😂) SQL은 테이블과 데이터 간의 관계가 중요하고 때문에 안정성도 높다.
그래서 은행 같이 데이터의 일관성이 중요한 기업에서 많이 쓰인다. 대표적으로 MySQL이 있다.

NoSQL은 SQL에 비해 유연성이 높다. collection과 데이터 간의 관계가 엄청 막 중요하진 않고 join도 없다. 때문에 비교적 쉽고 편하게 사용할 수 있다. SNS 같이 데이터 조회가 많이 이루어지는 서비스에서 많이 쓰인다.
SQL에서의 Table, Row, Column의 역할을 NoSQL에서는 Collection, Document, Field가 맡고 있다. 세미나에서 배운 MongoDB가 이에 속한다.

SQL 쪽은 공부해본 적이 있어서 수월하게 넘어갈 수 있었다 :)


💜 MongoDB?

NoSQL이 무엇인지는 알았으니 본격적으로 MongoDB를 알아보자.

Document 지향 NoSQL 데이터베이스 시스템
JSON 형태의 동적 스키마형 document 사용

위가 MongoDB의 간단한 설명이다. 여기서 Document라는 용어가 조금 생소했다.

Document

  • 데이터를 저장하는 최소 단위
  • 필드와 값의 쌍으로 구성
  • 입출력 시에 JSON, 저장 시에 BSON(Binary JSON)으로 변환
  • 관계를 갖는 데이터는 중첩을 활용하여 1개의 document로 나타냄.

파이썬의 딕셔너리와 비슷한 형식으로 이해했다.
SQL에서는 주요키, 외래키 등 테이블에서 테이블로의 관계 정의가 많았는데 NoSQL에서는 관계를 가져도 중첩을 통해 하나의 요소로 나타내는 점에서 확실히 유연하고 편하구나 싶었다.

MongoDB Atlas
🖕 들어가서 가입해주고 compass도 설치하고 플젝도 생성해주자 :)


💜 Mongoose

MongoDB ODM
비동기 환경에서 작동하도록 설계된 MongoDB 객체 모델링 도구
Promise, Call back 지원

ODM..? 처음 들어봤다 😲

ODM (Object Document Mapping)
객체와 Document를 매핑

ORM (Object Relational Mapping)
객체와 관계형 데이터베이스를 매핑

바로 설명해주셨다 ㅎㅎ
RDBMS에 익숙해서 ORM은 어느 정도 알고 있었는데 ODM은 처음 들어본다 했더니 NoSQL의 ORM 버전으로 이해했다 ㅎㅎ


💜 3-Layer Architecture

개인적으로 서버 개발자라면 무조건 알고 있어야 하는 개념이라고 생각한다. (물론 잘 알고 있는 건 아니다^^..) 근데 중요하니까 별꼬리 땅땅 잘 알아두자.

3개의 레이어, Controller, Service, Data Access가 있다. 요약하면,
Controller에서 Service를 호출하여 비즈니스 로직을 실행하면, Service는 호출에 반응하여 필요한 데이터를 저장 및 얻기 위해 Data Access에 접근한다. (여기서 Data Access는 MongoDB 같은 애들이라고 보면 됨)

Controller <=> Service <=> Data Access
간단하게 나타내면 요정도..?

Controller
Model이 처리하기 전에 1차적으로 데이터 가공
비즈니스 로직 수행 후 받은 데이터를 Response

Service
Controller에서 받아온 데이터를 가공하여 DB에 접근하여 결과값 호출
이러한 데이터 가공 작업이 비즈니스 로직

DB 쪽은 위에서 충분히 정리한 것 같으니, Controller와 Service에 대해 정리하자면 위와 같은 정도이다.

그럼! 왜! 3개의 레이어 관계가 중요하냐~~

  1. 독립적인 기능 수행
  2. 유지 보수
  3. 확장성, 유연성

대표적으로 3개의 이유이다. 기본적으로 아키텍처에서 비즈니스 로직이 독립적으로 분산되어야 확장성 및 유연성이 높아지고, 추후에 수정하는 등의 유지 보수도 수월해진다. (옷도 매칭할 때 옷장에 무더기로 쌓아두는 것보다 깔끔하게 종류별로 정리해서 두는 게 더 보기 좋고 쉽잖아요?)

정리한다. 3-Layer Architecture에서 비즈니스 로직이 어떻게 돌아가냐???

  • 비즈니스 로직은 Service Layer에서 기능한다. (구체적 로직 내용 포함)
  • Service Layer에서는 로직 작동만 할 뿐, request 및 response 데이터를 절대 직접 가져오지 않음
  • 그 데이터는 Controller Layer가 받음!

💜 DTO (Data Transfer Object)

계층 간 데이터 교환을 위해 사용하는 객체
TS에서는 Interface를 이용해 구현
Request-Controller-Service-Response 식으로 데이터 교환

쉽게 말해서 데이터를 주고 받는 형식으로 이해하면 된다.
만약 이름과 나이 데이터를 주고 받아야 한다면 Userinfo ( name: String, age: Int) 정도로 이해하면 된다.


💜 프로젝트 DB 연결

가보자 가보자 가보자 ~~
폴더 구조부터 살펴보자. 뭐가 많다 😲

config | configure 관리 폴더 (DB 연결 등 설정 관리로 보면 됨)
controllers | Controller 파일 관련
interfaces | DTO 파일 관련 (ts에서는 DTO를 interface로 구현)
loaders | DB Load 관련 (DB 실행 시 콘솔에 찍어주는 그거)
middlewares | 5차 세미나에서 계속 ㅎ.ㅎ
models | 스키마 관련
modules | 다양한 모듈 파일 관련
routes | Route 관련
services | Service 파일 관련

🧑‍💻 config

> src/config/index.ts


import dotenv from "dotenv";

// 현 서버가 production인지 development 환경인지
process.env.NODE_ENV = process.env.NODE_ENV || "development";

// .env 파일이 없으면 에러
const envFound = dotenv.config();
if (envFound.error) {
  throw new Error("⚠️  Couldn't find .env file  ⚠️");
}

// configure 설정
export default {
  // 사용 포트 번호
  port: parseInt(process.env.PORT as string, 10) as number,

  // MongoDB URI
  mongoURI: process.env.MONGODB_URI as string,
};

.env 파일을 통해 프로젝트 개발 환경을 설정하는 파일이다.


🧑‍💻 .env
보안상 공개되지 말아야 할 정보를 담은 파일이다. configure의 index.ts에서 보안 정보를 .env 파일에서 불러온다. (.gitignore에 반드시 포함할 것)

MONGODB_URI=mongodb+srv://<사용자ID>:<비밀번호>.w6r1r.mongodb.net/<DB이름>?retryWrites=true&w=majority
PORT=<포트번호>

위와 같이 .env 파일을 생성하여 작성해주면 된다.
MongoDB URI의 형식은 compass에서 connect 메뉴에 들어가 Connect your application을 클릭하여 uri string을 복사하면 된다.


🧑‍💻 loaders

> src/loaders/db.ts


import mongoose from "mongoose";
import config from "../config"; 

const connectDB = async () => {
  try {
  	// .env의 connect URI로 연결
    await mongoose.connect(config.mongoURI);

	// autoCreate : 서버 실행 시 Collection 자동 생성
    mongoose.set('autoCreate', true);

    console.log("Mongoose Connected ...");
    
  } catch (err: any) {
    console.error(err.message);
    process.exit(1);
  }
};

export default connectDB;

DB를 연결하는 파일로 이해하고 넘어가자.


🧑‍💻 util

> src/modules/util.ts


const util = {
    success: (status: number, message: string, data?: any) => {
        return {
            status,
            success: true,
            message,
            data,
        };
    },
    fail: (status: number, message: string, data?: any) => {
        return {
            status,
            success: false,
            message,
            data
        };
    },
};

export default util;

비즈니스 로직 실행 후 성공 및 실패 메시지를 위와 같은 형식으로 가공하여 반환한다.


💜 비즈니스 로직

회원 정보에 대한 비즈니스 로직을 만들어보자 >.<

🧑‍💻 Model

> src/models/User.ts


import mongoose from "mongoose";
import { UserInfo } from "../interfaces/user/UserInfo";

// 타입은 몽구스 홈페이지에서 참고해서 정확하게
const UserSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true
    }, 
    phone: {
        type: String,
        required: true
    },
    email: {
        type: String,
        required: true,
        unique: true
    }
});

export default mongoose.model<UserInfo & mongoose.Document>("User", UserSchema);

먼저 데이터 모델 스키마를 정의하자.

type : 데이터 타입 지정
required : 입력값을 반드시 받아야 하면 true
unique : 유일한 값이어야 하면 true

위와 같이 각 데이터에 타입과 옵션을 지정해준다.

하고 서버를 실행시키면 User Collection이 생성된 것을 compass에서 확인할 수 있다.


🧑‍💻 Interface

> src/interfaces/user/UserInfo.ts


export interface UserInfo {
    name: string;
    phone: string;
    email: string;
}

MongoDB에서는 DTO를 interface로 구현해준다고 했었다. User 스키마의 정보를 기본적으로 활용할 때 UserInfo 형식을 사용한다.

> src/interfaces/user/UserCreateDto.ts


export interface UserCreateDto {
    name: string;
    phone: string;
    email: string;
}

유저 정보를 입력값으로 받아올 때 UserCreateDto 형식으로 받아온다.

> src/interfaces/user/UserResponseDto.ts


import mongoose from "mongoose";
import { UserCreateDto } from "./UserCreateDto";

export interface UserResponseDto extends UserCreateDto {
    _id: mongoose.Schema.Types.ObjectId;
}

유저 정보를 조회할 때의 형식이다. 유저 데이터의 Id로 조회할 것이기 때문에, UserCreateDto를 확장하여 _id 필드를 추가하여 유저의 아이디와 정보를 담는다.

> src/interfaces/user/UserUpdateDto.ts


export interface UserUpdateDto {
    // update 들어올 수도 있고 아닐 수도 있음 -> optional
    name?: string;
    phone?: string;
    email?: string;
}

유저 정보를 업데이트 할 때의 입력값을 받아오는 형식이다. 정보를 수정할 때 선택적으로 필드를 수정할 수 있으므로 모든 필드를 nullable로 정의해준다.

🧑‍💻 Create
본격적으로 Create API를 만들어보자.

> src/controllers/UserController.ts


/**
 *  @route POST /user
 *  @desc Create User
 *  @access Public
 */
const createUser = async (req: Request, res: Response) => {
	// User Create Dto로 req.body 받아옴.
    const userCreateDto: UserCreateDto = req.body; 
    
    try {
        const data: PostBaseResponseDto = await UserService.createUser(userCreateDto);
        
        res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, message.CREATE_USER_SUCCESS, data));
    } catch (error) {
        console.log(error);
        // 서버 내부에서 오류 발생
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

// 반드시 해주어야 함 (안해주면 함수 호출 못 함)
export default {
    createUser
}

Controller에서 유저 정보를 Create하는 함수이다. UserService를 통해 Create 비즈니스 로직을 호출하고 성공 및 실패 메세지를 res를 통해 전달한다.

> src/services/UserService


const createUser = async (userCreateDto: UserCreateDto): Promise<PostBaseResponseDto|null> => {
    try {
        const user = new User({
            name: userCreateDto.name,
            phone: userCreateDto.phone,
            email: userCreateDto.email
        });

        await user.save(); // 유저 정보 저장

        const data = {
            _id: user.id
        };

        return data;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

export default {
    createUser
}

UserCreateDto.ts의 형식으로 입력값을 받아와 DB에 저장한다. 성공하면 data를, 실패하면 error를 던진다.


이렇게 Controller, Service, Interface를 서로 연결하여 하나의 API가 완성되었다. (야호!) 같은 방식으로 Read, Update, Delete API도 동일한 방식으로 비즈니스 로직을 구성하면 된다.

🧑‍💻 Read

const user = await User.findById(userId);

유저 id를 파라미터 값으로 받아와, findById로 유저의 Id로 정보를 찾아 조회할 수 있다.


🧑‍💻 Update

await User.findByIdAndUpdate(userId, userUpdateDto);

유저 id와 userUpdateDto 형식으로 입력값을 받아와, findByIdAndUpdate 함수로 유저 정보를 찾아 수정한다.


🧑‍💻 Delete

await User.findByIdAndDelete(userId);

파라미터 값으로 유저 id를 받아와, findByIdAndDelete로 유저 정보를 찾아 삭제한다.

🧑‍💻 Router
CRUD 데이터 처리 방식을 간단히 짚고 넘어가자.

Create: POST 방식
Read : GET 방식
Update : PUT 방식
Delete : DELETE 방식

> src/routers/UserRouter.ts


import { Router } from "express";
import {UserController } from "../controllers";
import User from "../models/User";
import { body } from "express-validator/check"

const router: Router = Router();

// route => use (/user) => post (/)
router.post('/', UserController.createUser);
router.put('/:userId', UserController.updateUser);
router.get('/:userId', UserController.findUserById);
router.delete('/:userId', UserController.deleteUser);

export default router;

/user 아래의 라우터를 지정하여 유저의 CRUD가 가능하도록 한다.

> src/routers/index.ts


//router index file
import { Router } from 'express';
import UserRouter from "./UserRouter";

const router: Router = Router();

router.use('/user', UserRouter);


export default router;

index.ts에서 루트(/)에서 유저 라우터를 연결해준다.


💜 API 실행 결과 (Thunder Client)

API 테스트를 해보면 성공적으로 이루어졌음을 확인할 수 있다. (password는 5주차에서~.~)


👩‍💻 Concluding

세미나에서 따라갈 땐 마냥 바빴지만 만든 API가 정상적으로 돌아가는 것보면 신기하기도 하고 뿌듯하기도 했다 >.<

회고하면서 API 만드는 것도 차근차근 다시 정리해볼 수 있었다!! 와아!~!

profile
쏘's 코딩·개발 일기장

0개의 댓글