본문으로 건너뛰기

안동민 개발노트

본문 시작

TypeORM을 사용한 데이터베이스 연동

ORM의 역할을 이해하고 TypeORM 엔티티·리포지토리·데이터소스를 Nest 모듈에 연결해 관계형 데이터를 다룹니다.

2장에서는 NestJS의 핵심 개념들을 깊이 있게 살펴보았습니다.

이제 3장에서는 NestJS 애플리케이션의 또 다른 중요한 축인 데이터베이스 통합에 대해 다룹니다.

대부분의 웹 애플리케이션은 사용자 데이터, 게시물, 상품 정보 등 다양한 데이터를 영구적으로 저장하고 관리해야 하므로 데이터베이스 연동은 필수입니다.

NestJS는 다양한 데이터베이스 연동을 지원하지만, 그중에서도 TypeORM은 관계형 데이터베이스(RDBMS) 연동에서 가장 널리 쓰이는 도구 중 하나입니다.

TypeORM은 이름 그대로 TypeScript 기반 ORM(Object-Relational Mapping) 라이브러리입니다.

ORM은 객체 지향 언어의 객체와 관계형 데이터베이스 테이블을 자동으로 매핑하는 기술을 의미합니다.

덕분에 복잡한 SQL을 직접 작성하는 대신, 익숙한 객체 지향 코드로 데이터베이스를 다룰 수 있습니다.

객체 코드는 엔티티와 리포지토리를 거쳐 테이블에 닿는다

TypeORM은 TypeScript 객체와 관계형 데이터베이스 테이블 사이의 변환을 맡는다. 컨트롤러는 SQL 대신 서비스와 리포지토리 API를 호출한다.

  1. Entity

    @Entity와 @Column으로 테이블 모양을 코드에 적는다.

  2. Repository

    엔티티별 데이터 접근 API를 제공한다.

  3. Service

    업무 규칙을 적용한 뒤 repository 메서드를 호출한다.

  4. Database

    TypeORM이 쿼리로 바꾸어 테이블과 통신한다.

INSERT INTO users (email) VALUES (...)
userRepository.save({ email })

ORM이란 무엇이며 왜 사용할까요?

TypeORM 데이터베이스 연동은 entity 정의, repository 주입, relation 처리, transaction 경계를 묶었습니다.

ORM 변환 계층

ORM은 객체 모델과 테이블 모델의 차이를 매핑한다. 개발자는 엔티티와 리포지토리로 작업하고, SQL 생성과 결과 변환은 ORM이 담당한다.

  1. 타입과 자동완성

    엔티티 필드가 코드 편집기에서 바로 보인다.

  2. 반복 쿼리 감소

    CRUD 기본 작업을 리포지토리 API로 처리한다.

  3. 관계 표현

    엔티티 관계를 코드 구조로 드러낼 수 있다.

객체 코드TypeORM 매핑데이터베이스
User class@Entity()users table
email property@Column()email column
repository.save()INSERT 또는 UPDATE 생성row 저장
repository.find()SELECT 생성row를 객체로 변환

ORM을 이해하기 위해 간단한 예를 들어보겠습니다.

여러분이 User라는 객체를 가지고 있고, 이 객체를 데이터베이스의 users 테이블에 저장하고 싶다고 가정해 봅시다.

ORM을 사용하지 않는 경우 (Raw SQL)
// 사용자 데이터를 데이터베이스에 삽입하는 SQL 쿼리
const username = 'Alice';
const email = 'alice@example.com';
const sql = `INSERT INTO users (username, email) VALUES ('${username}', '${email}')`;
// 데이터베이스 드라이버를 통해 SQL 실행...
ORM을 사용하는 경우 (TypeORM)
// User 엔티티 객체 생성
const newUser = new User();
newUser.username = 'Alice';
newUser.email = 'alice@example.com';

// TypeORM의 리포지토리(Repository)를 통해 객체 저장
await userRepository.save(newUser);

보시다시피, ORM을 사용하면 SQL 쿼리를 직접 작성할 필요 없이, 마치 JavaScript 객체를 다루듯이 데이터베이스 작업을 수행할 수 있습니다.

ORM 사용의 주요 장점
  • 생산성 향상: SQL 쿼리를 직접 작성하고 관리하는 수고를 덜어 개발 속도를 높일 수 있습니다.
  • 유지보수 용이성: 객체 지향 패러다임으로 데이터베이스를 다루기 때문에 코드의 가독성과 유지보수성이 향상됩니다.
  • 데이터베이스 독립성: 대부분의 ORM은 다양한 데이터베이스(MySQL, PostgreSQL, SQLite, MS SQL Server 등)를 지원합니다. 데이터베이스를 변경하더라도 코드 수정이 최소화됩니다.
  • 타입 안정성 (TypeScript와 함께): TypeORM은 TypeScript의 타입 시스템을 활용하여, 컴파일 시점에 데이터베이스 관련 오류를 미리 감지할 수 있도록 돕습니다.
  • 객체-관계 불일치 해소: 객체 모델과 관계형 모델 간의 패러다임 불일치(Impedance Mismatch)를 줄여줍니다.

