본문 바로가기

나만의 모음집

[모음집] NestJS(with TypeORM + PostgreSQL) 예제 모음집

반응형

 


포스트 목적

NestJS, TypeORM, PostgreSQL 을 사용하면서 적용해야 했던 내용들에 대해 하나하나 정리하여 찾아볼 수 있는 아카이브 형식으로 자료들을 모아두는 포스트 입니다. 정리 순서는 뒤죽박죽이라서 목차를 보고 필요한 내용이 있다면 참고하시면 좋을 것 같습니다. 해당 포스트는 관리 효율성을 위해 업데이트 되는 날을 기준으로 최상단에 포스트가 갱신되는 것을 원칙으로 합니다.

 


참고/참조 자료 링크

(TypeORM Where 문 예시) https://typeorm.io/select-query-builder#adding-where-expression


PostgreSQL 에서 제공하는 JSONB 타입의 데이터 다루기

아래는 JSONB 타입의 데이터를 조작하기 위해 사용하는 옵션에 대한 간략한 설명을 모아둔 것이다.

 

jsonb->'key': JSON 객체에서 지정된 키의 값을 JOSNB 형식으로 추출

현재 반환된 John 은 텍스트 문자열이 아니라 JSON 객체이다.

SELECT '{"name": "John", "age": 30}'->'name'; -- "John"

jsonb->>'key': JSON 객체에서 지정된 키 값을 텍스트 문자열로 추출

현재 반환된 John 은 JSON 객체가 아니라 텍스트 문자열이다.

SELECT '{"name": "John", "age": 30}'->>'name'; -- "John"

jsonb @> jsonb: 좌측 JSON 객체에 우측 JSON 객체가 하나라도 존재하면 true

{"a":1} 이  {"a": 1, "b": 2, "c": 3} 에 하나라도 존재하면 true 를 반환한다.

SELECT '{"a": 1, "b": 2, "c": 3}'::jsonb @> '{"a": 1}'::jsonb; -- true

jsonb <@ jsonb:  좌측 JSON 객체가 우측 JSON 객체에 하나라도 존재하면 true

{"a": 1, "b": 2, "c": 3}  에  '{"a": 1}' 이 하나라도 존재하면 true를 반환한다.

SELECT '{"a": 1}'::jsonb <@ '{"a": 1, "b": 2, "c": 3}'::jsonb; -- true

jsonb ? text: 해당 텍스트가 좌측 JSON 객체의 최상위 키 또는 요소로 존재하면 true

SELECT '{"a": 1, "b": 2}'::jsonb ? 'a'; -- true

jsonb || jsonb: 두 개의 JSON 객체를 하나로 합쳐서 반환

SELECT '{"a": 1}'::jsonb || '{"b": 2}'::jsonb; -- {"a": 1, "b": 2}

jsonb - text: 좌측 JSON 객체에서 우측에 지정한 키 혹은 값과 일치하는 요소 제거 후 반환

SELECT '{"a": 1, "b": 2}'::jsonb - 'b'; -- {"a": 1}

jsonb - integer: 좌측 JSON 배열에서 우측에 지정한 인덱스에 해당하는 요소 제거후 반환

SELECT '[1, 2, 3]'::jsonb - 1; -- [1, 3]

jsonb - text[]: 좌측 JSON 객체에서 우측에 지정한 키 배열에 해당하는 요소 제거 후 반환

SELECT '{"a": 1, "b": 2, "c": 3}'::jsonb - ['a', 'c']; -- {"b": 2}

jsonb @? jsonpath: 좌측의 JSON 객체가 우측에 지정한 경로에 해당하는 요소가 있고, 정상적으로 접근이 가능하다면 true 반환

SELECT '{"a": {"b": 2}}'::jsonb @? '$.a.b'; -- true

jsonb @@ jsonpath: 우측에 지정한 경로$.a.b( . 표기법으로 접근한) 위치에 좌측의 JSON 객체의 요소가 존재하는지 평가하고 일치(존재)하는 경우 true 반환

SELECT '{"a": {"b": 2}}'::jsonb @@ '$.a.b'; -- true


