NestJS에서 TypeORM을 사용하는 법을 알아보자!

윤학·2023년 3월 30일
1

Nestjs

목록 보기
6/13
post-thumbnail

사용이유

Nest에서는 데이터베이스 종류에 상관없이 쉽게 연결을 해서 사용할 수 있는데 가장 일반적인 방법은 각 데이터베이스에 맞는 Node 드라이버를 사용하면 된다.

하지만 나는 Nest에서 MySQL을 연결해 사용할 방법을 찾다가 객체와 데이터베이스 테이블을 매핑시켜주는 ORM을 사용해보기로 했었다.

대표적인 TS와 JS ORM 라이브러리로는 TypeORMSequelize가 있지만 다음과 같은 이유로 TypeORM을 선택했다.

  1. 데코레이터를 사용해서 클래스와 필드를 정의할 수 있다.
  2. TypeORM에서 사용가능한 Query Builder가 동적이고 복잡한 쿼리 작성에 적합하다.

Pattern

TypeORM에서는 Active Record PatternData Mapper Pattern으로 데이터베이스에 액세스 할 수 있는데 과정이 모델 내에서 이루어지냐 Repository 별도의 클래스에서 이루어지냐에 따라 두 패턴을 나눌 수 있다.

1. Active Record

모델 내에서 작성한 쿼리 메소드들을 통해 데이터베이스 작업을 하는 패턴을 말한다.

해당 패턴을 사용하기 위해선 기본 Repository에서 제공하는 많은 메소드들을 가지고 있는 BaseEntity를 반드시 상속받아야 하고 해당 Entity 클래스 내에서 정적 메소드로 기능들을 추가할 수 있다.

2. Data Mapper

쿼리 메소드들을 Repository라는 별도의 클래스로 분리해서 Repository에서 데이터베이스에 대한 접근이 이루어지는 패턴이다.

두 예시는 여기에서 확인할 수 있다.

Nest 문서에는 TypeORM이 Repository 패턴을 지원한다고 나오는데요?

사실 여기서 헷갈리고 이해가 잘 되지 않았었다...

간단하게만 살펴보면 Repository패턴이란, 비즈니스 로직을 수행하는 계층과 실제 데이터베이스 계층 사이에 Repository라는 하나의 계층을 더 두어 데이터베이스 접근 로직은 Repository 계층에서 수행한다는 개념이다.

그럼 비즈니스 로직을 수행하는 계층에선 데이터베이스로 뭘 쓰고 몇 개를 쓰든 변경사항에 관계 없이 Repository를 호출하면 되고 변경사항은 Repository만 수정하면 되기 때문에 역할이 분리가 된다는 이점이 있다.

반면 Data Mapper는 도메인 객체나 Dto를 테이블에 맞는 엔티티로 혹은 그 반대로 변환해 주는 역할을 하는데 Data Mapper개체에 관한 작업이라면 Repository는 개체의 집합(데이터베이스)에 관한 작업이라 생각하면 될 것 같다.

그니까 결국 Data Mapper는 Repository 내에서 동작한다고 생각하면 될 것 같다...

그럼 이제 사용하는 방법을 본격적으로 알아보자!

여기서 typeorm의 버전은 0.3대다.

1. 설치

간단히 아래의 명령어로 설치해준다.

npm install --save typeorm mysql2

2. Provider 생성

database.provider.ts

import { ConfigModule, ConfigService } from '@nestjs/config';
import { UserView } from 'src/api/v1/user/entity/user-view.entity';
import { User } from 'src/api/v1/user/entity/user.entity';
import { DataSource } from 'typeorm';

export const databaseProviders = [
    {
        provide: 'DATA_SOURCE', // 주입시킬 Token 값
        import: [ConfigModule], // config 파일을 불러오기 위한
        inject: [ConfigService], // ConfigModule import
        useFactory: (configService: ConfigService) => {
            const datasource = new DataSource({
                type: 'mysql',
                host: configService.get('database.host'),
                port: configService.get('database.port'),
                username: configService.get('database.username'),
                password: configService.get('database.password'),
                database: configService.get('database.database'),
                entities: [User, UserView], // 사용할 Entity
                synchronize: true, // schema 반영할지 여부
                logging: true // log 매세지 출력
            });
            return datasource.initialize();
        }
    }
];

DataSource 객체를 주어진 데이터베이스 옵션과 함께 생성하고 initialize 메소드를 통해서 연결한다.

DataSoucre의 옵션을 간단하게 살펴보면 다음과 같다.

  • entities: 로드시 연결 할 엔티티들을 명시하는 곳으로 entity classdirectory가 올 수 있다.
  • synchronize: 애플리케이션을 실행 할 때마다 스키마를 자동생성 해주는 옵션을 체크하는 필드로 개발 환경에서만 true
  • logging: 데이터베이스 작업이 이루어질 때 SQL문으로 변환된 log 내용을 출력해준다.

이후 module화 하여 다른 모듈에서도 사용가능하게 내보내준다.

database.module.ts

import { Module } from "@nestjs/common";
import { databaseProviders } from "./database.providers";

@Module({
    providers: [...databaseProviders],
    exports: [...databaseProviders]
})

export class DatabaseModule {}

그럼 MySQL 작업이 필요한 모듈에서는 해당 모듈을 import 시켜서 사용할 수 있다.

3. Entity

