7. 데이터베이스 연동 (TypeORM)
요약
이전 글에서는
[초보자의 눈으로 보는 NestJS] 6. DTO와 Validation
6. DTO와 Validation요약이전 글에서는 [초보자의 눈으로 보는 NestJS] 5. 유저 서비스의 구현5. 유저 서비스의 구현 요약 이전 글에서는 [초보자의 눈으로 보는 NestJS] 4. 유저 서비스의 구현과 의존성
ts01.tistory.com
- 컨트롤러와 프로바이더에서 주고 받는 데이터를 DTO(Data Transfer Object)로 정의합니다.
- DTO에 대한 데이터 검증(Data Validation)을 추가하여, 올바른 데이터만이 도달하도록 구현합니다.
본 글에서는
- TypeORM 을 이용하여 데이터베이스를 연동합니다.
- 데이터베이스를 연동한 CRUD 기능을 구현 및 테스트합니다.
7-1. ORM(Object Relational Mapping)이란?
ORM 이란, 객체(Object)와 데이터베이스 테이블(Table) 간 데이터 연결(Mapping)을 수행하는 기술입니다. 영속성(Persistence)을 보장하는 데이터베이스 시스템과 서버 애플리케이션 간 정의된 객체를 자동으로 연결해주기 때문에, 개발 효율이 매우 증대됩니다.
NestJS 과 같은 백엔드 서버 프레임워크에는 여러 ORM 들을 개발자 취향에 맞게 이용할 수 있도록 대부분 라이브러리를 지원합니다. 이 라이브러리(패키지)를 이용하면, 개발자가 개발하는 언어로 데이터베이스의 데이터들을 쉽게 관리 및 조작할 수 있기 때문에, 별도로 SQL 쿼리를 구현하거나 하지 않아도 됩니다. 이것에는 테이블 조회, 테이블 조작, 트랜잭션 관리 등이 포함될 수 있습니다.
7-2. NestJS 와 ORM
NestJS 와 친밀한 ORM 들에는 Prisma, TypeORM, MikroORM, Sequalize 등이 있습니다.
ORM 이름 | 장점 | 단점 |
Prisma ✨ | - 타입이 자동 생성됨 (Typescript 친화적) - 직관적인 관계형 데이터 모델링 - Prisma Studio (GUI) 제공 |
- 동적 쿼리 작성이 다소 불편 - 복잡한 SQL 기능은 직접 작성해야 함 |
TypeORM | - Active Record & Data Mapper 패턴 지원 - 관계형 데이터 모델링에 유용 - NestJS 공식 문서에서 예제 제공 |
- 마이그레이션 비교적 불편 - 일부 동작 불안정 (Type 버그 등 존재) |
MikroORM | - 성능이 뛰어나고 빠름 - 관계형, NoSQL 모두 지원 - Prisma 대비 동적 쿼리 작성이 편리 |
- 러닝커브가 있어 많이 사용되지 않음 |
Sequalize | - Active Record 패턴 중심 - Javascript 스타일의 API 지원 - 강력한 트랜잭션 기능 지원 |
- Typescript 지원이 아쉬움 - 관계형 DB 모델링이 번거로움 |
NestJS 는 Typescript 기반 언어이기 때문에, 언어 친화성이 좋은 Prisma 또는 TypeORM 이 대표적으로 추천됩니다. 본 글에서는 NestJS 공식 문서에서 확인할 수 있는 예제 수준을 설명하기 때문에, TypeORM 을 설명하고자 합니다. 공식 문서는 [링크]에서 확인할 수 있습니다.
7-3. Postgre 데이터베이스
본 글에서는 데이터베이스로 Postgres DB 를 선택하였습니다. Postgres DB 가 이미 설치 및 구동되어 있다면, 이 항목을 건너 뛰어도 좋습니다. 만약 설치되어 있지 않고 Docker Container 로 띄우길 원한다면 아래 글을 참고하시길 바랍니다.
Postgres 데이터베이스 실행하기
Postgres 데이터베이스 실행하기본 글에서는 Postgres 데이터베이스를 Docker Container 로 실행하는 방법을 설명합니다.저는 별도의 데이터베이스 서비스(예를 들어, AWS RDS 와 같은 것들)를 이용하지
ts01.tistory.com
이제 아래 설명에서, Postgres DB 는 다음과 같은 엔드포인트 정보를 가지고 있다고 가정하겠습니다.
- Postgres PORT =
5432
- Postgres USERNAME =
postgres
- Postgres PASSWORD =
password
7-4. TypeORM 설치
YARN 패키지를 기반으로, NestJS 보일러플레이트(또는 자신의 NestJS 개발 환경)에서 아래 명령어를 통해 TypeORM 을 설치합니다. 본 글에서는 [이전 글]의 소스코드에서 이어 진행합니다.
yarn add @nestjs/typeorm typeorm pg
3가지 패키지가 설치되는데, 각각 NestJS 와 TypeORM 을 연동하는 패키지, TypeORM 패키지, Postgres 지원 패키지입니다. 설치가 완료되면 package.json 파일에 아래와 비슷한 항목들이 포함되어 있음을 확인할 수 있습니다.
"dependencies": {
"@nestjs/common": "^9.0.0",
"@nestjs/core": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/typeorm": "^11.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"pg": "^8.14.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0",
"typeorm": "^0.3.21"
}
설치가 완료되었으면, 이제 NestJS 에서 이 TypeORM 을 통해 데이터베이스와 연결을 수행해 보겠습니다.
7-5. NestJS - TypeORM 을 통한 데이터베이스 연동
app.module.ts 파일로 갑니다. 아래와 같은 형태를 가지고 있습니다.