쉽게 말해 {a : { b: 2} } 가 있는 경우 a.b 로 접근했을 때, 정상적으로 2 라는 값이 존재한다면 경로의 일치 유무를 true 로 판단하며, 접근한 경로에 아무런 값이 존재하지 않는다면 false 를 반환

 

JSONB 로 저장된 배열 형태의 데이터를 NestJS 에서 읽어오려면

NestJS와 TypeORM을 사용하여 PostgreSQL 데이터베이스의 jsonb 타입 컬럼에서 배열 형식의 데이터를 필터링하는 방법을 정리해보고자 한다.

 

예를 들어, 서비스 테이블의 대상 테이블에 ["노년","아동","청소년"]과 같은 데이터가 저장되어 있고, 사용자가 "노년"과 "아동"을 선택하여 필터링하려는 경우에는 어떻게 로직을 작성해야 할까? 

 

서비스 엔티티 정의 및 jsonb 타입 컬럼 설정

먼저, 서비스 엔티티를 정의하고, jsonb 타입 컬럼을 설정해야 한다.

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class Service {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'jsonb', nullable: true })
  targetGroups: string[];
}

 

Repository 메서드 작성

TypeORM의 QueryBuilder를 사용하여 jsonb 타입 필드를 필터링하는 방법을 설명한다. 여기서는 @InjectRepository 데코레이터를 사용하여 ServiceRepository를 주입하고, 원하는 필터링을 적용한다.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Service } from './service.entity';

@Injectable()
export class ServiceService {
  constructor(
    @InjectRepository(Service)
    private readonly serviceRepository: Repository<Service>,
  ) {}

  async findByTargetGroups(targetGroups: string[]): Promise<Service[]> {
    return this.serviceRepository
      .createQueryBuilder('service')
      .where('service.targetGroups @> :targetGroups', { targetGroups: JSON.stringify(targetGroups) })
      .getMany();
  }
}


위 코드에서 @> 연산자는 JSONB 배열에 특정 값이 포함되어 있는지 확인하는 데 사용된다.  @> 연산자는 PostgreSQL의 JSONB 데이터 타입에서 특정 값이 포함되어 있는지 확인할 때 유용하다. 

 

여기서 중요한 점은 targetGroups 의 value 을 JSON 형태로 직렬화하였다는 점이다. 데이터베이스에 저장된 데이터 형식 자체가 JSON 형태의 바이너리 형태로 저장되어 있으므로 이를 비교하기 위해서 당연하게 해주어야 하는 절차이다.

 

Controller 작성

사용자가 요청한 필터 조건을 받아서 서비스 메서드를 호출하는 컨트롤러를 작성한다.

import { Controller, Get, Query } from '@nestjs/common';
import { ServiceService } from './service.service';
import { Service } from './service.entity';

@Controller('services')
export class ServiceController {
  constructor(private readonly serviceService: ServiceService) {}

  @Get()
  async getFilteredServices(
    @Query('targetGroups') targetGroups: string,
  ): Promise<Service[]> {
    const targetGroupsArray = targetGroups.split(',');
    return this.serviceService.findByTargetGroups(targetGroupsArray);
  }
}

 

여기서 주요 포인트는 쿼리 파라미터(쿼리 매개변수)로 받은 targetGroups 를 targetGroups.split(',')를 사용하여 배열로 변환한다.

 

예제 요청

위 설정이 완료되면, 다음과 같이 요청을 보낼 수 있다.

GET /services?targetGroups=노년,아동


이 요청은 targetGroups가 "노년"과 "아동"을 포함하고 있는 데이터를 반환한다.



NestJS 에서 새로운 Entity 를 추가하면 꼭 명시해야 하는 것

entity 를 추가하면 app.module.ts 파일에서 entities 프로퍼티의 배열 요소로 추가한 entity 를 명시해 주어야 한다. 만일 명시하지 않는 경우에는 EntityMetadataNotFoundError: No metadata for "00Model" was found. 와 같은 모델을 찾을 수 없다는 에러가 발생하는 것을 볼 수 있다.

 

바로 아래와 같이 RegionModel 을 import 하여 entities 에 넣어주기만 하면 된다.

import { RegionModel } from './regions/entities/regions.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      entities: [RegionModel], // 앞으로 생성할 테이블(모델)을 넣는 배열
    }),
    RegionsModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 