물론, ORM이 모든 상황에 완벽한 해결책은 아닙니다.

매우 복잡하고 최적화가 중요한 쿼리의 경우, 직접 SQL을 작성하는 것이 더 효율적일 수도 있습니다.

하지만 대부분의 애플리케이션에서는 ORM의 장점이 훨씬 크게 다가옵니다.


핵심 개념: 엔티티, 리포지토리, 데이터소스

TypeORM을 이해하고 사용하기 위해서는 몇 가지 핵심 개념을 알아야 합니다.

엔티티 (Entity): 엔티티는 데이터베이스 테이블과 매핑되는 TypeScript 클래스입니다.

각 엔티티 인스턴스는 테이블의 한 행(Row)에 해당하며, 엔티티 클래스의 속성(Property)은 테이블의 컬럼(Column)에 매핑됩니다.

@Entity(), @PrimaryColumn(), @Column() 등의 데코레이터를 사용하여 데이터베이스 스키마를 정의합니다.

예시
src/users/entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('users') // 'users' 테이블과 매핑됩니다.
export class User {
  @PrimaryGeneratedColumn() // 기본 키(Primary Key)이자 자동 증가하는 숫자 컬럼
  id: number;

  @Column({ type: 'varchar', length: 100, unique: true }) // 문자열 타입, 최대 길이 100, 유니크 제약 조건
  username: string;

  @Column() // 타입이 명시되지 않으면 자동으로 추론됩니다 (여기서는 string).
  email: string;

  @Column({ default: true }) // 기본값 설정
  isActive: boolean;
}

User 엔티티는 데이터베이스에 users라는 테이블을 생성하며, id, username, email, isActive 컬럼을 가집니다.

리포지토리 (Repository): 리포지토리는 특정 엔티티에 대한 데이터베이스 작업을 수행하는 추상화된 객체입니다.

save(), find(), findOne(), delete() 등과 같은 메서드를 제공하여 엔티티 데이터를 생성, 조회, 수정, 삭제(CRUD)할 수 있도록 돕습니다.

SQL 쿼리 없이 객체 지향적인 방식으로 데이터베이스를 조작할 수 있게 해주는 핵심 인터페이스입니다.

NestJS에서는 TypeORM 모듈을 통해 EntityManagerRepository를 의존성 주입으로 편리하게 사용할 수 있습니다.

src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User) // User 엔티티에 대한 Repository를 주입받습니다.
    private usersRepository: Repository<User>,
  ) {}

  async createUser(username: string, email: string): Promise<User> {
    const newUser = this.usersRepository.create({ username, email, isActive: true });
    return this.usersRepository.save(newUser); // 데이터베이스에 저장
  }

  async findAllUsers(): Promise<User[]> {
    return this.usersRepository.find(); // 모든 사용자 조회
  }

  async findUserById(id: number): Promise<User | null> {
    return this.usersRepository.findOneBy({ id }); // ID로 사용자 한 명 조회
  }

  async deleteUser(id: number): Promise<void> {
    await this.usersRepository.delete(id); // ID로 사용자 삭제
  }
}

데이터소스 (DataSource) / 연결 (Connection): 데이터소스는 TypeORM이 데이터베이스와 통신하기 위한 연결(Connection) 설정을 담당하는 객체입니다.

어떤 데이터베이스를 사용할지(MySQL, PostgreSQL 등), 연결 정보(호스트, 포트, 사용자명, 비밀번호), 사용할 엔티티 목록 등을 정의합니다.

NestJS 애플리케이션이 시작될 때 이 설정을 기반으로 데이터베이스 연결을 수립합니다.

NestJS에서는 일반적으로 TypeOrmModule.forRoot()를 사용하여 애플리케이션의 루트 모듈(AppModule)에서 이 데이터베이스 연결을 설정합니다.

src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; // TypeORM 모듈 임포트
import { User } from './users/entities/user.entity'; // 엔티티 임포트
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql', // 사용할 데이터베이스 타입 (예: 'mysql', 'postgres', 'sqlite')
      host: 'localhost',
      port: 3306,
      username: 'your_username',
      password: 'your_password',
      database: 'your_database_name',
      entities: [User], // 애플리케이션에서 사용할 엔티티 목록을 여기에 추가합니다.
      synchronize: true, // 개발 환경에서만 true로 설정하여 엔티티에 따라 테이블을 자동으로 생성/업데이트
      logging: true, // SQL 쿼리 로깅 활성화 (개발 시 유용)
    }),
    UsersModule, // 사용자 모듈도 임포트
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

