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 미들웨어(라이브러리 설치)
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와 어플리케이션간의 상호작용 단순화, 객체지향적으로 데이터를 다룰 수 있게 함
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
복사
•
이 과정까지 오류가 발생한다면 설정 부분에서의 오류를 찾아 확인하여 수정해야 한다.
Related Posts
Search