NestJS TypeORM + class-validator 에서 null 값에 대한 유효성 검증

file.entity.ts 파일에서 테이블 생성 및 DTO 확장을 위한 개체를 생성할 때, 특정 컬럼을 선택적으로 null 을 허용 하고자 한다면, 아래와 같이 작성하면 된다. 이렇게 되면, 해당 컬럼은 데이터베이스에 테이블을 생성 시 string 이면서도 null 값이 들어가는 것을 허용하며, 데이터 유효성 검사 시에도 선택적으로 null 을 허용하게 된다.

  @IsString()
  @IsOptional()
  @Column({ nullable: true })
  lifeTarget: string; // 대상

 

참고

 

Optional columns in TypeORM - René Kulik - Freelance Software Developer

Freelance Software Developer • Creating projects for web and mobile devices

www.kulik.io

 

NestJS 에서 Pipe 

pipe 는 컨트롤러에서 처리되는 메소드 함수가 호출 되기 전에 해당 메소드 함수의 인자로 전달되는 데이터를 수신하여 해당 데이터의 데이터 타입을 변경하는 등의 변환이나 유효성 검사를 통한 특정 데이터에 대한 변환을 진행하는데 사용된다.

 

예를 들어 NestJS 에서 내장된 파이프인 ParseIntPipe 를 사용하는 예시를 살펴보자. 이 경우 :id 로 넘겨받은 params 가 문자형 으로 "123" 이렇게 온다면 pipe 에 의해서 정수형으로 변경되어 id 매개변수에 담기게 된다.

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}

 

만일 "abc" 와 같은 문자열을 :id 에 담아서 보낸 것을 Pipe 가 수신한 경우, 다음과 같인 예외를 반환해준다.

{
  "statusCode": 400,
  "message": "Validation failed (numeric string is expected)",
  "error": "Bad Request"
}

 

커스텀 파이프

사용자가 직접 자신의 입맛에 맞게 파이프를 만들 수 있다. 참고로 해당 파이프 클래스의 경우에도 의존성 주입을 위해 사용되기 때문에 @InJectable() 데코레이터를 붙인 것을 볼 수 있다.

 

커스텀 파이프 만들기

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class CustomValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    // value는 요청으로부터 받은 값이고, metadata는 요청에 대한 메타데이터
    // 여기에서 유효성 검사를 수행하고, 유효하지 않은 경우 예외
    if (!this.isValid(value)) {
      throw new BadRequestException('Validation failed');
    }
    // 유효성 검사를 통과한 경우에는 값을 그대로 반환
    return value;
  }

  private isValid(value: any): boolean {
    // 여기에 사용자가 정의한 유효성 검사 로직을 구현
    // 예를 들어, 값이 존재하는지, 특정 조건을 만족하는지 등을 확인
    return true; // 유효성 검사 통과
  }
}

 

위 예시에서 value 은 사용자의 요청에 의해 전달받은 값을 담고 있고, metadata 는 해당 value 에 대한 추가적인 정보를 담고 있다. 

 

export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}

 

ArgumentMetadata 의 내부 타입 지정을 살펴보면 value 이 어떤 형식으로 전달받은 값인지에 대한 type 외 추가적인 정보가 담겨있음을 예측할 수 있다.

 

커스텀 파이프 사용하기

import { Controller, Get, UsePipes } from '@nestjs/common';
import { CustomValidationPipe } from './custom-validation.pipe';

@Controller('example')
export class ExampleController {

  @Get()
  @UsePipes(new CustomValidationPipe())
  getData(@Query('param') param: string) {
    // 파이프를 사용하여 요청 데이터를 검증하고, 예외를 처리하거나 데이터를 가공하여 반환
    return param; // 이 예제에서는 쿼리 파라미터 'param'을 반환
  }
}

 

 

앞서 만든 커스텀 파이프를 사용하려면  @nestjs/common 모듈에 있는 UsePipes 데코레이터를 불러와야 한다. 해당 데코레이터의 인자로 앞서 생성한 커스텀 파이프를 전달해주면 된다. 이때 주요한 포인트는 new 키워드를 붙여주어야 한다는 점이다.

 

