본문 바로가기
블로그 이미지

방문해 주셔서 감사합니다! 항상 행복하세요!

  
   - 문의사항은 메일 또는 댓글로 언제든 연락주세요.
   - "해줘","답 내놔" 같은 질문은 답변드리지 않습니다.
   - 메일주소 : lts06069@naver.com


Node.js/Nestjs (Nest.js)

Nestjs 프레임워크 서버(sequelize, database) -13

야근없는 행복한 삶을 위해 ~
by 마샤와 곰 2022. 9. 11.

 

시퀄라이즈(sequelize) 는 관계형 데이터베이스를 편리하게 사용 할 수 있게 해주는 프레임워크 입니다.

데이터베이스의 종류와 상관 없이 함수를 호출하는 방식을 통해서 쿼리를 편하게 사용 할 수 있습니다.

이번 포스팅에서는 관계형 데이터베이스인 포스트그레(postgre)를 사용 해 보았습니다.

 

먼저 모듈(라이브러리)을 설치하여 줍니다

npm install sequelize sequelize-typescript @nestjs/sequelize
npm install @types/sequelize
npm install pg

 

맨 위 2개의 모듈은 시퀄라이즈 프레임워크를 사용하기 위한 모듈이며, 마지막 pg 모듈은 포스트그레에 접속을 하기 위한 모듈 입니다.

만약 mysql이나 mssql등 다른 데이터베이스를 사용한 다면 그에 맞게 설치를 해 주어야 합니다.

 

#1. 모델 등록

모델이란, 데이터베이스의 ORM을 위한 기초단계 입니다.

내가 사용 하고자 하는 데이터베이스 테이블의 모양을 스크립트 파일로 만든뒤에, 해당 스크립트 파일에 지정한 형태의 값 들로 결과를 받기 위한 가장 기초적인 작업 입니다.

먼저 사용 하고자하는 테이블의 모습 입니다.

* 테이블 이름 : nestjs_table

어려운 내용은 없는 nestjs_table 이름을 가진 테이블 입니다.

 

위 테이블 이름을 참고하여 모델파일을 만들어 줍니다.

* 파일이름 : 모델.ts

import { Table, Column, Model,PrimaryKey } from 'sequelize-typescript';

@Table({
    modelName : 'nestjs_table', 
    freezeTableName : true,  //테이블에 s 붙이지 않는 옵션
    timestamps : false //createdAt, updatedAt 필드 생성유무
})
export class 모델 extends Model<모델> {
  @Column
  nest_text: string;

  @Column
  nest_number: number;

  @Column({defaultValue : new Date()})
  nest_date: Date;

  @PrimaryKey
  @Column
  nest_idx: string;  
}

 

클래스는 Model 이라는 추상클래스를 상속 받아서 구현되어 있습니다.

Model 이라는 추상클래스는 시퀄라이즈 함수의 기본 기능을 전부 가지고 있는 클래스 입니다.

 

먼저 @Table 데코레이터 입니다.

@Table 데코레이터는 해당 클래스에게 테이블임을 알려주는 기능을 담당 합니다.

클래스 이름을 애초에 테이블과 맞추어도 상관 없습니다.

위 코드 주석으로 된 3가지 기본 옵션 설명에 유의하여야 합니다.

 

다음으로 @Column 데코레이터를 살펴 보겠습니다.

@Column 데코레이터는 해당 클래스 변수가 테이블의 컬럼에 매칭해야되는 값을 의미 합니다.

옵션을 통해서 이름을 다르게 줄 수 있지만 가급적 동일한 이름을 써야 유지보수하는 데 편리 합니다.

 

@PrimaryKey 데코레이터 입니다.

@PrimaryKey 데코레이터는 해당 모델에서 유니크한 키 값을 지정할 때 사용합니다.

반드시 지정이 되어야 하며, 지정하지 않는 경우 자동으로 id 라는 값을 부여하여 쿼리를 조회하게 됩니다.

이러한 방법을 통해서 사용하고자 하는 테이블의 모델을 만들어 줍니다.

 

#2. 데이터베이스 연결 및 의존성 주입

데이터 베이스 연결을 위해서는 역시나 모듈 파일에서의 작업이 필요 합니다.

모듈에서는 2가지 작업을 합니다.

첫번째로는 데이터베이스와 연결하는 부분, 두번째는 사용하고자 하는 모델을 선언하는 부분 입니다.

* 파일이름 : app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { SequelizeModule } from '@nestjs/sequelize';
import {모델} from './모델';