synchronize: true 옵션은 개발 환경에서 매우 유용합니다.

엔티티 클래스의 변경 사항에 따라 데이터베이스 스키마를 자동으로 동기화해 주기 때문입니다.

하지만 운영 환경에서는 절대 true로 설정하면 안 됩니다!

데이터 손실의 위험이 있기 때문에, 운영 환경에서는 마이그레이션 도구(Migration Tool)를 사용하여 스키마 변경을 관리해야 합니다.

엔티티 정의가 리포지토리 호출로 이어지는 순서

엔티티를 정의했다고 바로 서비스에서 쓸 수 있는 것은 아니다. 모듈에 엔티티를 등록하고, 서비스에 repository를 주입해야 데이터 접근 경로가 완성된다.

  1. 1
    Entity

    User 클래스를 테이블 구조로 선언한다.

  2. 2
    forRoot

    DB 연결과 전체 엔티티 로딩 방식을 설정한다.

  3. 3
    forFeature

    현재 모듈에서 사용할 엔티티를 등록한다.

  4. 4
    InjectRepository

    서비스 생성자에 Repository<User>를 주입한다.

  5. 5
    Query

    save, find, update 같은 메서드로 작업한다.

TypeOrmModule.forFeature([User])
constructor(@InjectRepository(User) private users: Repository<User>) {}

NestJS와 TypeORM 설정 요약

NestJS에서 TypeORM을 통합하는 기본적인 단계는 다음과 같습니다.

필요한 패키지 설치: TypeORM과 사용할 데이터베이스 드라이버를 설치합니다.

예를 들어 MySQL을 사용한다면:

npm install @nestjs/typeorm typeorm mysql2
npm install --save-dev @types/node # Node.js 타입 정의 (TypeScript용)

또는 PostgreSQL을 사용한다면:

npm install @nestjs/typeorm typeorm pg

엔티티 정의: @Entity(), @Column() 등의 데코레이터를 사용하여 데이터베이스 테이블과 매핑되는 TypeScript 클래스를 만듭니다.

TypeOrmModule 설정: 루트 모듈(AppModule)의 imports 배열에 TypeOrmModule.forRoot()를 사용하여 데이터베이스 연결 정보를 설정하고, 정의한 엔티티들을 등록합니다.

리포지토리 주입 및 사용: @InjectRepository() 데코레이터를 사용하여 서비스(@Injectable() 프로바이더)에서 특정 엔티티의 Repository를 주입받아 데이터베이스 작업을 수행합니다.

특히 synchronize: true는 로컬 학습에는 편하지만 운영 환경에서는 마이그레이션과 분리해서 다루어야 합니다.

TypeORM 설정 분리

연결 설정은 환경별로 바뀌고, 엔티티와 마이그레이션은 운영 안전성과 직접 연결된다. 특히 synchronize는 개발과 운영을 분리해서 다뤄야 한다.

  1. connection

    host, port, username, password, database는 환경 변수로 분리한다.

  2. entities

    애플리케이션에서 사용할 엔티티 목록을 명확히 로딩한다.

  3. synchronize

    개발 편의 옵션이며 운영에서는 false로 둔다.

  4. migrations

    운영 스키마 변경은 파일과 이력으로 관리한다.

환경synchronize스키마 변경 방식
로컬 학습가능빠른 실습과 자동 반영
공유 개발주의팀 규칙에 따라 제한
운영falsemigration으로 검토 후 적용

TypeORM은 일대일·일대다·다대다 관계, 트랜잭션, 사용자 정의 쿼리 빌더를 제공합니다.

이번 절에서는 기본 개념과 연동 방식에 집중했고, 이후 절에서는 관계 매핑, 트랜잭션 경계, 쿼리 빌더 사용 기준을 이어서 다룹니다.


이제는 NestJS 애플리케이션에 데이터베이스를 통합하는 첫걸음을 내디뎠습니다.

TypeORM을 사용한 데이터베이스 연동 데이터 경계

컨트롤러는 HTTP 요청을 받고, 서비스는 업무 규칙을 적용하며, 리포지토리는 DB 작업을 담당한다. 이 경계를 지켜야 데이터 코드가 흩어지지 않는다.

  1. Controller

    DTO를 받아 서비스에 의도를 전달한다.

  2. Service

    검증된 입력으로 업무 규칙과 트랜잭션 경계를 정한다.

  3. Repository

    엔티티 단위 CRUD와 조건 조회를 수행한다.

  4. Table

    TypeORM이 SQL로 바꿔 관계형 DB와 통신한다.

  5. relations

    엔티티 사이 관계를 어떻게 가져올지 결정한다.

  6. transactions

    여러 변경을 하나의 성공/실패 단위로 묶는다.

  7. query builder

    복잡한 조건과 조인이 필요할 때 사용한다.