참고자료

https://docs.nestjs.com/pipes

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

 

NestJS 에서 예외 다루기

https://docs.nestjs.com/exception-filters#throwing-standard-exceptions

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

 

NestJS TypeORM 사용 시 그룹별(Group by) 통계내기

NestJS 에서 TypeORM 을 적용한 경우에 특정 컬럼을 기준으로  그룹을 묶고, 각 그룹별로 로우(Row)의 개수를 구하고 싶다면, 아래와 같이 사용한다.

 

  async findAllServiceTotal() {
    const serviceCounts = await this.regionRepository
      .createQueryBuilder('regions') // regions 테이블
      .select('regions.gugun') // 조회될 컬럼을 선택 : 여기서는 regions 테이블의 gugun 만 조회
      .addSelect('COUNT(*) AS serviceCountByGugun') // serviceCountByGugun 이라는 컬럼명으로 조회된 로우 개수를 가져옴
      .groupBy('regions.gugun') // GROUP BY 의 기준이 되는(즉, 그룹으로 묶을 기준이 되는) 컬럼을 선택
      .getRawMany(); // 순수 자바스크립트 객체/배열 형태로 반환한다.
    return serviceCounts;
  }

 

여기서 createQueryBuilder 는 regions 테이블에 대한 쿼리 빌더를 생성하는 TypeORM 에서 제공하는 메소드인데, 보통 TypeORM 에서 제공하는 정적인 ORM 맵핑 방식으로는 처리하기 어려운 동적인 쿼리 처리가 필요한 경우에 사용하는 도구이다.

 

.select 메소드는  컬럼 조회 시 조회할 컬럼을 명시적으로 설정하는 것으로서 regions.gugun 이라고 지정하여 regions 테이블에서도 gugun 컬럼만 선택(select) 해서 조회하겠다는 의미이다.

 

.addSelect 는 일반적인 쿼리로 따지면 SELECT COUNT(*)  AS serviceCountByGugun FROM regions 와 같이 작성할 때 SELECT 와 FROM 사이에 입력된 값을 의미한다. 즉, SELECT 절을 추가한다는 의미이다.

 

.groupBy지정한 컬럼을 기준으로 그룹을 지정할 때 사용한다. 여기서 regions.gugun 으로 지정했으므로 구군별로 로우(Row)를 그룹할 것이라 예상할 수 있다.

 

.getRawMany() 는 조회된 로우(Row)를 반환하는데,  이는 순수한 자바스크립트 배열 형태로 반환 받겠다는 의미이다.

 

.getMany() 와 .getRawMany()의 차이점

getRawMany()

 이 메서드는 순수한 JavaScript 객체를 반환한다. 쿼리 결과가 엔티티 인스턴스로 변환되지 않는다. 따라서 데이터베이스에서 직접 가져온 원시 데이터를 가져올 때 사용된다.


getMany()

이 메서드는 TypeORM 엔티티(클래스)의 인스턴스 배열을 반환한다. 데이터베이스에서 검색된 결과를 엔티티로 변환하여 반환한다.


예를 들어, User 엔티티가 있다고 가정해 보자. getRawMany()를 사용하면 데이터베이스에서 사용자 정보를 가져와서 JavaScript 객체로 반환하는 반면, getMany()를 사용하면 데이터베이스에서 가져온 사용자 정보를 User 엔티티의 인스턴스로 반환한다.

 

즉, 반환 후 바로 데이터를 사용하고 싶으면 getRawMany() 를 사용하자.

 

NestJS 에서 CORS 설정방법

NestJS 에서 CORS 를 설정하려면 main.ts 파일에서 아래와 같이 설정해주면 된다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // CORS 활성화
  app.enableCors();

  await app.listen(3000);
}
bootstrap();

 

만일 특정한 도메인이나 특정 메서드만 허용하려면, 아래와 같이 옵션을 추가하면 된다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 특정 Origin만 허용
  app.enableCors({
    origin: 'http://localhost:4200',
  });

  // 특정 메서드만 허용
  app.enableCors({
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
  });

  await app.listen(3000);
}
bootstrap();

 

 

참고자료

https://github.com/expressjs/cors#configuration-options

반응형