@Module({
  imports: [    
    SequelizeModule.forRoot({  //1. 시퀄라이즈 모듈에 데이터베이스 설정
      dialect: 'postgres',  //가능한 데이터베이스 : mysql, mariadb, sqlite, postgres, mssql
      host: 'localhost',
      port: 12345,
      username: '아이디',
      password: '비밀번호',
      database: '사용하고자하는데이터베이스',
      models: [모델]
    }),
    SequelizeModule.forFeature([모델])//2. 시퀄라이즈 모듈에 사용할 모델을 지정
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 

여기까지 하였다면 초기 설정은 끝났다고 볼 수 있습니다.

connection 이라는 오류나 auth 와 관련된 오류는 전부 데이터베이스에서의 설정이 되지 않는 것 이므로 이러한 오류가 나면 데이터베이스 설정을 확인 하여야 합니다.

 

모듈에서의 2개의 설정을 통해서 이제 시퀄라이즈와 관련된 기능을 주입받아서 사용할 수 있게 되었습니다.

의존성을 주입받아 사용 가능한 대표적 기능은 2개 입니다.

1. 내가 만든 모델
2. 트랜젝션이나 데이터베이스의 추가적인 행동(DDL,DCL 등)을 위한 Sequelize 객체

말로는 어려우므로 역시 간단한 기능을 만들어보아야 겠습니다!

 

#3. 기본 CRUD 동작

데이터베이스에 연결하는 동작은 컨트롤러 보다는 서비스(service)에서 주로 사용하는 비지니스 성격의 행동 이므로 역시 서비스에서 의존성을 받아서 기능을 작성하여 봅니다.

* 파일이름 : app.service.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Op, fn, col } from 'sequelize';
import { 모델 } from './모델';
import { Sequelize } from 'sequelize-typescript';

@Injectable()
export class AppService {
  constructor(@InjectModel(모델) private model: typeof 모델, private se: Sequelize ) {

  }
  
  select(): Promise<모델[]> {
    return this.model.findAll();
  }  
  
}

 

model 으로 의존성을 주입받은 객체는 맨 처음 만들어준 "모델.ts" 파일에서의 모델입니다.

해당 모델은 시퀄라이즈의 Model 추상 클래스를 상속받았으므로 Model클래스가 제공하는 함수를 사용 할 수 있습니다.

조회(select)하는 함수는 데이터베이스 종류와 상관없이 find 로 시작합니다.

데이터베이스의 접속을 통해서 행위(I/O)가 이루어지므로 역시나 Promise를 반환하는 것을 볼 수 있습니다.

몇개 샘플코드를 더 만들어 보았습니다.

* 파일이름 : app.service.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Op, fn, col } from 'sequelize';
import { 모델 } from './모델';
import { Sequelize } from 'sequelize-typescript';

@Injectable()
export class AppService {
  constructor(@InjectModel(모델) private model: typeof 모델, private se: Sequelize ) {

  }
  
  select(): Promise<모델[]> {
    return this.model.findAll();
  }  

  insert(): Promise<모델> {
    let value = {
      nest_text: '값1',
      nest_number: 12345,
      //nest_date : new Date(),  //옵션 defaultValue 동작여부 확인을 위한 주석!
      nest_idx: '값2'
    };
    return this.model.create(value);
  }

  update() {
    let data = { nest_number: 123456 };
    let where = { nest_idx: 'aaaa' };
    return this.model.update(data, { where });
  }

  delete() {
    let where = { nest_idx: 'aaaa' };
    this.model.destroy({ where });
  }

  selectWithOption(): Promise<모델[]> {
    let option = {
      where: {
        nest_text: { [Op.like]: '%a%' },
        nest_number: { [Op.gte]: 3, [Op.lte]: 10000 },
      },
      offset: 0,
      limit: 10,
      raw: true, //조회한 결과 객체로만 표기 옵션
    };
    return this.model.findAll(option);
  }  
}

 

등록, 수정,삭제 그리고 조건이 있는 조회기능까지의 모습 입니다.

조회를 하기 위해서는 "sequelize" 패키지의 함수를 사용하면 됩니다.

시퀄라이즈는 반환된 결과값을 객체 그 자체로 제공하고 있습니다.

그러므로 단순하게 값만 사용하고자 하는 경우에는 raw 옵션을 true 하여 붙여주어야 합니다.

raw 옵션이 없으면 요리 나오고....
raw 옵션을 true로 하면 데이터만 나옵니다.

 

개발자가 만든 모델이라는 형식의 파일이 CRUD 동작도 하면서 동시에 ORM 도구로 사용되는 모습을 볼 수 있습니다.

데이터베이스 종류에 상관없이 필요한 기능의 함수만 호출하는 것도 너무 편리하구요!

다음으로 데이터베이스 테이블의 join 입니다!

 

#4. 기본 테이블 join

관계형 데이터베이스에서 테이블이 1개만 있는 프로젝트는 본 적이 없습니다.

그러므로 테이블과 테이블이 join하기 위해서는 역시나 모델 파일을 만들어서 설정을 해 주어야 합니다.

join을 위해 테이블을 추가 하였습니다.

* 테이블 이름 : nestjs_table_friend

오호?

 

다음으로 #1. 모델등록에서 한 것처럼 위 테이블에 매칭되는 클래스를 만들어 줍니다.

* 파일이름 : 모델2.ts

import { Table, Column, Model,PrimaryKey } from 'sequelize-typescript';

@Table({
    modelName : 'nestjs_table_friend', 
    freezeTableName : true,  //테이블에 s 붙이지 않는 옵션
    timestamps : false //createdAt, updatedAt 필드 생성유무
})
export class 모델2 extends Model<모델2> {
  @Column
  your_idx: string;

  @Column
  friend_data: string;

  @PrimaryKey
  @Column
  friend_idx: string;  
}

 

컬럼 friend_idx값은 nestjs_table_friend 테이블에서 유니크한 키 값으로 사용될 예정이며, your_idx는 처음 만들어준 테이블 nestjs_table의 nest_idx에 참조할 예정 입니다.

요렇게!

 

새롭게 만들어준 모델2 클래스를 사용하기 위해서는 모듈파일에 등록을 추가 해 주어야 합니다.

* 파일이름 : app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { SequelizeModule } from '@nestjs/sequelize';
import {모델} from './모델';
import {모델2} from './모델2';

@Module({
  imports: [    
    SequelizeModule.forRoot({  
    
      //생략..
      models: [모델, 모델2],  //추가!!
      
    }),
    SequelizeModule.forFeature([모델, 모델2])//추가!!
  ]
})
export class AppModule {}

 

사용할 서비스에서도 마찬가지로 의존성을 주입받아서 사용하여 줍니다.

join을 위해서는 첫번째로 관계를 설정하고, 두번째로 자식 모델을 조회하는 함수에 붙여주는 형태로 사용해야 합니다.

* 파일이름 : app.service.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Op, fn, col } from 'sequelize';
import { 모델 } from './모델';
import { 모델2 } from './모델2';

