nestJS도 백엔드니까 어쨌든 데이터베이스와의 커넥션이 필요하다.
뭐 일일히 sql문을 작성해서 DBMS와 소통하는 방식도 있겠지만, 그것은 우리에게 바람직한 방식은 아니다. 불편하니까
그래서 스프링에서는 JPA가 나왔고, nest에서는 TypeORM이 나온 것 같다.
라이브러리 설치
npm install @nestjs/typeorm
npm install typeorm
npm install sqlite3
DBMS를 무엇을 쓸지에 따라 sqlite가 아니라 mysql이 될수도 있고 mongodb가 될수도 있다. mongodb는 mongoose라는 라이브러리도 사용 가능하니 참고
설정
typeORM을 사용하기 위한 설정을 먼저 해보자. 최상위 모듈, 예를 들어 app.module에서 작성하자
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'db.sqlite',
entities: [User, Report],
synchronize: true,
}),
UsersModule,
ReportsModule,
],
...
})
imports 문에 TypeOrmModule.forRoot를 추가한다.
- type : 어떤 DBMS를 사용할 것인가?
- databse : 사용할 데이터베이스의 이름
- entities : 사용할 엔티티들
- synchronize : true로 사용하면 코드 내에서의 DB 구조 변경을 감지해 자동으로 반영, 개발 환경에서만 사용 추천
Entity
엔티티는 데이터베이스 내에서의 테이블 하나를 의미한다. 테이블이 뭐냐고 묻는다면... 당신은 DB를 먼저 공부하고 와야한다.
@Entity()
export class User{
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
@Column()
password: string;
}
데코레이터들을 먼저 살펴보자
- Entity : 이 클래스가 엔티티 클래스라는 것을 명시한다. 이 데코레이터를 달아놓으면 typeOrm이 synchronize 플래그를 통해 변경을 감지하고, DB에 반영한다
- Column : DB의 컬럼
- PrimaryGeneratedColumn : 이 테이블의 primary key라고 명시하고, 생성될때마다 고유한 값을 부여한다.
hooks
이 엔티티가 생성되거나 업데이트되거나, 제거되었을 때의 행동이 필요할 때도 있다. 이때 hooks를 사용해서 특정 상황에 필요한 행동을 취할 수 있다.
| @BeforeInsert() | 엔티티가 DB에 삽입되기 직전 |
| @AfterInsert() | 엔티티가 DB에 삽입된 직후 |
| @BeforeUpdate() | 엔티티가 DB에서 업데이트되기 직전 |
| @AfterUpdate() | 엔티티가 DB에서 업데이트된 직후 |
| @BeforeRemove() | 엔티티가 삭제되기 직전 |
| @AfterRemove() | 엔티티가 삭제된 직후 |
| @AfterLoad() | DB에서 로딩된 후 (예: find() 호출 후) |
예시를 보자
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
@Column()
password: string;
@AfterInsert()
logInsert() {
console.log(`Inserted User id : ${this.id}`)
}
@AfterUpdate()
logUpdate() {
console.log("Update User id : " + this.id)
}
@AfterRemove()
logRemove() {
console.log("Removed User with id =" + this.id)
}
}
이렇게 만들어져 있는 엔티티를 생성했을 때

