NestJS에서 Service는 애플리케이션의 핵심 비즈니스 로직을 담당하는 클래스다. Controller가 HTTP 요청을 받아 응답을 돌려주는 역할만 한다면, 실제 데이터를 처리하고 규칙을 적용하는 일은 Service가 맡는다. 이 분리 덕분에 코드는 단순해지고, 테스트는 훨씬 쉬워진다.
NestJS 애플리케이션은 세 계층으로 나뉜다.
HTTP Request
│
▼
┌─────────────┐
│ Controller │ ← 요청/응답 처리, 라우팅
└──────┬──────┘
│ 호출
▼
┌─────────────┐
│ Service │ ← 비즈니스 로직
└──────┬──────┘
│ 호출
▼
┌─────────────┐
│ Repository │ ← DB 접근
└─────────────┘
Controller는 Service를 호출하고, Service는 Repository를 통해 데이터를 읽거나 쓴다. 각 계층은 자신의 책임만 진다.
Service 클래스에는 반드시 @Injectable 데코레이터를 붙인다. 이 데코레이터는 NestJS의 DI(Dependency Injection) 컨테이너에 해당 클래스를 등록한다.
@Injectable — NestJS IoC 컨테이너가 이 클래스의 인스턴스를 생성하고 주입할 수 있도록 메타데이터를 추가하는 데코레이터.
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
findAll() {
return this.users;
}
findOne(id: number) {
return this.users.find((u) => u.id === id);
}
}
Service는 Controller 생성자에서 주입받는다. NestJS가 인스턴스를 자동으로 생성해 넘겨주므로 new UsersService를 직접 호출할 필요가 없다.
import { Controller, Get, Param } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(Number(id));
}
}
Service를 Module의 providers 배열에 등록해야 DI가 동작한다.
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
Service가 비즈니스 로직을 독립적으로 가지면 두 가지 이점이 생긴다.
| 이점 | 설명 |
|---|---|
| 단일 책임 | Controller는 HTTP만, Service는 로직만 담당 |
| 테스트 용이성 | HTTP 없이 Service만 단독으로 유닛 테스트 가능 |
단일 책임 원칙(SRP) — 하나의 클래스는 하나의 이유로만 변경되어야 한다는 객체지향 설계 원칙.
아래처럼 Service만 격리해 테스트할 수 있다.
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(() => {
service = new UsersService();
});
it('전체 사용자를 반환한다', () => {
expect(service.findAll()).toHaveLength(2);
});
});
Controller나 HTTP 레이어 없이 순수하게 로직만 검증할 수 있어 테스트가 빠르고 명확하다. Repository 패턴을 추가하면 DB 의존성도 Mock으로 교체할 수 있어 테스트 격리가 더욱 완전해진다.