이곳에 TypeOrmModule
을 추가(Import)하겠습니다.

TypeOrmModule
은 데이터베이스와 연동할 수 있는 전역 설정들을 수립하는 .forRoot
함수와 소규모 모듈 영역에서만의 설정을 주입할 수 있는 .forFeature
함수를 실행할 수 있는 Class 입니다. 여기서는 AppModule
단위, 즉, 전역 단위에서 데이터베이스를 연결하고 각 모듈 내에서 해당 데이터베이스에 대한 접근을 가능하게 하도록 합니다.
TypeOrmModule.forRoot
에 포함될 수 있는 설정에는 다양한 것들이 있지만, 많이 사용되는 옵션에는 아래와 같은 것들이 있습니다.
TypeOrmModule.forRoot({
type: "postgres", // 데이터베이스 타입을 지정
host: "localhost", // 데이터베이스 HOST 주소를 지정
port: 5432, // 데이터베이스 PORT 번호를 지정
username: "postgres", // 데이터베이스 USER 이름을 지정
password: "password", // 데이터베이스 비밀번호를 지정
database: "postgres", // 데이터베이스 이름(구분)을 지정
entities: [], // 선언된 데이터베이스 엔티티들을 지정
synchronize: true, // 소스코드에서 데이터베이스로의 SYNC(동기화) 여부 (주의! 저 아래 설명 있음)
}),
TypeOrmModule.forRoot
함수 내에 자신의 데이터베이스 정보를 모두 입력하였다면, NestJS 애플리케이션을 실행하여 연결성을 확인해 보도록 합니다. 만약 오류가 발생한다면, 데이터베이스가 올바르게 구동된 것이 아니거나, 정보를 잘못 기입한 것이므로 검토해 보도록 합니다.

만약 데이터베이스에 연결하는 것에 에러가 발생하면 아래와 같은 문구가 발생하고 정해진 Connection Retry 를 실행합니다.
[Nest] 14579 - 2025. 01. 01. 오후 12:00:00 ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
error: password authentication failed for user "postgres"
위 에러의 경우, 입력된 Password 값이 잘못되었다는 의미이므로, DB 비밀번호를 점검하도록 합니다.
올바르게 구동되었다면, 아래와 같은 문구가 나타나게 됩니다. 로그 사이에 TypeOrmModule 및 TypeOrmCoreModule 등이 보이는 것을 확인할 수 있습니다.

데이터베이스 연동이 완료되었다면, 이제 TypeORM 을 기반으로 NestJS 코드를 통한 데이터베이스 작업 수행이 가능하게 됩니다.
7-6. 데이터베이스 Entity 생성
엔티티(Entity)란 단위/독립/대상 개체로써, 데이터 모델링에서 사용되는 객체를 의미합니다. 이는 직접적인 대상 명사가 되기도 하고, 개념적으로 표현되기도 하는데, (그룹(팀) 간 엔티티를 정의하는 것은 디테일에서 차이가 있을 수 있지만) 기본적으로 개발/업무에서 사용하는 데이터 집합을 의미합니다. 업무에서 정의하고 관리하고 싶은 형태가 속성(Attribute)으로 정의되며, 다수의 속성을 가진 데이터들이 모여 관리되어야 할 때, 이를 엔티티로써 관리하게 됩니다.
예를 들어, 사용자를 관리하는 시스템에서는 유저 엔티티가 있을 것이고, 강의/수업을 관리하는 시스템에서는 강의 엔티티가 있을 것입니다. 그리고 이 각 엔티티 간의 관계가 정의되고, 이 엔티티에 해당하는 데이터들을 관리하기 위해 데이터베이스가 이용되므로, 데이터베이스를 이용한다는 것은 데이터/엔티티 설계가 뒤따르게 됩니다.
[예전 글]에서 인용하면, 속성을 가진 데이터는 데이터 도메인을 담은 객체(Object)이거나, 데이터베이스와 연계된 개체(Entity)로 구현 및 표현되는데, 우리는 이제 데이터베이스를 연동했기 때문에, 도메인 객체 외에 데이터베이스 엔티티를 정의할 수 있게 되었습니다.
아래는 기존 정의했던 유저 도메인 객체입니다.

