TypeScript에서 인터페이스는 객체가 특정 형태를 갖추고 있어야 함을 명시하는 계약입니다. 인터페이스는 특정 타입에 대한 요구사항을 명시하며, 해당 요구사항을 충족하는 객체는 인터페이스를 구현(implement)한다고 표현합니다.
인터페이스는 컴파일 시간에만 존재하고, JavaScript로 컴파일된 후에는 제거되어 런타임 시점에서는 존재하지 않습니다. 이 특징 때문에 인터페이스는 타입 체크를 위해 사용되며, 실행 시간에는 어떠한 오버헤드도 추가하지 않습니다.
DI(Dependency Injection, 의존성 주입)는 소프트웨어 설계 패턴 중 하나로, 컴포넌트 간의 의존성을 외부에서 주입하는 방식을 말합니다. 이 패턴을 사용하면 컴포넌트는 자신이 의존하는 다른 컴포넌트를 직접 생성하거나 찾는 대신, 외부(보통 프레임워크나 컨테이너)에서 제공받게 됩니다. DI는 코드의 결합도를 낮추고, 유연성과 재사용성을 향상시키며, 단위 테스트를 용이하게 합니다.
TypeScript에서 인터페이스는 컴파일 후에는 JavaScript 코드에 존재하지 않기 때문에, NestJS의 DI 컨테이너가 런타임 시점에서 인터페이스를 참조할 수 없어 의존성 에러가 발생합니다. 이를 우회하기 위한 인터페이스를 기반으로 DI를 구현하기 위한 방법을 소개합니다.
DI에 사용할 인터페이스에 고유한 식별자로 문자를 작성합니다. 토큰을 사용하는 이유는 고유성을 보장하기 위해서입니다.
// di.tokens.ts
export const IAUTH_SERVICE = 'IAuthService'
export const IBUDGET_SERVICE = ('IBudgetService'
export const IEXPENSE_SERVICE = 'IExpenseService'
export const IUSER_SERVICE = 'IUserService'
인터페이스 정의 시 메서드 명명 규칙을 일관되게 작성해야하고 다른사람이 이해하기 쉽도록 명확해야합니다.
// budget.service.interface.ts
export interface IBudgetService {
createBudget(createBudgetDto: CreateBudgetDto, user: User): Promise<Budget>
updateBudget(
id: number,
updateBudgetDto: UpdateBudgetDto,
user: User,
): Promise<Budget>
deleteBudget(id: number): Promise<void>
}
// budget.service.ts
@Injectable()
export class BudgetService implements IBudgetService {
constructor(
@InjectRepository(Budget)
private budgetRepository: Repository<Budget>,
) {}
async createBudget(
createBudgetDto: CreateBudgetDto,
user: User,
): Promise<Budget> {
// create 로직
}
async updateBudget(
id: number,
updateBudgetDto: UpdateBudgetDto,
user: User,
): Promise<Budget> {
// update 로직
}
async deleteBudget(id: number): Promise<void> {
// dlete 로직
}
모듈의 providers 배열에 useClass와 함께 심볼을 키로 사용하는 커스텀 프로바이더를 추가합니다.
커스텀 프로바이더를 사용하는 이유는 확장성을 높일 수 있고 테스트하기 용이하기 때문입니다.
// budget.module.ts
@Module({
providers: [
{
provide: IBUDGET_SERVICE,
useClass: BudgetService,
},
],
})
export class BudgetModule {}
일반적으로 프로바이더를 주입 할때, 생성자 주입 패턴을 사용하여 프로바이더를 주입합니다.
하지만 이 패턴을 사용하려면 클래스를 의존하고 있어야 하며, 인터페이스를 의존하고 있으면 런타임 시점에 인터페이스가 존재하지 않기 때문에 생성자 주입 패턴을 사용할 수 없습니다.
이럴 경우 위와 같이 토큰방식으로 프로바이더를 등록 후 의존하고 있는 인터페이스에생성자를 통해 @Inject() 데코레이터를 달아주면 useClass에 명시한 구현체가 주입됩니다.
// budget.controller.ts
@Controller('budgets')
export class BudgetController {
constructor(@Inject(IBUDGET_SERVICE) private budgetService: IBudgetService) {}
async create(
@Body() createBudgetDto: CreateBudgetDto,
@GetUser() user: User,
): Promise<Budget> {
return this.budgetService.createBudget(createBudgetDto, user)
}
async update(
@Param('id') id: string,
@Body() updateBudgetDto: UpdateBudgetDto,
@GetUser() userId: User,
): Promise<Budget> {
return this.budgetService.updateBudget(+id, updateBudgetDto, userId)
}
async delete(@Param('id') id: string): Promise<void> {
await this.budgetService.deleteBudget(+id)
}