Server/Node.js (Express)

[Node.js] CRUD: RDS를 이용하여 동적으로 구성하기 (1)

ooeunz 2019. 10. 30. 00:01
반응형

CRUD란?

CRUD는 대부분의 컴퓨터 소프트웨어가 가지는 기본적인 데이터 처리 기능인 Create(생성), Read(읽기), Update(갱신), Delete(삭제)를 묶어서 일컫는 말이다. 게시물을 올리고, 읽고, 수정하고, 삭제하는 것을 예로 생각하면 이해하기 편리하다.

 

이러한 CRUD는 http method get(read), post(create), put(update), delete(delete)로 구현할 수 있다. 각각의 http 메서드들은 해당하는 각각의 기능만 수행하는 것은 아니지만 통상적으로 rest api(참고: https://ooeunz.tistory.com/11?category=814267)를 따라 위와 같은 역할로 메서드들을 기능적으로 분리하여 사용한다.

 

이번 포스팅은 3번에 걸쳐서 각각의 기능들을 구현할 예정이고, 이번 포스팅에서는 전체적인 프로젝트의 구조와 CRUD를 구현해보도록 하겠다.

 

 


사전 준비

이전 포스팅에서는 사전에 세팅해야 할 개발 환경이 몇 가지 있다.

 

1. AWS RDS (Local DB를 사용할 것이라면 세팅하지 않아도 상관없다.)

※Reference: https://ooeunz.tistory.com/36?category=816210

 

[AWS] RDS 설정하기

Amazon Relational Database Service(Amazon RDS)란? RDS는 AWS Cloud에서 제공하는 관계형 데이터베이스이다. 쉽게 설치, 운영 및 확장할 수 있고, 필요한 용량을 조절할 수 있다. RDS 설정하기 EC2->[네트워크..

ooeunz.tistory.com

 

 

2. POSTMAN

http method를 쉽게 실습해 볼 수 있는 환경이다.

 

※ Download URL: https://www.getpostman.com/downloads/

 

Postman | The Collaboration Platform for API Development

Simplify workflows and create better APIs – faster – with Postman, a collaboration platform for API development.

www.getpostman.com

 

 


Create Project

먼저 프로젝트를 생성하도록 하겠다. (express와 non-blocking에 대한 이해가 없다면 앞선 포스팅을 먼저 선행 학습할 것을 권한다.)

터미널을 열은 후 프로젝트를 생성하기 원하는 프로젝트로 이동한 후 아래의 명령어를 입력한다.

express {프로젝트 이름}

 

그런 다음 습관적으로 다음 명령어를 입력한다. 프로젝트가 생성되면 json에 필수적인 npm 버전들이 나열되어 있을 뿐 moduel들이 직접적으로 install 된 환경이 아니다. 그러므로 프로젝트를 생성과 동시에 이 명령어를 입력하도록 하자. npm install  !!!

npm install

// permission error가 날 경우
sudo npm install

※ 사실 CRUD 포스팅을 보고 있는 독자들이라면 이미 알고 있을지도 모르겠지만 npm install은 npm i로 입력해도 똑같이 동작한다.

 

 


Directory 구조

이번 프로젝트는 MVC 디자인 패턴에 관한 개념이 들어간다. 프로젝트 디렉토리의 전반적인 설명을 앞서 하고 넘어가도록 하겠다.

(Reference: https://ooeunz.tistory.com/41)

 

  • config > dbConfig.js : 연결할 database에 관한 정보를 입력한다. 외부에 유출되어서 안 되는 file이다. (endpoint 및 password가 들어있음)
  • models > Blog.js : 데이터베이스에 읽고 쓰는 것과 같은 코드들이 들어간다. model을 분리하면 routes에서 어떤 방식으로 수행되는지 내부적으로 알 수없게 추상화되기 때문에 기능적으로 분리된다.
  • modules > db : routes와 기능을 분리하기 위해 만들어진 디렉토리이다. dbConfig.js에 있는 정보를 사용하여 database를 조작하기 위한 코드들이 구현되어 있다.
  • modules > util : Node.js는 디버깅이 힘들다. error 메시지가 친절하지 않기 때문이다. 그러한 고통을 조금 줄여주기 위해 error 메시지 한 부분을 조금 우리가 알아듣기 쉬운 형태로 작성해둔다.
  • .gitignore : git에 commit을 하게 될 경우 dbConfig.js와 같이 외부로 유출되면 안 되는 정보가 있는 file을 제외시키기 위한 파일이다.

 

 


MySQL Workbench

※ 설치와 세팅 방법은 위의 RDS 링크에 나와있음.

아래 이미지와 같이 table을 생성한다.

blogIdx 값은 blog가 생길 때 마다 자동으로 값이 올라가도록 Auto Increment로 설정한다.

 

 


config > dbConfig.js

const mysql = require('promise-mysql')

const dbConfig = {
    host: 'RDS 엔드 포인트',
    port: 3306,
    user: 'admin',
    password: 'RDS 비밀번호',
    database: 'RDS DB 이름',
    dateStrings: 'date',
}

module.exports = mysql.createPool(dbConfig)

보시다시피 유출되면 안되는 DB정보가 저장되는 파일이다. 유출되면 과금의 원인이 될 수 있으니 각별히 조심하도록 하자.

 

 


.gitignore (option)

githup에 commit 할 사람만 이 부분을 따라 하도록 하자. dbConfig.js는 유출되면 안 되는 파일이기 때문에 .gitignore 파일에 해당 파일을 commit에서 제외할 것을 명시해주는 것이 목적인 파일이다. 프로젝트 최상위 디렉토리이 .gitignore이라는 파일을 만들고 아래 이미지와 같이 입력해준다.

 

1번 라인은 config 디렉토리 및 모든 파일을 제외한다는 뜻이고, 2번 라인은 dbconfig.js라는 파일을 제외한다는 뜻이다. 둘 중 하나만 적용해도 기능은 같다.

 

 


여기는 복붙하기

앞서 설명한 modules 디렉토리를 생성하고 그 안에 util과 db 디렉토리를 생성해준다. 그리고 여기에 관한 부분은 이번 포스팅의 목적에서 벗어나기 때문에 그냥 복붙해서 사용하도록 하겠다.

 

 

modules > db > pool.js

const poolPromise = require('../../config/dbConfig');

module.exports = {
    queryParam_None: async (...args) => {
        const query = args[0]
        let result
        const pool = await poolPromise;
        try {
            var connection = await pool.getConnection() // connection을 pool에서 하나 가져온다.
            result = await connection.query(query) || null // query문의 결과 || null 값이 result에 들어간다.
        } catch (err) {
            console.log(err)
            connection.rollback(() => {})
        } finally {
            pool.releaseConnection(connection) // waterfall 에서는 connection.release()를 사용했지만, 이 경우 pool.releaseConnection(connection) 을 해준다.
            return result
        }
    },
    queryParam_Arr: async (...args) => {
        const query = args[0]
        const value = args[1] // array
        let result
        try {
            var connection = await pool.getConnection() // connection을 pool에서 하나 가져온다.
            result = await connection.query(query, value) || null // 두 번째 parameter에 배열 => query문에 들어갈 runtime 시 결정될 value
        } catch (err) {
            connection.rollback(() => {})
            next(err)
        } finally {
            pool.releaseConnection(connection) // waterfall 에서는 connection.release()를 사용했지만, 이 경우 pool.releaseConnection(connection) 을 해준다.
            return result
        }
    },
    queryParam_Parse: async (inputquery, inputvalue) => {
        const query = inputquery
        const value = inputvalue
        let result
        try {
            var connection = await pool.getConnection()
            result = await connection.query(query, value) || null
            console.log(result)
        } catch (err) {
            console.log(err)
            connection.rollback(() => {})
            next(err)
        } finally {
            pool.releaseConnection(connection)
            return result
        }
    },
    Transaction: async (...args) => {
        let result = "Success"

        try {
            var connection = await pool.getConnection()
            await connection.beginTransaction()
            await args[0](connection, ...args)
            await connection.commit()
        } catch (err) {
            await connection.rollback()
            console.log("mysql error! err log =>" + err)
            result = undefined
        } finally {
            pool.releaseConnection(connection)
            return result
        }
    }
}

 


 

modules > util > authUtil.js

// authUtil.js 
module.exports = {
    successTrue: (message, data) => { 
        return { success: true, message: message, data: data } 
    },
    successFalse: (message) => { 
        return { success: false, message: message }
    }, 
}

 

 

 

modules > util > responseMessage.js

//responseMessage.js 
module.exports = {
    NULL_VALUE: "필요한 값이 없습니다.", 
    OUT_OF_VALUE: "파라미터 값이 잘못 되었습니다.",
    SIGN_UP_SUCCESS: "회원가입 성공", 
    SIGN_UP_FAIL: "회원 가입 실패", 
    SIGN_IN_SUCCESS: "로그인 성공",
    SIGN_IN_FAIL: "로그인 실패",
    ALREADY_ID: "존재하는 ID 입니다.",
    NO_USER: "존재하지 않는 유저 id 입니다.", 
    MISS_MATCH_PW: "비밀번호가 일치하지 않습니다",
    BOARD_CREATE_SUCCESS: "게시글 작성 성공", 
    BOARD_CREATE_FAIL: "게시글 작성 실패", 
    BOARD_READ_ALL_SUCCESS: "게시글 전체 조회 성공", 
    BOARD_READ_ALL_FAIL: "게시글 전체 조회 실패", 
    BOARD_READ_SUCCESS: "게시글 조회 성공", 
    BOARD_READ_FAIL: "게시글 조회 실패", 
    BOARD_UPDATE_SUCCESS: "게시글 수정 성공", 
    BOARD_UPDATE_FAIL: "게시글 수정 실패", 
    BOARD_DELETE_SUCCESS: "게시글 삭제 성공", 
    BOARD_DELETE_FAIL: "게시글 삭제 실패",
    NO_USER: "존재하지 않는 유저 입니다.", 
    NO_BOARD: "존재하는 게시글 입니다.", 
    MISS_MATCH_PW: "비밀번호가 일치하지 않습니다",
    INTERNAL_SERVER_ERROR: "서버 내부 오류" 
}

 

 

modules > util > statusCode.js

// statusCode.js
module.exports = { 
    OK: 200,
    CREATED: 201,
    NO_CONTENT: 204, 
    RESET_CONTENT: 205, 
    NOT_MODIFIED: 304, 
    BAD_REQUEST: 400, 
    UNAUTHORIZED: 401, 
    FORBIDDEN: 403,
    NOT_FOUND: 404, 
    INTERNAL_SERVER_ERROR: 500, 
    SERVICE_UNAVAILABLE: 503, 
    DB_ERROR: 600,
}

 

 

 


Models > Blog.js

포스팅 앞부분을 보면 이 프로젝트는 추상화의 개념이 들어간다고 했다. 추상화란 무엇일까?

위키백과는 추상화를 "컴퓨터과학에서 추상화(abstraction)는 복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 말한다."라고 설명한다. 쉽게 말해 핵심적인 로직은 models안에 있는 코드가 수행하고 routes는 models에서 수행한 결과만을 받아서 (어떤 식 수행되었는지 routes는 알지 못함. routes 입장에서 models는 추상화됨) routes 본연의 일에만 충실하게 된다.

 

물론, models에 있는 코드를 분리하지 않고 routes에 포함시켜도 코드는 잘 돌아간다. 하지만 그렇게 되면 각 기능들의 경계가 모호해지고, 프로젝트의 규모가 커질수록 코드의 유지관리가 힘들어질 수 있다. 그러므로 기능적으로 추상화시키는 연습을 하는 것이 중요하다.

 

그럼 시작해보도록 하겠다.

먼저 routes에 blogs.js 파일을 만들어주고 index.js에서 blogs.js를 라우팅 해준다. (여기까진 앞선 포스팅에 나왔던 부분이기 때문에 앞으로는 이미지와 코드로 설명은 하지 않도록 하겠다.)

 

 

우리는 blogs.js 하나에 CRUD를 모두 포함시켜 구현할 것이다. rest api의 크나 큰 장점 중 하나는 동일한 url로 get, post, put, delete를 모두 다르게 구현할 수 있다는 것이다. 때문에 url의 네이밍과 메서드만을 보고 대략 이 코드가 어떤 일을 하는지 알 수 있다.

 

다음으로 moduels 디렉토리를 만들고 Blog.js라는 파일을 만들어준다. 앞으로 Blog.js는 앞으로 실질적인 로직을 수행하여 그 결과 값을 blogs.js에 리턴해 줄 것이다.

 

그럼 이제 models의 코드를 보도록 하겠다.

가장 먼저 할 일은 아까 modules 디렉토리에 복붙한 모듈들을 불러오는 것이다. au, rm, sc는 에러 처리와 상태 메시지를 위해 사용할 것이고, pool은 이야기했듯, dbConfig.js의 정보를 받아와 실질적으로 db에 접근해주는 모듈이다.

 

// global variable

const table = 'blog'는 그냥 전역 변수이다. 앞으로 db에 접근할 때마다 해당 변수를 사용해야 하는데, 그때마다 새로 변수를 선언해도 되지만 코드 베이스를 줄이기 위해 필자는 전역 변수로 초기화하였다.

 

이제 본격적으로 CRUD 기능을 구현해 보겠다. 사실 model에서 CRUD의 역할은 거의 비슷하기 때문에 대표적으로 CREATE 부분만을 다루도록 하겠다. (전체 코드는 아래에 삽입해 뒀다.)

 

Blog.js는 로직을 수행하고 blogs.js로 export 하는 것이 목적이기 앞으로 구현할 객체 자체를 moduel.exports 하도록 한다. export 되는 구조는 객체이고 그 객체 안에 값들에 함수가 함수 선언식을 통해 적용되는 형태이다.

 

그럼 차례차례 한 줄씩 살펴보도록 하겠다.

  • 11번: insert 함수 내에는 특별히 비동기 처리를 할만한 코드가 없지만 pool.queryParam_None() 메서드가 비동기로 구현되었기 때문에 이를 비동기로 값을 처리하기 위해 화살표 함수 앞에 async를 붙여준다.
  • 12번, 13번: 둘 다 14번 query문에 들어갈 값들을 초기화하는 코드이다. 12번 라인은 고정적으로 blonName column에 들어갈 것이기 때문에 문자열 형태로 초기화하고, 13번 라인은 매번 저장하는 값이 다르기 때문에 변수 값을 넣어준다.
  • 14번: 그렇게 완성된 query문의 형태는 이와 같을 것이다. INSERT INTO blog (title, content) VALUE('블로그 이름')
    (title 변수는 아까 전역 변수로 초기화했었다.)
  • 15번: 완성된 query문을 pool.queryParam_None() 메서드에 파라미터로 넣어주고, 에러 확인을 위해 result 변수에 값을 넣어준다.
  • 18번 ~ 22번: result가 없을 경우(error가 발생했을 경우) 객체 형태로 code 변수와 json변수에 module로 받아왔던 적적한 에러 메시지를 초기화해서 return 해 준다.
  • 24번 ~ 28번: if문에서 error가 걸리지 않았다면 code와 json에 성공에 관한 값들을 넣어준다.

 

여기서 return 된 값들은 routes에서 받게 될 텐데 헷갈릴 독자들을 위해 미리 잠깐 설명을 하고 넘어가도록 하겠다.

res.status(code).send(json);     // routes에서 받게 되는 형태

 

sc.OK 값은 200이다. 이 값은 routes에서 res.status(200)의 형태로 들어가게 될 것이고, (200은 성공을 의미한다) 뒤이어서 오는 .send를 실행시킨다.

 

au가 successTrue일 경우에는 False와 다르게 파라미터로 data값을 함께 받도록 되어있다. 따라서 send() 함수를 통해 어떤 값이 insert 되었는지 확인하게 될 것이다.

 

 

 

Blog.js

const au = require('../modules/util/authUtil');
const rm = require('../modules/util/responseMessage');
const sc = require('../modules/util/statusCode');
const pool = require('../modules/db/pool');

// global variable	
const table = 'blog';

// exports
module.exports = {
    insert: async (blogName) => {
        const fields = 'blogName';
        const questions = `'${blogName}'`;
        const query = `INSERT INTO ${table} (${fields}) VALUES(${questions})`;
        const result = await pool.queryParam_None(query);
        
        // running
        if (!result) {
            return {
                code: sc.BAD_REQUEST,
                json: au.successFalse(rm.BOARD_CREATE_FAIL)
            };
        }
        return {
            code: sc.OK,
            json: au.successTrue(rm.BOARD_CREATE_SUCCESS, result)
        };
    },
    selectOne: async (blogIdx) => {
        const query = `SELECT * FROM ${table} WHERE blogIdx = '${blogIdx}'`;
        const result = await pool.queryParam_None(query);

        if (!result) {
            return {
                code: sc.BAD_REQUEST,
                json: au.successFalse(rm.BOARD_READ_FAIL)
            };
        }
        return {
            code: sc.OK,
            json: au.successTrue(rm.BOARD_READ_SUCCESS, result)
        };
    },
    selectAll: async () => {
        const query = `SELECT * FROM ${table}`;
        const result = await pool.queryParam_None(query);

        // running
        if (!result) {
            return {
                code: sc.BAD_REQUEST,
                json: au.successFalse(rm.BOARD_READ_ALL_FAIL)
            };
        }
        return {
            code: sc.OK,
            json: au.successTrue(rm.BOARD_READ_ALL_SUCCESS, result)
        };
    },
    update: async (blogIdx, blogName) => {
        const query = `UPDATE ${table} SET blogName = '${blogName}' WHERE blogIdx = ${blogIdx}`;
        const result = await pool.queryParam_None(query);

        // running
        if (!result) {
            return {
                code: sc.BAD_REQUEST,
                json: au.successFalse(rm.BOARD_UPDATE_FAIL)
            };
        }
        return {
            code: sc.OK,
            json: au.successTrue(rm.BOARD_UPDATE_SUCCESS, result)
        };
    },
    delete: async (blogIdx) => {
        const query = `DELETE FROM ${table} WHERE blogIdx = ${blogIdx}`;
        const result = await pool.queryParam_None(query);

        // running
        if (!result) {
            return {
                code: sc.BAD_REQUEST,
                json: au.successFalse(rm.BOARD_DELETE_FAIL)
            };
        }
        return {
            code: sc.OK,
            json: au.successTrue(rm.BOARD_DELETE_SUCCESS, result)
        };
    }
}

 

 

 


Routes > blogs.js

blogs.js도 전체적인 구조는 비슷하기 때문에 CREATE 부분만 다루도록 하겠다.

13번: post 형식으로 받아온 값을 blogName 변수에 초기화한다.

16번: blogName의 값이 비어있다면 "값이 비어있다는" 에러 메시지를 출력한다.

20번: model에 정의했던 insert 메서드를 호출한다. 이때 await을 붙여주지 않으면 비동기적으로 처리되지 않아서 promise 객체가 return 되니 주의하자!

22번: 위에서 설명했던 코드.

23번~: 에러 메시지를 설명하는 코드.

 

 

blog.js

const express = require('express');
const router = express.Router({mergeParams: true})

const au = require('../../modules/util/authUtil');
const rm = require('../../modules/util/responseMessage');
const sc = require('../../modules/util/statusCode');

const Blog = require('../../models/Blog');

router.post('/', async (req, res) => {
    const {blogName} = req.body;

    // TODO 1: blogName 값 확인하기
    if (!blogName) res.status(sc.BAD_REQUEST).send(au.successFalse(rm.NULL_VALUE));

    // TODO 2: 작성하기
    try {
        const {code, json} = await Blog.insert(blogName);
        res.status(code).send(json);
    } catch (err) {
        console.log(err);
        res.status(sc.INTERNAL_SERVER_ERROR).send(au.successFalse(rm.BOARD_CREATE_FAIL));
    }
});

// READ_ALL
router.get('/', async (req, res) => {
    // TODO 1: 읽어오기
    try {
        const {code, json} = await Blog.selectAll();
        res.status(code).send(json);
        
    } catch (err) {
        console.log(err);
        res.status(sc.INTERNAL_SERVER_ERROR).send(au.successFalse(rm.BOARD_READ_ALL_FAIL));
    }
});

// READ_ONE
router.get('/:blogIdx', async (req, res) => {
    const blogIdx = req.params.blogIdx;

    // TODO 1: blogIdx 값 확인하기
    if (!blogIdx) res.status(sc.BAD_REQUEST).send(au.successFalse(rm.NULL_VALUE));

    // TODO 2: 읽어오기
    try {
        const {code, json} = await Blog.selectOne(blogIdx);
        res.status(code).send(json);
    } catch (err) {
        console.log(err);
        res.status(sc.INTERNAL_SERVER_ERROR).send(au.successFalse(rm.BOARD_READ_ALL_FAIL));
    }
});

// UPDATE
router.put('/:blogIdx', async (req, res) => {
    const {blogIdx} = req.params;
    const {blogName} = req.body;

    // TODO 1: blogIdx, blogName 값 확인하기
    if (!blogIdx || !blogName) res.status(sc.BAD_REQUEST).send(au.successFalse(rm.NULL_VALUE));
    
    // TODO 2: 수정하기
    try {
        const {code, json} = await Blog.update(blogIdx, blogName);
        res.status(code).send(json);
    } catch (err) {
        console.log(err);
        res.status(sc.INTERNAL_SERVER_ERROR).send(au.successFalse(rm.BOARD_UPDATE_FAIL));
    }
});

// DELETE
router.delete('/:blogIdx', async (req, res) => {
    const {blogIdx} = req.params;

    // TODO 1: blogIdx 값 확인하기
    if (!blogIdx) res.status(sc.BAD_REQUEST).send(au.successFalse(rm.NULL_VALUE));

    // TODO 2: 삭제하기
    try {
        const {code, json} = await Blog.delete(blogIdx);
        res.status(code).send(json);
    } catch (err) {
        console.log(err);
        res.status(sc.INTERNAL_SERVER_ERROR).send(au.successFalse(rm.BOARD_DELETE_FAIL));
    }
});

module.exports = router;

 

 


Postman으로 확인해보기

node server를 동작 시킨 후에 postman으로 아래와 같이 CRUD가 잘 동작하는지 확인할 수 있다.

 

좌: post, 중: get, 우: get/:idx

 

좌: put, 우: delete

반응형