기술과 산업/언어 및 프레임워크

NestJS 마스터 시리즈 5화. 서비스와 의존성 주입(DI) – 로직의 핵심을 어떻게 설계할 것인가

B컷개발자 2025. 4. 28. 16:54
728x90

"비즈니스 로직은 서비스에, API는 컨트롤러에. 이 원칙이 실무의 기본이다"

NestJS 서비스(Service) 구조와 의존성 주입(DI)의 원리를 실전 예제와 함께 설명합니다. 서비스 분리 전략, 테스트 가능성, DI 컨테이너 관리까지 실무 중심으로 분석합니다.


서비스(Service)란 무엇인가

NestJS에서 서비스는 비즈니스 로직을 담당하는 계층이다.
컨트롤러는 요청과 응답만 처리하고, 실제 계산, 데이터 처리, 외부 통신 등 핵심 로직은 모두 서비스에 위치해야 한다.

NestJS는 Angular 스타일의 의존성 주입(Dependency Injection) 시스템을 내장하여,
서비스 간 결합도를 낮추고 확장성과 테스트 용이성을 극대화했다.

"컨트롤러는 얇게, 서비스는 두껍게"
이것이 좋은 백엔드 설계의 기본이다.


기본적인 서비스 구조

서비스는 @Injectable() 데코레이터를 사용하여 의존성 주입이 가능하도록 선언한다.

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  findAll() {
    return [{ id: 1, name: 'Alice' }];
  }

  findOne(id: number) {
    return { id, name: 'Bob' };
  }

  create(createUserDto) {
    return { id: Date.now(), ...createUserDto };
  }
}
  • @Injectable()은 이 클래스가 NestJS DI 컨테이너에 의해 관리될 것임을 선언한다.
  • 메서드에는 데이터 처리, 비즈니스 규칙, 외부 API 호출 등 로직만 포함한다.

컨트롤러와 서비스 연결

컨트롤러는 서비스를 주입받아 사용한다.

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }
}
  • 생성자 주입 방식을 사용하여 UsersService 인스턴스를 자동으로 주입받는다.
  • 직접 인스턴스를 생성하지 않고, NestJS가 관리하는 라이프사이클에 따라 주입된다.

의존성 주입(DI) 컨테이너란 무엇인가

DI 컨테이너는 객체의 생성과 생명주기를 관리하는 시스템이다.
NestJS는 애플리케이션이 시작될 때 @Injectable()로 등록된 클래스들을 자동으로 스캔하고, 필요한 곳에 인스턴스를 주입한다.

역할 설명

등록 서비스, 리포지토리 등 @Injectable()로 선언
생성 요청 시 인스턴스를 생성 또는 재사용
주입 생성자나 프로퍼티를 통해 자동 주입

의존성 주입을 통해 다음을 얻을 수 있다:

  • 느슨한 결합(Loose Coupling)
  • 테스트 편의성(Mock 주입 가능)
  • 코드 재사용성 향상

실무 기준 서비스 설계 전략

  1. 하나의 서비스는 하나의 책임만 가져야 한다 (Single Responsibility Principle)
  2. 데이터베이스 접근은 별도의 Repository로 분리하는 것이 좋다
  3. 외부 API 호출은 별도 Provider 또는 Gateway로 구성해 의존성을 낮춘다
  4. 비즈니스 로직과 트랜잭션 처리는 서비스 레이어에서 담당한다

실전 예제 – 다중 서비스 주입

다른 서비스를 의존하는 복잡한 서비스도 DI로 쉽게 구성할 수 있다.

@Injectable()
export class OrdersService {
  constructor(
    private readonly usersService: UsersService,
    private readonly productsService: ProductsService,
  ) {}

  createOrder(userId: number, productId: number) {
    const user = this.usersService.findOne(userId);
    const product = this.productsService.findOne(productId);
    return { user, product, orderedAt: new Date() };
  }
}
  • 서비스 간 의존성이 명확히 관리된다.
  • 테스트 시 Mock 객체를 쉽게 주입할 수 있다.

주의할 점

  • 서비스가 비대해지면 기능별로 분리해야 한다.
  • 순환 의존성(Circular Dependency)을 피하기 위해 모듈 레벨에서 의존성을 관리해야 한다.
  • 모든 의존성은 private readonly로 선언하여 외부 노출을 막는다.

마무리 인사이트

NestJS에서 서비스와 DI는 단순한 코드 규칙이 아니다.
설계 철학을 관통하는 핵심 개념이다.

  • 컨트롤러는 얇게
  • 서비스는 두껍게
  • DI를 통해 의존성을 관리

이 원칙을 지키면 서비스가 커져도 코드가 무너지지 않는다.

"비즈니스 로직이 구조를 흐트러뜨리게 놔두지 말자. 서비스가 로직을 품고, DI가 구조를 지켜낸다."


다음 회차 예고

6화. DTO와 Validation – 데이터 무결성과 API 품질의 시작
사용자 입력 검증과 데이터 전송 객체(DTO) 설계 원칙을 다룬다.

728x90