Blog

[NestJS] 5. DB와 TypeORM 연동

Category
Author
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
복사
•
typeorm을 app.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이 인식 할 수 있도록 수정

📌
entity를 typeorm이 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