import { Sequelize } from 'sequelize-typescript';

@Injectable()
export class AppService {
  constructor(
    @InjectModel(모델) private model: typeof 모델,
    @InjectModel(모델2) private model2: typeof 모델2,
    private se: Sequelize,
  ) {

  }

  //생략..
  
  selectLeftOuterJoin() {
    this.model.hasMany(this.model2, {
      foreignKey: 'your_idx',
    });
    return this.model.findAll({
      include: [
        {
          model: this.model2,
          attributes: ['your_idx', 'friend_data', 'friend_idx'],
        },
      ],
      raw: true,
    });
  }

}

 

hasMay 함수를 통해서 모델 클래스는 모델2 클래스를 많이 갖을 수 있다(?) 관계로 표현합니다.

이때 모델 클래스의 primary키가 참고할 값은 모델2 클래스의 your_idx 값을 참고하도록 참조키 설정(foreignKey)을 해 줍니다.

만약 모델 클래스의 primary 키로 join을 하지 않는 경우 라면 sourceKey 옵션을 주어 필드 이름을 지정 해 주도록 합니다.

위 방식은 outer 결과를 수행하며, include 내부에 "required: true" 옵션을 추가하면 inner 효과를 가져오게 됩니다.

inner 효과를 주어 가져온 결과 모습

 

이러한 방법을 통해서 join을 할 수 있습니다.

물론...복잡하고 머리아플 정도의 쿼리는 어렵겠지요..ㅠ

 

#5. group by

group by 기능도 어렵지 않습니다.

역시나 findAll 함수에 옵션처럼 그룹을 지어줄 내용을 선언하면 됩니다.

* 파일이름 : app.service.ts

  groupping() {
    return this.model.findAll({
      attributes: [
        'nest_text',
        [fn('count', col('nest_text')), 'countResult'],  //카운트
        [fn('sum', col('nest_number')), 'sumResult'],    //합계
      ],
      group: ['nest_text'],  //그룹할 필드
      raw: true
    });
  }

 

정말 find 형식의 함수 1개로 모두 동작하게 만드는 기술에 감탄을 안할수가 없습니다!

물론 조건을 주려면 #3. 기본 CRUD 동작에 있는 샘플코드를 참조하여 where 값을 주어서 동작하게 하면 됩니다.

잘되네요!

 

#6. 트랜젝션(transaction)

트랜젝션을 사용하기 위해서는 서비스 클래스에서 의존성 주입을 받았던 se 객체를 사용해야 합니다.

