[NestJS] TypeORM 기본 CRUD 작성하기

JeungJoo Lee
CrocusEnergy
Published in
15 min readSep 17, 2020

지난 시간에는 [NestJS] 그 외 기본 개념들 을 살펴보았다. 이번 시간에는 NestJS 에서 TypeORM 을 사용하는 방법을 알아보도록 하겠다. 기본적인 CRUD 를 통해 알아보도록 할텐데 지난 포스팅에서 PostgreSQL Docker 를 설치한 글을 확인하면 조금 더 실습을 하는데 원활할 것이다.

TypeORM — 기본 CRUD 작성

사실 TypeORM 은 NestJS 를 위해 나온 것은 아니고 NestJS 와 궁합이 잘 맞는 Object Relation Mapper( ORM ) 이기 때문에 사용하는 것이다. 우선 DB는 관계형 DB든 NoSQL이든 Vendor 가 무엇이든 대표적으로 Sequelize와 TypeORM 을 많이 사용한다. TypeORM은 Typescript를 잘 지원하고 있고 상대적으로 Java의 JPA 와 비슷한 부분이 많고 개인적 취향에 의해 TypeORM 이 더 매력적이어서 선택하게 되었다.

ORM 을 쓸 때 가장 매력적이었던 부분은 개발자가 TypeORM 을 통해 Vendor 가 어떤 것이든 추상화 객체를 통해 접근하여 코드의 일원화를 꾀할 수 있다는 부분이다. 물론 복잡한 쿼리를 사용할 때는 Native Query 를 사용할 수 밖에 없는 상황들이 발생할 때도 있지만 그래도 생산성이 높고 SQL 작성 보단 도메인 모델을 기반으로 적절한 설계와 유연성을 발휘할 수 있다는 것이 우선 ORM 의 일반적인 매력이다.

TypeORM 설치

npm install --save @nestjs/typeorm typeorm pg

우선 코드를 작성하기 전 dependency 설치가 필요하다 위와 같이 설치를 진행하자.

이전 챕터에서 도커 컴포즈를 통해 postgres 컨테이너를 올려 보았는데 해당 컨테이너 DB 에 NestJS app.module.ts 의 imports 를 통해 DB와 접속 해보도록 하겠다.

src/app.module.ts

... 생략  ...@Module({
imports: [
**TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'uaa',
entities: [],
synchronize: true,
}),**
UserModule,
TestModule,
ExceptModule,
],
controllers: [AppController],
providers: [AppService],
})
... 생략 ...

AppModule 데코레이터의 imports 부분에 TypeOrmModule 객체의 forRoot 를 통해 접속 정보와 커넥션 option을 작성 후 Context Reload 가 되면 Nest 코어는 아래와 같이 TypeOrmModule 을 초기화 한다.

User Entity 만들기

지난 예제에서 만든 src/user 위치에 domain 폴더를 만들고 User 클래스를 만들어 Entity 의 역할을 할 수 있도록 코드로 작성 해보겠다.

src/user/domain/User.ts

import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm/index';@Entity()
export class User {
@PrimaryGeneratedColumn('rowid')
id: number;
@PrimaryColumn()
userId: string;
@Column()
userName: string;
@Column()
userPassword: string;
@Column()
age: number;
@Column({ default: true })
isActive: boolean;
}

위와 같이 간단한 User Entity 를 만들어 보았고 완성된 User Entity 는 AppModule 의 TypeOrm의 Dynamic Module 의 entities 프로퍼티에 value로 넣는다.

❓AppModule 에 넣는데 왜 굳이 user 디렉토리에서 관리하는 것 일까라고 생각할 수 있는데 앞으로 쓰여질 기능 도메인의 위치에 엔티티 파일을 넣는 것이 조금 더 명확하게 구조를 잡을 수 있다는 생각이라 한다는 NestJS 프로젝트의 Reference 공식 문서에 있다.

src/app.module.ts

... 생략 ...@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'uaa',
**entities: [User], // 여기에 생성한 Entity 를 넣어준다.**
synchronize: true,
}),
UserModule,
TestModule,
ExceptModule,
],
controllers: [AppController],
providers: [AppService],
})
... 생략 ...

AppModule 에 등록 후 실제 사용하게 될 단위 모듈 위치인 UserModule 에서 아래와 같이 imports 를 통해 해당 Entity 객체를 세팅 해준다.

src/user/user.module.ts

import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './domain/User';
@Module({
**imports: [TypeOrmModule.forFeature([User])],**
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}

이제 모든 설정이 완료 되었고 Java의 JPA 처럼 Repository 디자인 패턴으로 CRUD 를 해보도록 하겠다. 우선 기존에 만들었던 UserService 의 코드를 변경해보도록 하겠다.

src/user/user.service.ts ( 기존 코드 )

import { Injectable } from '@nestjs/common';
import { UserDto } from './dto/user.dto';
@Injectable()
export class UserService {
private users: UserDto[] = [
new UserDto('lee1', '이정주'),
new UserDto('kim1', '김명일'),
];
findAll() : Promise<UserDto[]> {
return new Promise((resolve) =>
setTimeout(
() => resolve(this.users),
100,
),
);
}
findOne(id: string) : UserDto | object {
const foundOne = this.users.filter(user => user.userId === id);
return foundOne.length ? foundOne[0] : { msg: 'nothing' };
}
saveUser(userDto: UserDto) : void {
this.users = [...this.users, userDto];
}
}

src/user/user.service.ts ( User Repository 디자인패턴 적용 코드 )

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './domain/User';
import { Repository } from 'typeorm/index';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User) private userRepository: Repository<User>,
) {
this.userRepository = userRepository;
}
/**
* User 리스트 조회
*/
findAll(): Promise<User[]> {
return this.userRepository.find();
}
/**
* 특정 유저 조회
* @param id
*/
findOne(id: string): Promise<User> {
return this.userRepository.findOne({ userId: id });
}
/**
* 유저 저장
* @param user
*/
async saveUser(user: User): Promise<void> {
await this.userRepository.save(user);
}
/**
* 유저 삭제
*/
async deleteUser(id: string): Promise<void> {
await this.userRepository.delete({ userId: id });
}
}