유저는 서로를 구분할 수 있는 ID, 각자의 정보에 해당하는 닉네임, 이메일, 비밀번호, 생년월일을 가지고 있습니다. 우리 시스템에서는 "유저"라고 하는 단위가 가지는 속성들을 위와 같이 정의한 것입니다. 이를 기반으로 우리는 데이터베이스 엔티티를 구현해 보겠습니다.
도메인 객체와 엔티티를 어떻게 선언할 것인가?
우리가 처음 구현한 것은 도메인 객체, 즉, 소스코드에서 이 데이터를 관리하기 위해 정의한 단위(=유저)와 속성들(=닉네임, 비밀번호 등)입니다. 가장 쉽게 엔티티를 구현하는 것은 이 도메인 객체와 일치한 형태의 엔티티를 만드는 방법입니다. 단위나 속성을 그대로 이용함으로써, '데이터베이스 데이터(Row)' = '소스코드에서 정의한 엔티티' = '소스코드에서 정의한 도메인 객체'를 일치시켜 그대로 데이터를 다루는 방식이므로, 크게 다른 점을 고려하지 않아도 되는 장점이 있습니다. 그러나 우리가 더 거대한 프로젝트를 경험하게 되면, 이것들이 꼭 일치하지 않는다는 사실을 알게 됩니다. 그 경우 도메인 객체와 데이터베이스 엔티티는 크게 달라질 수 있습니다. 각 사용되는 영역과 관리되는 영역에서 어떤 것을 중점으로 두는가에 대해 꼭 고민하고 설계를 진행하는 것이 좋습니다.
이 글에서는 도메인 객체와 동일한 데이터베이스 엔티티를 선언하여, 이용하는 방식에 대해 설명하도록 하겠습니다.
src/user 경로에 /entity 디렉토리를 생성하고, user.entity.ts 파일을 생성합니다.

새로운 파일에 아래 내용을 작성합니다.

이곳에서는 기존 도메인 객체 User
에 해당하는 동일한 엔티티를 만들기 위하여 확장(Extends)을 수행하였습니다.
각 Decorator 는 typeorm
에서 제공하는 데코레이터로써, 아래와 같은 기능을 수행합니다.
데코레이터 이름 | 설명 | 매개변수 |
@Entity |
이 Class 가 Entity 임을 명시 | 이 Entity 에 해당하는 테이블의 이름을 지정 |
@PrimaryGeneratedColumn |
자동 생성되는 Primary Key Column 임을 명시 | PK 형태가 무엇인지 지정 |
@Column |
테이블의 Column 에 해당함을 명시 | 옵션을 추가할 수 있음 |
엔티티 작성을 완료하였으면, 이 엔티티가 데이터베이스 상 존재해야 하고, 그것과 연결될 수 있습니다.
기본적으로 데이터베이스를 따로 관리한다고 생각하면, SQL문 등을 이용하여 NestJS 작업 이전에 데이터베이스에 테이블을 생성해둘 수 있습니다. 이 경우에는TypeOrmModule
옵션의synchronize: true
옵션을 이용해서는 안 됩니다. 이 옵션을 이용하는 경우 NestJS 에서 작성한 엔티티가 가장 우선순위가 되고 이를 기준으로 동기화가 수행되기 때문에 기존 데이터베이스에 선언된 테이블이 강제 변경되면서 데이터의 손실을 경험할 수도 있습니다. 본 글에서는 엔티티를 직접 NestJS 에서 선언하고 이에 대한 테이블 생성을 요청하게 되므로true
값을 이용하나, 위와 같이 진행중이라면 반드시false
로 설정하시기 바랍니다.
엔티티의 연결을 위해 자동으로 찾지 않기 때문에, 이 UserEntity
를 직접 TypeOrmModule
에게 알려주어야 합니다. 아까 설정 값에서 본 entities
값에 해당 UserEntity
를 포함한 배열을 대입하도록 합니다.

