Blog

[NestJS] 5. DB와 TypeORM 연동

Category
Author
citeFred
citeFred
PinOnMain
1 more property
NestJS, TypeORM 이해하기
Table of Content

1. 데이터베이스 환경 구축

1.1 데이터베이스란?

데이터베이스(Database)는 데이터를 체계적으로 저장하고 관리하는 시스템, 데이터의 집합을 이야기 한다.
데이터를 효율적으로 저장, 검색, 업데이트, 삭제할 수 있는 공간
데이터베이스는 다양한 응용 프로그램에서 필수적인 역할을 하며, 데이터를 안전하게 보관하고 필요한 사람이나 시스템이 쉽게 접근할 수 있도록 함
DBMS란? DBMS(Database Management System)
데이터베이스를 관리, 운영 해주는 소프트웨어
MySQL, PostgreSQL, Oracle, MongoDB 등이 대표적인 DBMS
RDBMS란?
RDBMS(Relational DBMS)
데이터를 테이블 형태로 저장하며, 테이블 간의 관계를 정의
SQL을 사용하여 데이터를 관리합니다.
MySQL, PostgreSQL, Oracle 등
NoSQL? Not Only SQL
비관계형 데이터베이스로, 다양한 데이터 모델(키-값, 문서, 그래프 등)을 지원
대규모 데이터를 처리할 때 사용
MongoDB, Cassandra, Redis

1.2 MySQL 설치

로컬 MySQL 설치
로컬 환경의 MySQL 데이터베이스를 구축하고 프로젝트와 연결하고자 한다. MySQL 설치와 초기 환경 설정은 다음 글을 참고하도록 한다.
로컬 환경 MySQL 설치는 위 문서를 참고하여 진행 할 수 있다. Windows 버전의 경우 보다 손쉽게 클라이언트를 공식 홈페이지를 통해 설치하여 사용 할 수 있다. 데이터베이스 GUI툴은 MySQL-Workbench 를 포함하여 수없이 많이 있지만 DBeaver 또한 직관적이고 라이트하여 사용하기 편리하다.
하지만 우선 CLI환경에서도 익숙해지는것도 필요하기 때문에 간단한 CLI로 진행하고자 한다.
우선 위 MySQL 로컬 데이터베이스 설치 및 실행이 완료되면 아래 과정을 진행한다.
MySQL에 데이터베이스 생성
프로젝트에서 사용할 MySQL 의 데이터베이스를 생성해야 한다.
터미널을 통해 mysql 콘솔에 접근한다.
mysql -u root -p
Shell
복사
mysql 콘솔로 접근하면 다음과 같이 데이터베이스를 생성하는 쿼리를 작성한다.
create database board_app;
SQL
복사

1.3 데이터베이스 개념

Database와 SQL

Database
개념
용어 정리
SQL
개념
DDL
DCL
DML

1.4 SQL 실습

SQL 연습하기

주요 SQL 정리
CREATE
ALTER
INSERT
UPDATE
DELETE
SELECT
JOIN

1.5 게시판 기능의 데이터베이스 연동

현재 서버의 임시 메모리 데이터베이스를 실제 MySQL 로컬 데이터베이스로 변경해야 한다.
Express.js 실습때 작성하던 코드의 형태를 기억하며 유사한 부분들이 적용 된다.

1.5.1 미들웨어(라이브러리 설치)

express에서 MySQL과 연결하기 위해서는 mysql2 미들웨어를 설치해야 한다.
npm install mysql2
Shell
복사

1.5.2 연결 정보 설정 및 준비

