[NestJS] PostgreSQL과 TypeORM

NestJS #3 — PostgreSQL과 TypeORM

따라하면서 배우는 NestJS 강의 내용을 정리한 글이다.

1. PostgreSQL 로컬 설정

TypeORM은 PostgreSQL, MySQL, SQLite, Oracle 등 다양한 DB를 지원한다. 이 강의에서는 PostgreSQL을 사용한다. 설치 후 데이터베이스를 만들어야 한다. 테이블은 TypeORM이 자동 생성하므로 직접 CREATE TABLE을 작성할 필요 없다.

macOS는 Homebrew, Windows는 공식 사이트 인스톨러로 설치한다. 설치 후 아래 명령으로 데이터베이스를 생성한다.

createdb board-app

pgAdmin 같은 GUI 툴을 쓰는 경우, 접속 후 "board-app"이라는 이름으로 새 데이터베이스를 생성한다. Windows 설치 시 설정한 비밀번호를 이후 TypeORM 설정에서 그대로 사용한다.

synchronize: true
TypeORM의 synchronize: true 옵션을 사용하면 서버가 시작될 때 Entity 클래스의 구조를 읽어 DB 스키마를 자동으로 맞춰준다. 개발 환경에서는 편리하지만 프로덕션에서는 반드시 false로 설정해야 한다. Entity를 수정하면 기존 데이터가 유실될 수 있다.

2. TypeORM 패키지 설치

NestJS에서 TypeORM을 사용하려면 세 개의 패키지가 필요하다. @nestjs/typeorm은 NestJS와의 통합 모듈이고, typeorm은 ORM 본체, pg는 PostgreSQL Node.js 드라이버다.

npm install @nestjs/typeorm typeorm pg

ORM(Object Relational Mapper)은 TypeScript 클래스와 관계형 DB 테이블을 연결한다. 직접 SQL을 작성하는 대신 TypeScript 메서드 호출로 DB 작업을 수행할 수 있다.

방식 코드 예시
SQL 직접 작성 SELECT * FROM board WHERE id = $1
TypeORM 사용 this.boardRepository.findOne({ where: { id } })

TypeORM은 Active RecordData Mapper 두 가지 패턴을 지원한다. Active Record는 Entity 클래스 자체에 DB 메서드가 있는 방식이고, Data Mapper는 별도 Repository 클래스가 DB 작업을 담당하는 방식이다. NestJS에서는 테스트와 결합도 분리 측면에서 Data Mapper 패턴을 권장한다.


3. TypeOrmModule.forRoot 설정

연결 설정은 별도 파일로 분리하는 것이 관례다. src/configs/typeorm.config.ts를 만들어 설정 객체를 정의한다.

import { TypeOrmModuleOptions } from '@nestjs/typeorm';

export const typeORMConfig: TypeOrmModuleOptions = {
  type: 'postgres',
  host: 'localhost',
  port: 5432,
  username: 'postgres',
  password: 'postgres',
  database: 'board-app',
  entities: [__dirname + '/../**/*.entity.{js,ts}'],
  synchronize: true,
};