올바르게 실행되었다면 데이터베이스에도 해당 유저와 관련된 테이블이 생성되었음을 확인할 수 있습니다. 확인에는 DBeaver 와 같은 데이터베이스 관리 GUI 툴을 이용하여도 좋겠습니다.
이제 우리는 UserEntity
를 기반으로 하여 데이터베이스 user
테이블과의 소통을 수행할 수 있게 되었습니다.
7-7. Repository Pattern
Repository Pattern 이란, 데이터와의 상호작용을 캡슐화하는 디자인 패턴을 의미합니다. 이 패턴을 이용하면, 우리가 직접 다루는 도메인(비즈니스) 영역과 실제 데이터 액세스(저장하거나 조회하는) 영역을 분리할 수 있게 됩니다. 어떻게 보면 일을 2배로 하는 것 아닌가라고 생각할 수 있는데, 이 영역을 분리해 두면, 특정 로직을 확장 및 수정할 때 유리하게 됩니다.
우리의 비즈니스 영역은 실제 데이터베이스와 연결하고, 테이블과 소통하기 위해 직접 호출하는 것이 아니라, 중간에 Repository 라고 하는 코드 영역만을 호출하기 때문에, 데이터베이스가 변경되거나(Postgres에서 MySQL로 변경 등), ORM을 변경하거나 등에 코드 수정을 더 편리하게 수행할 수 있습니다. 관통하는 모든 로직을 변경할 필요 없이, Repository 에서만 수정하면 되기 때문입니다.
그래서 대부분의 ORM 에서는 Repository Pattern 을 지원합니다. 거의 대부분 두 기술이 일맥상통하여 이용되곤 합니다. TypeORM 에서도 당연히 이 Repository Pattern 을 지원하며, Entity 를 선언하고 이를 주입하면, 자동으로 Repository 패턴을 이용할 수 있도록 제공합니다.
선언된 UserEntity
를 Repository 로 이용하기 위해서 UserModule
코드를 수정합니다.

UserEntity
가 해당 UserModule
모듈 내에서 Repository 로써 주입되었습니다. 이를 선언하면, 유저 모듈 하위에서는 UserEntity
에 대한 Repository 를 주입하여 이용할 수 있게 됩니다.
기존 user.service.ts 코드를 확인하겠습니다.

내용이 길지만 함축해보면, 이전에 선언했던 도메인 객체 User
를 기반으로 유저 배열을 선언해두고, 새로운 유저를 생성하거나 ID를 기반으로 해당 유저를 조회하는 기능이 구현되어 있습니다.
이는 Class 내 멤버변수로 선언되어 있는 배열이기 때문에 애플리케이션의 종료/재실행에 따라 초기화됩니다. 하지만 이제 우리는 데이터베이스를 연결했으므로, 이 도메인 객체 대신 유저 엔티티를 이용할 수 있게 되었습니다.
그에 따른 코드가 아래와 같이 변경됩니다.