이렇게 로그를 통해 만들어진 후의 id를 반환하는 것을 볼 수 있다.
Repository
리포지토리는 직접 데이터베이스와 소통하며 필요한 데이터들을 가져오는 역할을 한다. typeORM에서 우리는 리포지토리를 직접 만들 팔요가 없으며 그저 의존성 주입을 통해 가져오기만 하면 된다.
간단한 sql 쿼리문들은 미리 만들어진 메소드를 통해 사용이 가능하다
주의 !! 엔티티를 생성해서 호출하는 메소드(save, remove)가 아닌 직접 필드를 입력해서 호출하는 메소드 (insert, update, delete)에서는 hooks가 동작하지 않음 !!
조회
| find() | 모든 엔티티 조회 (옵션 가능) |
| findOne(options) | 조건에 맞는 하나의 엔티티 조회 |
| findBy(conditions) | 조건에 맞는 다수의 엔티티 조회 (짧은 문법) |
| findOneBy(conditions) | 조건에 맞는 하나의 엔티티 조회 (짧은 문법) |
| findOneOrFail() | 못 찾으면 예외 발생 |
| findByIds() | 여러 ID를 기준으로 조회 (TypeORM 0.3 이후 제거됨) |
| count() | 조건에 맞는 레코드 수 계산 |
| exist() | 조건에 해당하는 레코드 존재 여부 확인 |
저장, 추가
| save(entity) | 새로운 엔티티를 저장하거나 업데이트 |
| insert(entity) | 새 엔티티 삽입 (기존 존재하면 에러) |
| update(conditions, partialEntity) | 조건에 맞는 엔티티를 수정 (엔티티 불러오지 않음) |
| upsert() | 존재하면 업데이트, 없으면 삽입 (TypeORM 0.3 이상) |
삭제
| remove(entity) | 엔티티 인스턴스를 이용해 삭제 |
| delete(conditions) | 조건에 맞는 엔티티를 바로 삭제 |
| softRemove(entity) | soft delete 수행 (@DeleteDateColumn 필요) |
| softDelete(conditions) | soft delete 수행 |
| restore(conditions) | soft deleted 된 엔티티 복구 |
유틸
| create() | 새 엔티티 인스턴스 생성 (DB 저장은 안 됨) |
| preload() | 존재하는 엔티티를 불러온 뒤 병합 |
| merge(target, ...sources) | 여러 엔티티 속성을 병합 |
복잡한 sql쿼리문
queryBuilder를 사용하여 sql문을 짤 수도 있다
const users = await dataSource
.getRepository(User)
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.where('user.age > :age', { age: 18 })
.andWhere('post.isPublished = :isPublished', { isPublished: true })
.orderBy('user.name', 'ASC')
.skip(0) // 페이징
.take(10)
.getMany();
위의 복잡한 쿼리문을 재사용하고 싶다면 커스텀 리포지토리를 만들어서 사용할 수도 있다
@EntityRepository(User)
export class UserRepository extends Repository<User> {
async findWithPostCount(): Promise<User[]> {
return this.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.loadRelationCountAndMap('user.postCount', 'user.posts')
.getMany();
}
}
요렇게
아무튼 복잡하지 않은 쿼리문들을 사용하려면 일단 의존성 주입을 통해 가져오기부터 해야한다.
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User) private repo : Repository<User>) {
}
}
User라는 엔티티 클래스가 있을 때, 이렇게 사용하면 User 테이블과 소통하는 리포지토리가 주입된다.
Update 시 팁
엔티티에 여러개의 속성이 있을 때, 특정 속성만 업데이트하고 싶을 때는 어떻게 해야할까? 업데이트하고 싶은 속성 외의 다른 속성들은 null로 주어야할까? 아니면 기존의 속성을 줘서 엔티티 전체를 업데이트해야할까?
스프링에서는 더티체킹을 통해 업데이트를 해줘서 딱히 고민할 게 없었던 것 같다.
nest에서는 Partial이라는 타입을 사용해서 해결한다.
async update(id: number, attrs: Partial<User>) {
const user = await this.findOne(id);
if (!user) {
throw new NotFoundException('User not found');
}
// user에 attrs에 덮어쓰기
Object.assign(user, attrs);
return this.repo.save(user);
}
이렇게 받아오면 User 엔티티의 일부만 받아서 업데이트가 가능하다.
Partial<User>은 User 엔티티의 필드가 모두 Optional 타입이라 가능한 트릭인 것 같다.
그러면 컨트롤러에서는 어떻게 업데이트할 정보만 받아올 수 있을까?
먼저 업데이트할 정보를 받을 DTO를 만들자
export class UpdateUserDto {
@IsEmail()
@IsOptional()
email:string;
@IsString()
@IsOptional()
password:string;
}
@IsOptional은 이 필드가 null이라면 검증을 건너뒤도록 만드는 데코레이터이다. 따라서 이 DTO로 인자를 받을 때 선택적으로만 업데이트가 가능한 것이다.
@Patch('/:id')
updateUser(@Param('id') id:string , @Body() body: UpdateUserDto) {
return this.usersService.update(parseInt(id), body);
}
컨트롤러에서는 이렇게 받아오면 된다 !!
'NestJS' 카테고리의 다른 글
| 반환 정보 편집하기 (1) | 2025.06.15 |
|---|---|
| 입력 정보 검증하기 (0) | 2025.06.15 |
| 제어 역전 / 의존성 주입 (0) | 2025.06.14 |
| 컨트롤러 라우팅 / 요청 인자 받기 (0) | 2025.06.14 |
| nestJS 시작 (2) | 2025.06.14 |