* se 객체야...오래 기달렸네..

@Injectable()
export class AppService {
  
  constructor(
    @InjectModel(모델) private model: typeof 모델,
    @InjectModel(모델2) private model2: typeof 모델2,
    private se: Sequelize // 요놈 입니다!!!!
  ) {

  }
  
}

 

트랜젝션은 역시나 동기화(sync)가 필수 입니다.

데이터를 처리하는 모든 구간에 동기화를 해야 하므로 Promise 형식으로 작성하면 코드가 상당히 길어지게 됩니다.

* 파일이름 : app.service.ts

//생략..

async gogoTransaction(){
    //1. 트랜젝션을 설정 합니다.
    let tran = await this.se.transaction({autocommit : false});

}

 

첫번째로 의존성 주입을 받았 던 se객체로부터 트랜젝션 함수를 동작 시킵니다.

여기서는 간단하게 autocommit 옵션을 false 로만 하였습니다.

transaction 함수에는 4가지 옵션을 줄 수 있습니다.

#1. transaction 함수에서 줄 수 있는 옵션
{
    autocommit?: boolean;
    isolationLevel?: Transaction.ISOLATION_LEVELS;
    type?: Transaction.TYPES;
    deferrable?: string | Deferrable;
}

#2. 격리수준 종류
enum ISOLATION_LEVELS {
    READ_UNCOMMITTED = 'READ UNCOMMITTED',
    READ_COMMITTED = 'READ COMMITTED',
    REPEATABLE_READ = 'REPEATABLE READ',
    SERIALIZABLE = 'SERIALIZABLE'
}

 

이제 테스트 코드를 추가하여 봅니다.

데이터는 2번 등록을 합니다.

첫번째는 올바른 값을 넣어주고, 두번째는 일부러 틀려줍니다.

그러면 트랜젝션이 제대로 동작 하였다면 데이터 2개는 들어가서는 안됩니다.

* 파일이름 : app.service.ts

//생략..

async gogoTransaction(){
    //1. 트랜젝션을 설정 합니다.
    let tran = await this.se.transaction({autocommit : false});

    try {

    let value = {
        nest_text: 'aaaa',
        nest_number: 1111,
        nest_idx: 'abcdefghjk',
    };
    await this.model.create(value,{transaction : tran});

    value = {
    nest_text: 'bbbb',
    nest_number: 2222,
    nest_idx: 'abcdefghjkabcdefghjkabcdefghjkabcdefghjk',  //일부러 20자릿수 초과를 넣어봅니다.
    };
    await this.model.create(value,{transaction : tran});

    await tran.commit();  //다 됬으면 커밋

    } catch (error) {
        console.log(error)
        tran.rollback();
    }

}

 

컬럼 nest_idx 값은 처음 nestjs_table 테이블을 만들 때 15자리 이하만 넣을수 있게 하였습니다.

여기서는 일부러 15자리 이상값을 주어보았습니다.

정상적인 트랜젝션 동작이 완료 되었다면 아래와 같은 결과가 나와야 합니다.

ROLLBACK!!!!!

 

만약 트랜젝션을 시작한 뒤에 async, await 또는 promise 처리를 하지 않는다면 아래와 같은 오류를 만날수 있습니다.

The rejected query is attached as the 'sql' property of this error

그러므로 then 또는 await 제어를 통해서 데이터 변경행위는 전부 끝내고 commit 하도록 합니다.

이런식으로 promise 값 형태를 그냥 내비두고 커밋하면....안됩니다!!!

 

어려우면서도 재미있는 관계형 데이터베이스 탐험이였습니다. +ㅁ+

시퀄라이즈 프레임워크는 nestjs를 위해 만들어진게 아니므로 좀더 세부적인 동작을 위해서는 연습이 필요할 거 같습니다.

 

위 내용에 사용된 코드는 아래 제 깃허브에서 받아볼 수 있습니다.

https://github.com/TaeSeungRyu/NestProject/tree/main/step12

 

GitHub - TaeSeungRyu/NestProject: Nestjs 프로젝트

Nestjs 프로젝트. Contribute to TaeSeungRyu/NestProject development by creating an account on GitHub.

github.com

 

공부하면 할 수록 재미있고 즐거운 nestjs!!!
다음 포스팅에서는 비관계형 데이터베이스 연결에 대해서 살펴보겠습니다.
궁금한점 또는 틀린부분은 언제든 연락 주세요!

 

반응형
* 위 에니메이션은 Html의 캔버스(canvas)기반으로 동작하는 기능 입니다. Html 캔버스 튜토리얼 도 한번 살펴보세요~ :)
* 직접 만든 Html 캔버스 애니메이션 도 한번 살펴보세요~ :)

댓글