entities의 글로브 패턴 **/*.entity.{js,ts}는 프로젝트 내 모든 Entity 파일을 자동으로 등록한다. 새 Entity를 만들 때 별도 등록 없이 파일명 규칙만 지키면 된다.

루트 모듈에서 TypeORM을 등록한다.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BoardsModule } from './boards/boards.module';
import { typeORMConfig } from './configs/typeorm.config';

@Module({
  imports: [
    TypeOrmModule.forRoot(typeORMConfig),
    BoardsModule,
  ],
})
export class AppModule {}
TypeORM 버전 주의
강의 원본은 TypeORM 0.2 기준이지만, 현재 최신 버전은 0.3(1.x)이다. 0.2와 0.3 사이에는 Repository 사용 방식에 중요한 차이가 있다. 아래 7번 섹션에서 차이를 정리한다.

4. Board Entity

이전 편의 Board 인터페이스를 TypeORM Entity 클래스로 교체한다. @Entity()가 붙은 클래스가 하나의 DB 테이블에 매핑된다.

import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { BoardStatus } from './board-status.enum';

@Entity()
export class Board extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column()
  description: string;

  @Column()
  status: BoardStatus;
}
데코레이터 역할
@Entity() 이 클래스가 DB 테이블임을 선언한다. 기본적으로 클래스명 소문자를 테이블명으로 사용한다 (board).
@PrimaryGeneratedColumn() 자동 증가(AUTO_INCREMENT) 기본 키 컬럼을 선언한다. UUID로 하려면 @PrimaryGeneratedColumn('uuid')를 사용한다.
@Column() 일반 컬럼을 선언한다. @Column({ nullable: true })처럼 옵션을 줄 수 있다.
extends BaseEntity Active Record 스타일의 메서드(save(), remove())를 Entity에서 직접 쓸 수 있게 한다. Data Mapper에서는 선택 사항이다.
id 타입의 변화
이전 메모리 버전에서 iduuid로 생성한 string이었다. TypeORM으로 교체한 뒤에는 DB가 자동으로 부여하는 number형 정수가 된다. 따라서 컨트롤러 파라미터에서 ParseIntPipe를 사용해야 한다.

5. 커스텀 Repository — DataSource 주입 패턴 (TypeORM 0.3)

TypeORM 0.3(1.x)에서는 @EntityRepository() 데코레이터가 제거되었다. 커스텀 Repository를 만드는 방법이 완전히 달라진다.

0.3에서는 DataSource를 주입받아 Repository<T>를 확장하는 방식을 사용한다. 클래스에 @Injectable()을 붙여 NestJS DI 시스템에 등록한다.

import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { Board } from './board.entity';
import { BoardStatus } from './board-status.enum';
import { CreateBoardDto } from './dto/create-board.dto';

@Injectable()
export class BoardRepository extends Repository<Board> {
  constructor(private dataSource: DataSource) {
    super(Board, dataSource.createEntityManager());
  }

  async createBoard(createBoardDto: CreateBoardDto): Promise<Board> {
    const { title, description } = createBoardDto;

    const board = this.create({
      title,
      description,
      status: BoardStatus.PUBLIC,
    });

    await this.save(board);
    return board;
  }
}

create()는 Entity 인스턴스를 생성만 한다. DB에는 아무것도 쓰지 않는다. save()가 실제로 DB에 INSERT(혹은 UPDATE)를 실행한다.

BoardsModule에서 Repository를 등록하는 방식도 바뀐다. forFeature에 클래스 대신 Entity를 전달하고, Repository는 providers에 직접 추가한다.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Board } from './board.entity';
import { BoardRepository } from './board.repository';
import { BoardsController } from './boards.controller';
import { BoardsService } from './boards.service';

@Module({
  imports: [TypeOrmModule.forFeature([Board])],
  controllers: [BoardsController],
  providers: [BoardsService, BoardRepository],
})
export class BoardsModule {}
forRoot vs forFeature
TypeOrmModule.forRoot()는 앱 루트에서 DB 연결 자체를 설정한다. TypeOrmModule.forFeature([])는 특정 모듈에서 특정 Entity를 사용할 수 있도록 등록한다. 두 단계를 모두 거쳐야 해당 모듈에서 Repository를 사용할 수 있다.

6. CRUD 메서드를 Repository 방식으로 변환

Service에서 메모리 배열을 완전히 제거하고, Repository를 생성자 DI로 주입받는다. 모든 메서드가 async/await를 사용해 비동기로 바뀐다.

Service 의존성 주입

TypeORM 0.3에서는 @InjectRepository() 없이 생성자 DI로 바로 주입받을 수 있다. BoardRepository@Injectable()로 등록되어 있기 때문이다.

@Injectable()
export class BoardsService {
  constructor(
    private boardRepository: BoardRepository,
  ) {}
}

전체 조회 — getAllBoards

async getAllBoards(): Promise<Board[]> {
  return this.boardRepository.find();
}

단건 조회 — getBoardById

TypeORM 0.3에서 findOne(id)는 제거되었다. 반드시 findOne({ where: { id } }) 형태로 작성해야 한다.

async getBoardById(id: number): Promise<Board> {
  const found = await this.boardRepository.findOne({ where: { id } });

  if (!found) {
    throw new NotFoundException(`Can't find Board with id ${id}`);
  }

  return found;
}

생성 — createBoard

생성 로직은 Repository에 캡슐화했다. Service는 Repository의 createBoard()를 그대로 호출한다.

createBoard(createBoardDto: CreateBoardDto): Promise<Board> {
  return this.boardRepository.createBoard(createBoardDto);
}

삭제 — deleteBoard

TypeORM에서 삭제 메서드는 두 가지다.

메서드 특징 DB 쿼리 횟수
remove(entity) Entity 인스턴스를 받아서 삭제한다. 반드시 조회 후 사용해야 한다. SELECT + DELETE (2번)
delete(조건) id나 조건 객체를 받아서 바로 삭제한다. Entity를 먼저 가져오지 않아도 된다. DELETE (1번)

delete()가 반환하는 DeleteResultaffected 속성이 실제로 삭제된 행 수를 나타낸다. 0이면 해당 id의 게시물이 없다는 의미이므로 404를 던진다.

async deleteBoard(id: number): Promise<void> {
  const result = await this.boardRepository.delete(id);

  if (result.affected === 0) {
    throw new NotFoundException(`Can't find Board with id ${id}`);
  }
}

상태 업데이트 — updateBoardStatus

조회 후 수정하고 저장하는 흐름이다. getBoardById()가 이미 404 처리를 포함하므로 중복 작성이 필요 없다.

async updateBoardStatus(id: number, status: BoardStatus): Promise<Board> {
  const board = await this.getBoardById(id);

  board.status = status;
  await this.boardRepository.save(board);

  return board;
}

Controller — ParseIntPipe 적용

URL 파라미터는 항상 문자열로 들어온다. DB의 idnumber이므로 ParseIntPipe를 파라미터에 적용해 자동 변환한다. 변환에 실패하면 NestJS가 자동으로 400 에러를 반환한다.

@Get('/:id')
getBoardById(
  @Param('id', ParseIntPipe) id: number
): Promise<Board> {
  return this.boardsService.getBoardById(id);
}

@Delete('/:id')
deleteBoard(
  @Param('id', ParseIntPipe) id: number
): Promise<void> {
  return this.boardsService.deleteBoard(id);
}

@Patch('/:id/status')
updateBoardStatus(
  @Param('id', ParseIntPipe) id: number,
  @Body('status', BoardStatusValidationPipe) status: BoardStatus
): Promise<Board> {
  return this.boardsService.updateBoardStatus(id, status);
}

7. TypeORM 0.2 vs 0.3(1.x) 차이

강의 원본은 TypeORM 0.2 기준으로 작성되어 있다. 2022년 이후 출시된 0.3 버전에서 여러 API가 변경 또는 제거되었다. 현재 프로젝트를 최신 버전으로 세팅할 때 반드시 확인해야 하는 항목들이다.

항목 TypeORM 0.2 (강의 원본) TypeORM 0.3 (현재 최신)
커스텀 Repository 등록 @EntityRepository(Board)
extends Repository<Board>
@Injectable()
DataSource 생성자 주입
forFeature 등록 forFeature([BoardRepository]) forFeature([Board])
providers에 BoardRepository 추가
Service DI @InjectRepository(BoardRepository)
private boardRepository
private boardRepository: BoardRepository
(@InjectRepository 불필요)
단건 조회 findOne(id) findOne({ where: { id } })
getCustomRepository() connection.getCustomRepository() 제거됨. DataSource.getRepository() 사용
강의 코드가 실행되지 않을 때
강의를 따라 작성했는데 @EntityRepository is not a function 오류가 발생한다면 TypeORM 0.3이 설치된 것이다. 위 표를 참고해 코드를 수정하거나, npm install typeorm@0.2로 구버전을 명시 설치할 수 있다. 실무에서는 최신 버전(0.3)에 맞춰 작성하는 것을 권장한다.

8. 변경 요약

메모리 기반에서 TypeORM 기반으로 교체할 때 코드 전반에서 달라지는 부분을 정리한다.

항목 이전 (메모리) 이후 (TypeORM)
Board 데이터 타입 interface Board class Board (@Entity)
id 타입 string (uuid) number (AUTO_INCREMENT)
데이터 저장소 private boards: Board[] BoardRepository (PostgreSQL)
메서드 반환 타입 Board, void (동기) Promise<Board>, Promise<void>
id 파라미터 파싱 @Param('id') id: string @Param('id', ParseIntPipe) id: number
삭제 방식 filter()로 배열 교체 repository.delete(id)

메모리 배열 기반 CRUD를 PostgreSQL + TypeORM으로 전환했다. Board 인터페이스를 Entity 클래스로 교체하고, 데이터 접근은 커스텀 Repository로 캡슐화했다. 모든 서비스 메서드가 async/await로 바뀌었고, URL 파라미터의 id를 ParseIntPipe로 자동 변환해서 타입 불일치 문제를 없앴다.

'Study > Node.JS' 카테고리의 다른 글

[NestJS] JWT 로그인 구현  (0) 2026.05.31
[NestJS] 인증 모듈과 회원가입  (0) 2026.05.31
[Nest.js] 메모리 CRUD와 Pipes  (0) 2026.05.17
[NestJS] NestJs 소개와 기본 구조  (0) 2026.05.17
[Express] 인증 & JWT & OAuth  (0) 2026.04.08