먼저, 기존에 없던 생성자 함수 constructor
가 추가되었습니다. 이는 기존 UserController
에서 UserService
를 주입하던 것과 같은 방식인데, UserService
에서 Repository<UserEntity>
를 사용하기 위해 UserModule
단위에서 주입한 UserEntity
의 Repository 를 가져오도록 @InjectRepository
데코레이터를 이용한 것입니다. 이 데코레이터를 통해 UserEntity
를 기반으로 하는, 자동 생성된 Repository 를 이용할 수 있게 됩니다. (즉, 바로 데이터베이스 CRUD 기능을 이용할 수 있게 됩니다.)
각 함수의 기능도 달라졌습니다. 원래 새로운 유저 가입 시, 배열에 저장하던 부분 대신 repository.save
함수를 이용하여 직접 데이터베이스에 저장하는 기능으로 변경되었습니다. 반면, 유저를 조회할 때 id
값을 기반으로 유저 조회 시, repository.findOneBy
함수를 이용하여 id
를 기반으로 하는 UserEntity
를 조회하도록 하였습니다.
이제 NestJS 애플리케이션은 외부 영역인 데이터베이스와 연결되었기 때문에, 비동기(Async)를 고려해야 합니다. NestJS 가 혼자만 동작하는 애플리케이션이었다면 비동기를 고려할 필요가 없으나, 외부에 데이터베이스가 존재하고, 이 데이터베이스가 처리되고 통신하는, 별도의 시간이 필요하기 때문에, NestJS 서버 기능(함수)은 때에 따라 이를 기다려야 합니다. 이 때문에, 아래 조회 함수는 데이터베이스 조회 결과를 기다리고 그 결과를 반환해야 하므로, 비동기 프로세스를 동기처럼 기다리기 위해 몇가지 구문(async, await, promise 등)이 추가된 것을 확인할 수 있습니다. 반면, 저장하는 기능은 기다리지 않고 별도로 데이터베이스에게 명령을 내려둔 다음 지나가도 무방하도록 구현되어, 비동기 처리가 존재하지 않습니다.
여기서 하나 살펴봐야 할 부분이 있는데, 바로 생성 시에 이용되는 id
값입니다. 앞서 UserEntity
선언 당시 id
값에 대한 정의를 @PrimaryGeneratedColumn("uuid")
로 해두었기 때문에, 이는 id
값을 대입하지 않고 테이블에 저장하는 경우, 자동으로 UUID 를 할당하겠다는 의미가 됩니다. 따라서 기존에는 랜덤한 값으로 id
를 직접 대입해 주었으나, 이곳에서는 의도적으로 undefined
값을 할당하여, 자동 생성된 ID 를 받겠다고 한 것입니다.
근데 또 이렇게 해두면 우리가 비동기 방식으로 저장을 수행하고 있기 때문에 자동 생성된 id
값을 알지 못해 조회를 할 수가 없습니다. 그러므로 최종적으로 아래와 같이 생성 함수 또한 비동기 처리 코드로 변경합니다.

위와 같이 코드를 변경하였으면, 에러가 발생합니다. 왜냐하면 UserService
함수들이 비동기 처리 함수가 되었으므로, 이를 이용하는 UserController
함수들도 변경되어야 하기 때문입니다. 아래와 같이 코드를 수정합니다.

7-8. 유저 생성과 조회 테스트
여기서는 앞선 코드 수정과 함께 데이터베이스 연동이 잘 수행되는지 확인하는 테스트를 Postman 으로 수행하겠습니다.
먼저, NestJS 애플리케이션을 실행하고, 유저 생성을 호출해 봅니다.

결과에서 보이는 것과 같이 입력한 데이터대로 유저가 생성되었으며, 생성 당시에 새로운 UUID 값을 할당 받아, 저장완료된 데이터가 반환된 것을 확인할 수 있습니다.
다음으로 해당 유저를 그대로 조회해 보겠습니다.

반환된 id
값을 이용하여 유저 조회를 수행하면, 데이터베이스에 저장되어 있던 데이터가 조회된 것을 확인할 수 있습니다.
데이터베이스와 연동되어 데이터를 저장하고 있으므로, NestJS 애플리케이션을 재실행하여도 조회는 계속 실행되는 것을 확인할 수 있습니다.
결론
NestJS 에서 TypeORM 을 기반으로 데이터베이스와 연동하고, 도메인 객체 외에 엔티티를 생성하며, Repository 패턴을 통해 직접 데이터베이스와 소통(저장 및 조회)하는 것을 수행하였습니다.
이는 다른 ORM 을 이용하거나, 여러 Entity 를 생성하고 관계를 정의하거나, 직접 Repository 패턴을 구현하는 등으로 확장될 수 있으며, 이제 NestJS 를 통해 단순 실행 애플리케이션이 아닌 데이터를 직접 관리할 수 있는 영속성 있는 애플리케이션으로 구현할 수 있게 되었습니다.
다음 글에서는, 이를 좀더 확장하여 좀더 다양한 CRUD 기능 및 엔티티 관계와 Repository 의 사용법 등에 대해서 설명하도록 하겠습니다.
읽어주셔서 감사합니다.
'개발 💻 > NestJS' 카테고리의 다른 글
[Swagger] BasicAuth 문서 접근 보안 (0) | 2025.01.08 |
---|---|
[초보자의 눈으로 보는 NestJS] 6. DTO와 Validation (1) | 2024.05.31 |
class-validator 의 @IsBoolean 데코레이터와 enableImplicitConversion 설정된 Transform 의 문제 (0) | 2024.02.29 |
[초보자의 눈으로 보는 NestJS] 5. 유저 서비스의 구현 (1) | 2024.02.24 |
[초보자의 눈으로 보는 NestJS] 4. 유저 서비스의 정의와 의존성 주입 (2) | 2023.11.13 |