Service가 직접 SQL을 작성하거나 DB 연결을 관리하면 비즈니스 로직과 데이터 접근 코드가 뒤엉킨다. Repository 패턴은 이 두 관심사를 명확히 분리한다. NestJS에서는 TypeORM의 Repository를 주입받아 Entity 단위로 DB 작업을 캡슐화한다.
Client Request
│
▼
[ Controller ] HTTP 요청 수신, DTO 변환
│
▼
[ Service ] 비즈니스 로직 처리
│
▼
[ Repository ] DB 쿼리 실행 (TypeORM)
│
▼
[ Database ] PostgreSQL / MySQL / SQLite 등
Controller는 요청을 받고, Service는 규칙을 적용하며, Repository는 실제 데이터를 읽고 쓴다. 각 계층은 바로 아래 계층하고만 대화한다.
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 컨테이너에서 꺼내 주입하도록 지시하는 데코레이터.
| 메서드 | 설명 |
|---|---|
find(options?) | 조건에 맞는 레코드 배열 반환 |
findOneBy(where) | 단일 레코드 반환, 없으면 null |
create(data) | Entity 인스턴스 생성 (DB 저장 안 함) |
save(entity) | INSERT 또는 UPDATE 실행 |
delete(id) | 기본 키로 레코드 삭제 |
count(options?) | 조건에 맞는 레코드 수 반환 |
복잡한 쿼리는 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 {}
Service에 DataSource를 직접 주입해 쿼리를 작성할 수도 있지만, Repository를 쓰면 두 가지 이점이 생긴다. 첫째, 테스트 시 Repository를 Mock으로 교체하기 쉬워 Service 단위 테스트가 DB 없이 가능해진다. 둘째, 동일한 쿼리 로직이 여러 Service에서 필요할 때 Repository 메서드를 재사용할 수 있다.