InjectRepository 데코레이터를 통해 User Entity 를 userRepository 를 UserService 에 주입하여 사용하는 모습이다. 이것이 가능했던 준비의 과정들은 이전에 Entity를 만들고 AppModule 에 TypeOrm 다이내믹 모듈을 통해 커넥션 정보와 Entities 에 User 를 등록하고 사용하는 UserModule 에서 User Entity를 TypeOrm 다이내믹 모듈의 forFeature 를 통해 등록하였다. 이 과정이 Setup이 되어야만 위의 Repository 로 의존성 주입이 가능하고 해당 Repository를 통해 CRUD 를 할 수 있다.

아마 위의 Service 코드가 바뀌면서 Controller도 변경을 할 수 밖에 없는 상황이 왔다. 아무래도 UserDTO 라는 객체에서 Entity 객체로 바뀌면서 Return 타입이 변경 되었고 Promise 를 반환하고 있기 때문에 해당 코드를 변경할 수 밖에 없는 상황이다.

src/user/user.controller.ts

import { Body, Controller, Delete, Get, Param, Post, Res } from '@nestjs/common';
import { UserDto } from './dto/user.dto';
import { UserService } from './user.service';
import { TestService } from '../test/test.service';
import { User } from './domain/User';
@Controller('user')
export class UserController {
constructor(
private userService: UserService,
private testService: TestService
) {
this.userService = userService;
this.testService = testService;
}
@Get('test')
findAnotherTest(): string {
return this.testService.getInfo();
}
@Get('list')
async findAll(): Promise<User[]> {
const userList = await this.userService.findAll();
return Object.assign({
data: userList,
statusCode: 200,
statusMsg: `데이터 조회가 성공적으로 완료되었습니다.`,
});
}
@Get(':userId')
async findOne(@Param('userId') id: string): Promise<User> {
const foundUser = await this.userService.findOne(id);
return Object.assign({
data: foundUser,
statusCode: 200,
statusMsg: `데이터 조회가 성공적으로 완료되었습니다.`,
});
}
@Post()
async saveUser(@Body() user: User): Promise<string> {
await this.userService.saveUser(user);
return Object.assign({
data: { ...user },
statusCode: 201,
statusMsg: `saved successfully`,
});
}
@Delete(':userId')
async deleteUser(@Param('userId') id: string): Promise<string> {
await this.userService.deleteUser(id);
return Object.assign({
data: { userId: id },
statusCode: 201,
statusMsg: `deleted successfully`,
});
}
}

아직 Response 하는 객체를 공통화 시키지 않고 Object.assign 을 통해 JSON String 으로 반환하고 있는데 일단 테스트니 향후에 본격적인 개발을 할 때 바꾸도록 한다. 우선 간단한 CRUD 를 통해 데이터를 조회/수정/삽입/삭제 를 수행한다.

리스트 조회 ( UserService 객체 )

/**
* User 리스트 조회
*/
findAll(): Promise<User[]> {
return this.userRepository.find();
}

위의 코드를 보면 Native Query는 존재하지 않는다 이미 추상화된 TypeORM의 find() 함수를 통해 Vendor 가 MySQL, Postgres, MariaDB, Mongoose 등 어떠한 것을 사용하던지 SELECT 를 할 수 있다. 물론 현재는 where 조건이나 Entity 의 One To One, One To Many 등의 관계 설정이 없는 심플한 상태이지만 복잡한 쿼리나 이런 것들이 가능하다. 리스트 조회를 해보면 아래와 같이 출력된다.

/user/list GET

ℹ️ TypeOrm 다이내믹 모듈 설정에서 Sync 가 true 로 설정 되어있어서 User Entity의 컬럼이 변경되면 해당 테이블의 구조도 변경되니 운영 시 적절한 설정 값을 통해 운영이 되도록 해야 한다. DEV와 PROD 그리고 LOCAL 에 따라 DB 접속정보를 env 로 다르게 가져갈 수 있도록 향후 개발 시 설정하면 될 것이다.

User 테이블의 컬럼 정보 — Primary Key : userId

/user/{userId} GET

위의 Path Variable 로 esak248 아이디를 아래 userRepository.findOne 함수로 조회한다. 결과는 위의 캡처 이미지에서 보는 바와 같이 User Entity 타입으로 data 프로퍼티에 Binding 되어 출력된다.

/**
* 특정 유저 조회
* @param id
*/
findOne(id: string): Promise<User> {
return this.userRepository.findOne({ userId: id });
}

/user/{userId} DELETE

반대로 esak248 의 ID 로 삭제를 하게 되면 삭제가 된다.

/user POST

데이터 삽입과 수정의 Action 을 나누지 않았다. 간편하게 POST를 통해 테스트 삼아 User Type의 객체를 Body 에 넣어 POST 로 요청하면 User 테이블에 userId 가 존재한다면 Update를 수행하고 없다면 신규로 데이터를 삽입하게 된다.

결론

간단한 CRUD를 TypeORM 을 NestJS 에 구성하여 Repository 디자인 패턴으로 진행해보았다. 다음 세션에서는 TypeORM 에서 Entity 간의 Relation 을 조금 더 살펴 보도록 하겠다.

다음. [NestJS] TypeORM — Relation, Transactions

--

--