개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

NestJS Repository — 데이터베이스와 직접 통신하는 계층

2025년 3월 20일
NestJSRepositoryTypeORMData AccessService

Service가 직접 SQL을 작성하거나 DB 연결을 관리하면 비즈니스 로직과 데이터 접근 코드가 뒤엉킨다. Repository 패턴은 이 두 관심사를 명확히 분리한다. NestJS에서는 TypeORM의 Repository를 주입받아 Entity 단위로 DB 작업을 캡슐화한다.

Service → Repository → DB 흐름

Client Request
     │
     ▼
[ Controller ]   HTTP 요청 수신, DTO 변환
     │
     ▼
[   Service  ]   비즈니스 로직 처리
     │
     ▼
[ Repository ]   DB 쿼리 실행 (TypeORM)
     │
     ▼
[  Database  ]   PostgreSQL / MySQL / SQLite 등

Controller는 요청을 받고, Service는 규칙을 적용하며, Repository는 실제 데이터를 읽고 쓴다. 각 계층은 바로 아래 계층하고만 대화한다.

@InjectRepository로 주입받기

TypeORM의 Repository<T>를 사용하려면 모듈에 Entity를 등록하고 Service에 주입해야 한다.

// user.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UserService],
  controllers: [UserController],
})
export class UserModule {}
// user.service.ts
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.userRepository.find();
  }

  findOne(id: number): Promise<User | null> {
    return this.userRepository.findOneBy({ id });
  }

  async create(data: Partial<User>): Promise<User> {
    const user = this.userRepository.create(data);
    return this.userRepository.save(user);
  }

  async remove(id: number): Promise<void> {
    await this.userRepository.delete(id);
  }
}

@InjectRepository(Entity) — TypeORM이 해당 Entity에 대한 Repository 인스턴스를 DI 컨테이너에서 꺼내 주입하도록 지시하는 데코레이터.

자주 쓰는 Repository 메서드

메서드설명
find(options?)조건에 맞는 레코드 배열 반환
findOneBy(where)단일 레코드 반환, 없으면 null
create(data)Entity 인스턴스 생성 (DB 저장 안 함)
save(entity)INSERT 또는 UPDATE 실행
delete(id)기본 키로 레코드 삭제
count(options?)조건에 맞는 레코드 수 반환

Custom Repository

복잡한 쿼리는 Custom Repository 클래스로 분리하면 Service가 깔끔해진다.

// user.repository.ts
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { User } from './user.entity';

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

  findActiveUsers(): Promise<User[]> {
    return this.createQueryBuilder('user')
      .where('user.isActive = :isActive', { isActive: true })
      .orderBy('user.createdAt', 'DESC')
      .getMany();
  }
}

QueryBuilder — SQL을 직접 문자열로 작성하지 않고 메서드 체이닝으로 쿼리를 조립하는 TypeORM의 빌더 패턴.

Custom Repository는 providers 배열에 직접 등록하고 @InjectRepository 대신 일반 DI로 주입한다.

// user.module.ts
@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UserService, UserRepository],
})
export class UserModule {}

Repository가 필요한 이유

Service에 DataSource를 직접 주입해 쿼리를 작성할 수도 있지만, Repository를 쓰면 두 가지 이점이 생긴다. 첫째, 테스트 시 Repository를 Mock으로 교체하기 쉬워 Service 단위 테스트가 DB 없이 가능해진다. 둘째, 동일한 쿼리 로직이 여러 Service에서 필요할 때 Repository 메서드를 재사용할 수 있다.

← 이전 글NestJS Lifecycle — 요청 처리 전체 흐름
다음 글 →NestJS Configuration — 환경 변수와 설정을 관리하는 방법