민감 정보 은닉을 위하여 dotenv 설치 및 NestJS의 설정을 위한 NestJS의 config 설치와 이후 의존성 설치 및 주입 관계 설정
npm install dotenv @nestjs/config
Shell
복사
database.config.ts 사용자 정의 데이터베이스 정보 작성
import * as dotenv from 'dotenv'; dotenv.config(); export const databaseConfig = { host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PW, port: +process.env.DB_PORT, //+는 문자->숫자로 바꿔줌 database: process.env.DB_NAME, connectionLimit: 10, insecureAuth: true, };
TypeScript
복사
.env 파일 생성 및 DB 연결 정보 설정
DB_HOST="localhost" DB_USER="root" DB_PW="1234" DB_PORT=3306 DB_NAME="board_app"
TypeScript
복사
app.module.ts DB 의존성 설정
import { Module } from '@nestjs/common'; import { BoardsModule } from './boards/boards.module'; import { ConfigModule } from '@nestjs/config'; import { databaseConfig } from './configs/database.config'; @Module({ imports: [ ConfigModule.forRoot(), BoardsModule ], controllers: [], providers: [ { provide : 'DATABASE_CONFIG', useValue: databaseConfig } ], exports: ['DATABASE_CONFIG'] }) export class AppModule {}
SQL
복사
board.module.ts 에 Repository 클래스 의존성 주입 준비 등록
import { Module } from '@nestjs/common'; import { BoardsController } from './boards.controller'; import { BoardsService } from './boards.service'; import { BoardsRepository } from './boards.repository'; @Module({ controllers: [BoardsController], providers: [BoardsService, BoardsRepository] }) export class BoardsModule {}
SQL
복사
_MySQL 데이터베이스의 기본 테이블 생성(엔터티 기준)
CREATE TABLE board ( id INT AUTO_INCREMENT PRIMARY KEY, author VARCHAR(255), title VARCHAR(255), contents TEXT, status ENUM('PUBLIC', 'PRIVATE') );
SQL
복사

1.5.3 데이터베이스 연동 Service계층의 Repository 부분 분리

우선 MVC 패턴과 3 Layer 개념을 적용한 데이터베이스 액세스 계층을 최대한 분리해본다.
Service의 Repository 계층 분리 boards.service.ts
우선 기본 테스트가 될 수 있도록 Create 기능을 분리하고 Read 기능을 분리해보고자 한다.
새로 생성된 Repository 계층을 호출(사용)해야 하기 때문에 DI(의존성 주입)이 필요하다.
Service 계층이 Repository 계층을 사용하는 관계이므로 생성자 주입을 통해서 주입 받는다.
기존 코드와 크게 변경되지 않지만, 데이터베이스 쿼리를 실행하던 소스코드가 Repository로 이동하게 된다.
import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { Board } from './boards.entity'; import { BoardStatus } from './boards-status.enum'; import { CreateBoardDto } from './dto/create-board.dto'; import { UpdateBoardDto } from './dto/update-board.dto'; import { BoardsRepository } from './boards.repository'; @Injectable() export class BoardsService { // DB Access (Repository 계층) constructor(private boardsRepository: BoardsRepository) {} // 게시글 조회 기능 async getAllBoards(): Promise<Board[]> { const foundBoards = await this.boardsRepository.findAll(); // 데이터베이스에서 모든 게시글을 가져오는 메서드 return foundBoards; // boards가 undefined일 경우 오류 발생 } ... // 게시글 작성 기능 async createBoard(createBoardDto: CreateBoardDto): Promise<string> { const { author, title, contents } = createBoardDto; // 구조분해할당 표현식 권장됨 if (!author || !title || !contents) { throw new BadRequestException('Author, title, and contents must be provided'); } const newBoard: Board = { id: 0, // Temporary ID author, // 구조분해전 기존 표현으로는 author: createBoardDto.author title, // 구조분해전 기존 표현으로는 author: createBoardDto.title contents, // 구조분해전 기존 표현으로는 author: createBoardDto.contents status: BoardStatus.PUBLIC, }; const message = await this.boardsRepository.saveBoard(newBoard); return message; } ... }
TypeScript
복사

1.5.4 데이터베이스 연동 Repository 계층생성

데이터베이스 액세스 계층에서 쿼리 및 실행 부분만 작성
boards.repository.ts 를 새로 작성하게 된다.
Repository는 mysql2 라이브러리로부터 커넥션풀을 주입받아서 사용해야 하므로 생성자 주입을 통해 의존성을 주입받아 사용하게 된다.
커넥션 풀은 Promise(성공/실패) 객체를 반환하기 때문에 전체적으로 async/await을 사용하여 Promise를 반환하는 메서드를 구성해야 한다. 쿼리를 실행하는 query() 메서드 또한 Promise 반환이 기본이다. 이에 따라 연결된 service, controller 모두 변경된다.
try/catch문을 통해 Promise의 성공/실패의 반환을 간편하게 구분하여 작성 할 수 있다. 실행부에 문제가 없으면 try가 정상적으로 실행되며, 오류가 발생하면 catch문으로 진행되어 예외처리가 진행된다.
그 외 Express.js 실습과 전반적인 흐름의 특별한 수정사항은 없다.
import { createPool, Pool } from 'mysql2/promise'; // mysql2/promise에서 임포트 import { databaseConfig } from '../configs/database.config'; // 데이터베이스 설정 임포트 import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { Board } from './boards.entity'; @Injectable() export class BoardsRepository { private connectionPool: Pool; constructor() { this.connectionPool = createPool(databaseConfig); this.connectionPool.getConnection() .then(() => console.log('Database connected successfully')) .catch(err => console.error('Database connection failed:', err)); } async findAll(): Promise<Board[]> { const selectQuery = `SELECT * FROM board`; try { const [results] = await this.connectionPool.query(selectQuery); return results as Board[] || []; } catch (err) { throw new InternalServerErrorException('Database query failed', err); } } async saveBoard(board: Board): Promise<string> { const insertQuery = `INSERT INTO board (author, title, contents, status) VALUES (?, ?, ?, ?)`; try { const result = await this.connectionPool.query(insertQuery, [board.author, board.title, board.contents, board.status]); const message = 'Article created successfully.' return message; } catch (err) { throw new InternalServerErrorException('Database query failed', err); } } }
TypeScript
복사

1.5.5 Controller 계층의 비동기 적용

Repository 계층의 쿼리, DB 연결부의 비동기 메서드 사용으로 Controller도 연결되어 변경된다.
boards.controller.ts
import { Body, Controller, Delete, Get, Param, ParseEnumPipe, Patch, Post, Put, Query, UsePipes, ValidationPipe } from '@nestjs/common'; import { BoardsService } from './boards.service'; import { Board } from './boards.entity'; import { CreateBoardDto } from './dto/create-board.dto'; import { BoardStatus } from './boards-status.enum'; import { UpdateBoardDto } from './dto/update-board.dto'; import { BoardStatusValidationPipe } from './pipes/board-status-validation.pipe'; @Controller('api/boards') @UsePipes(ValidationPipe) export class BoardsController { // 생성자 주입 constructor(private boardsService: BoardsService){} // 게시글 조회 기능 @Get('/') async getAllBoards(): Promise<Board[]> { return await this.boardsService.getAllBoards(); // 비동기적으로 게시글 가져오기 } ... // 게시글 작성 기능 @Post('/') async createBoard(@Body() createBoardDto: CreateBoardDto): Promise<string> { return await this.boardsService.createBoard(createBoardDto); } ... }
TypeScript
복사

2. TypeORM 설치

DB를 직접 다룰 때의 문제점
ORM이란?
객체 관계 매핑(Object-relational mapping; ORM)은 데이터베이스와 객체 지향 프로그래밍 언어 간의 호환되지 않는 데이터를 변환하는 프로그래밍 기법이다. 객체 지향 언어에서 사용할 수 있는 "가상" 객체 데이터베이스를 구축하는 방법이다. …. - wikipedia
객체 관계 매핑(Object-relational mapping; ORM)은 객체 지향 프로그래밍에서의 객체와 관계형 데이터베이스의 테이블 사이의 맵핑을 관리해주는 기술.
목표는 DB와 어플리케이션간의 상호작용 단순화, 객체지향적으로 데이터를 다룰 수 있게 함
NestJS에서 TypeORM을 사용하기 위해서는 typeorm , @nestjs/typeorm미들웨어를 설치해야 한다.
typeorm은 Node.js 환경에서 typeorm을 사용 할 수 있도록 해주는 실제 ORM 기능을 제공
@nestjs/typeorm은 NestJS에서 위 typeorm을 쉽게 다룰 수 있도록 도와주는 기능을 제공
아래 명령어를 통해 두개를 모두 설치하면 NestJS 프로젝트에서 사용 할 수 있다.
npm i @nestjs/typeorm typeorm
Shell
복사

3. NestJS에서 TypeORM 설정

위 두개의 패키지 설치 후 프로젝트에서 사용하기 위한 설정 파일을 작성한다.
root(boards) 폴더에 configs 폴더 생성
typeorm.config.ts 파일 생성
import { TypeOrmModuleOptions } from "@nestjs/typeorm"; import * as dotenv from 'dotenv'; dotenv.config(); export const typeOrmConfig: TypeOrmModuleOptions = { type: 'mysql', // 사용할 데이터베이스 유형 (MySQL, PostgreSQL, SQLite 등) host: process.env.DB_HOST, // 데이터베이스 호스트 port: +process.env.DB_PORT, // 데이터베이스 포트 username: process.env.DB_USER, // 데이터베이스 사용자 이름 password: process.env.DB_PW, // 데이터베이스 비밀번호 database: process.env.DB_NAME, // 사용할 데이터베이스 이름 entities: [__dirname + '/../**/*.entity.{js,ts}'], // 엔티티 파일의 위치 synchronize: true, // 애플리케이션 실행 시 스키마를 동기화할지 여부 (개발 중에만 true로 설정) logging: true, // SQL 쿼리 로그를 출력할지 여부 };
TypeScript
복사
typeorm이 적용 될 엔터티가 있는 모듈 boards.module.ts에서 엔터티를 등록
import { Module } from '@nestjs/common'; import { BoardsController } from './boards.controller'; import { BoardsService } from './boards.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Board } from './boards.entity'; @Module({ imports: [ TypeOrmModule.forFeature([Board]), // Board 엔터티를 TypeORM 모듈에 등록 ], controllers: [BoardsController], providers: [BoardsService] }) export class BoardsModule {}
TypeScript
복사
typeormapp.module.ts루트모듈에 등록하여 프로젝트 전역에서 사용 할 수 있도록 위 클래스를 등록해준다.
import { Module } from '@nestjs/common'; import { BoardsModule } from './boards/boards.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { typeOrmConfig } from './configs/typeorm.config'; @Module({ imports: [ TypeOrmModule.forRoot(typeOrmConfig), BoardsModule], }) export class AppModule {}
TypeScript
복사

4. 엔터티를 ORM이 인식 할 수 있도록 수정

entitytypeorm이 DB 테이블과 맵핑하기 위해서는 여러 데코레이터(@)를 추가해주어야 한다.
@Entity() : 해당 클래스가 typeorm이 DB의 테이블과 맵핑할 대상 클래스임을 지정
@PrimaryGeneratedColumn() : 해당 필드는 DB의 테이블에서 기본키(PK) 컬럼임을 지정
@Column() : 해당 필드는 DB의 테이블에서 일반 컬럼임을 지정(각 속성)
위 내용에 따라 클래스와 각 필드에 아래와 같이 데코레이터를 추가한다.
board.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; import { BoardStatus } from "./boards-status.enum"; @Entity() export class Board { @PrimaryGeneratedColumn() id: number; @Column() author: string; @Column() title: string; @Column() contents: string; @Column() status: BoardStatus; }
TypeScript
복사
boards.service.ts
import { Injectable, NotFoundException } from '@nestjs/common'; import { Board } from './board.entity'; import { BoardStatus } from "./board-status.enum"; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { UpdateBoardDto } from './dto/update-board.dto'; import { CreateBoardDto } from './dto/create-board.dto'; @Injectable() export class BoardsService { constructor( @InjectRepository(Board) private boardsRepository: Repository<Board> ){} // 게시글 작성 async createBoard(createBoardDto: CreateBoardDto): Promise<Board> { ...
TypeScript
복사
이제 Repository의 계층이 불필요하다. 실행 시 다음처럼 테이블을 자동 설정된다면 완료
이제 전체 컨트롤러 및 서비스의 소스 코드를 TypeORM이 활용 할 수 있도록 리팩토링하자.

5. Custom Repository 사용 방법

3 Layer Architecture 구조를 따르기 위해 Custom Repository 계층을 생성하던 것은 최신 버전 typeorm에서 삭제되고 있다.
이전 직접 계층을 생성하던 @EntityRepository(resource) 애너테이션이 현재 버전에서는 Deprecated 상태로 더이상 사용하지 않는 방식으로 변경되고 있다.
따라서 boards.repository.ts 과 같은 기본 계층을 cli generater에서도 더이상 생성해주지 않는 것
그럼 Custom Repository가 필요할 경우는?
boards.repository.ts 를 아래처럼 작성해도 된다.
// import { EntityRepository, Repository } from 'typeorm'; // import { Board } from './board.entity'; // @EntityRepository(Board) // export class BoardsRepository extends Repository<Board> { // 커스텀 메서드 예제 // async findByAuthor(author: string): Promise<Board[]> { // return this.find({ where: { author } }); // } // async findOneById(id: number): Promise<Board | undefined> { // return this.findOneBy({ id }); // } // }
TypeScript
복사
boards.module.ts 에서도 BoardRepository를 받아오도록 변경해야 한다.
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BoardsService } from './boards.service'; import { Board } from './board.entity'; import { BoardsRepository } from './boards.repository'; // 리포지토리 파일의 경로 @Module({ imports: [TypeOrmModule.forFeature([Board])], providers: [ BoardsService, BoardsRepository ], exports: [BoardsService], }) export class BoardsModule {}
TypeScript
복사
하지만 이번 연습에서는 해당 파일은 주석처리하고 간편하게 service계층에서의 TypeORM Repository를 주입받아 사용하고자 한다.

6. TypeORM 설정 체크

기존 코드는 메모리의 배열로 작성한 임시 DB로 구현해본 코드이다.
이제 실제 DB연결과 typeorm을 연결하는 CRUD를 구현해봐야 하기 때문에 boards.controller.ts, boards.service.ts 클래스의 내부 소스 코드들을 임시로 주석처리해둔다.
IDE 오류 없이 다음과 같이 준비해둔다.
현재까지 진행한 Entity 데코레이터 설정 및 DB 연결 상태를 확인하려면 서버를 실행해보면 된다.
우리는 ORM기술을 통해 Board 엔터티에 데코레이터로 데이터베이스와 엔터티 클래스를 맵핑했다.
따라서 서버가 실행되면 해당 객체 정보를 데이터베이스 테이블과 맵핑하기 때문에 초기 실행에선 테이블을 자동 생성해준다.
위 과정처럼 컨트롤러와 서비스의 오류를 임시 비활성화해두고 서버를 한번 실행시켜 본다.
npm start run:dev
Shell
복사
실행 시 아래와 같은 DDL(Data Definition Language, 데이터 정의어)이 실행되어야 정상적으로 MySQL DB연동, typeorm 설정이 성공한 것으로 볼 수 있다.
실제로 MySQL 콘솔 또는 GUI툴로 해당 테이블이 생성되었는지 확인해보자
use board_app;
SQL
복사
desc boards;
SQL
복사
이 과정까지 오류가 발생한다면 설정 부분에서의 오류를 찾아 확인하여 수정해야 한다.
Search
 | Main Page | Category |  Tags | About Me | Contact | Portfolio