Entity란 데이터베이스의 테이블과 매핑되는 클래스로, 클래스를 @Entity 데코레이터와 함께 작성하면 해당 클래스를 Entity로 인식한다.

Entity에 다양한 옵션을 줄 수 있는데 간단하게만 살펴보자.

  • database: 선택한 DB서버의 데이터베이스 이름
  • schema: 스키마 이름
  • engine: InnoDB, MyISAM과 같은 엔진 종류를 테이블이 생성되는 동안 설정해 줄 수 있지만, 이미 테이블이 생성되었거나 이후에 이 값을 변경해도 이미 설정한 engine은 바뀌지 않는다.
  • synchronize: 해당 entity의 스키마를 업데이트 할지 여부
  • orderBy: find 메소드나 QueryBuilder를 사용할 때 특정 필드의 정렬 기준을 지정
@Entity({
    name: 'user',
  	engine: 'InnoDB',
  	database: 'example_entity',
  	schema: 'example_schema',
    synchronize: false,
    orderBy: { id: "DESC", name: "ASC" }
})
export class User { }

4. Column

위의 예시는 Entity의 다양한 옵션들을 보여주기 위함이였고 원래는 Entity로 선언한 클래스 내부에는 @Column 데코레이터로 데이터베이스의 테이블 column과 매핑될 column을 만들어 줘야 한다.

그럼 예시와 함께 살펴보자.

@Entity('user')
export class User {
    @PrimaryColumn({ type: 'varchar', length: 11})
    id: string

    @Column({ type: 'simple-json', nullable: true })
    warning: { first: string, second: string, third: string}

    @Column({ default: false })
    profile_off: boolean

    @CreateDateColumn()
    createDate: Date

    @Column({type: 'int'})
    token_index: number

    @OneToOne(() => Token, (token) => token.index, {
        cascade: ['insert', 'update']
    })
    @JoinColumn({
        name: 'token_index',
        referencedColumnName: 'index'
    })
    token: Token
}

1) PrimaryColumn

MySQL의 Primary Key로 설정하는 column이 맞다.

위의 예시는 휴대폰 번호를 Primary Column으로 설정한 예시인데 좋지 않은 예시이며 자동으로 Primary Column을 생성하려면 다음과 같이 작성한다.

@PrimaryGeneratedColumn(('increment'|'uuid))

MySQL 기준 increment는 AUTO_INCREMENT, uuid는 UUID 값을 자동으로 생성해준다.

몽고DB와 같은 경우는 document마다 ObjectID column을 무조건 가지고 있어야 하기 때문에 @ObjectIdColumn() 데코레이터로 생성할 수 있다.

2) 다양한 Column Option

기본적으로 @Column() 데코레이터에 사용할 수 있는 옵션은 너무 많기 때문에 여기에서 필요한 옵션을 쓰자.

위의 예시로 관계를 제외하고 간단히만 살펴보자.

  • type: varchar, int, simple-json등 column type이 올 수 있다. simple-json 같은 경우 JSON.stringify로 변환된 값들이 저장될 수 있어 유용하고 bigint 같은 경우는 string으로 보는걸 조심하자!
  • length: 값의 최대 길이
  • nullable: null 값이 될 수 있는지 여부
  • default: insert시 값을 입력하지 않아도 저장될 default 값

5. ViewEntity

View Entity는 MySQL의 View와 같은데 여러 테이블을 조인하거나 여러 개의 Column 중 조회 결과에 몇개의 Column만 사용된다면 해당 Column들을 가지고 View Table을 구성할 수 있을 것이다.

user-view.entity.ts

import { ViewColumn, ViewEntity, DataSource } from 'typeorm';
import { User } from './user.entity';

@ViewEntity({
    expression: (dataSource: DataSource) =>
        dataSource
        .createQueryBuilder()
        .select("user.id", "id")
        .addSelect("user.nickname", "nickName")
        .from(User, 'user'),
})
export class UserView {
    @ViewColumn()
    id: string;

    @ViewColumn({
        name: 'nickname'
    })
    nickName: string;
}

@ViewEntity도 일반 @Entity와 대부분 같은 옵션을 가지지만 @ViewEntity는 expression이라는 필드를 필수 옵션으로 가진다.

해당 필드에는 해당 뷰에 저장할 SQL문을 작성하면 되는데 RAW SQL문으로 작성할 수 있고, Query Builder로도 작성할 수 있으니 맞게 사용하면 된다.

마치면서..

TypeORM을 프로젝트에서 사용해보았지만 아직 query cache나 replication등 유용한 기능들을 많이 사용해보지 못한 것 같다.

하지만 크게 와닿은 것은 RAW SQL문을 잊어버리게 된다..

나의 SQL문 공부량이 부족한 것일 수도 있지만 제공하는 메소드들을 사용하다 보면 어느순간 떠올릴려고 해도 헷갈린다.

TypeORM에서 모르는 걸 찾더라도 SQL문을 알면 찾기 수월하지만 SQL문을 모르고 사용하기에는 살짝 어려울 수도 있을 것 같다.

그나마 QueryBuilder가 좀 비슷한 느낌이니 나한텐 더 맞는 것 같아 사용하기로..

사용했던 내용들을 추후에 계속해서 정리해 보겠다!

참고

NestJS - SQL (TypeORM)
TypeORM 공식문서
TypeORM GitBook
What exactly is the difference between a data mapper and a repository?
Data Mapper
Repository

0개의 댓글