서비스(Service)란? 서비스는 비즈니스 로직을 담당하는 계층입니다. @Injectable() 데코레이터로 정의되며, 컨트롤러에서 분리하여 재사용 가능하고 테스트하기 쉬운 코드를 작성할 수 있습니다.
서비스의 역할
비즈니스 로직 처리
데이터베이스 접근
외부 API 호출
데이터 변환 및 가공
재사용 가능한 기능 제공
기본 서비스 생성 CLI로 서비스 생성 nest g service users nest g service modules/users nest g service users --no-spec
기본 서비스 구조 import { Injectable } from '@nestjs/common' ;@Injectable ()export class UsersService { private users = []; findAll ( ) { return this .users ; } findOne (id: number ) { return this .users .find (user => user.id === id); } create (user: any ) { this .users .push (user); return user; } update (id: number , user: any ) { const index = this .users .findIndex (u => u.id === id); if (index !== -1 ) { this .users [index] = { ...this .users [index], ...user }; return this .users [index]; } return null ; } remove (id: number ) { const index = this .users .findIndex (u => u.id === id); if (index !== -1 ) { return this .users .splice (index, 1 )[0 ]; } return null ; } }
의존성 주입(Dependency Injection) 의존성 주입은 객체가 필요로 하는 의존성을 외부에서 제공받는 디자인 패턴입니다.
생성자 기반 주입 가장 일반적인 방법입니다.
import { Controller , Get } from '@nestjs/common' ;import { UsersService } from './users.service' ;@Controller ('users' )export class UsersController { constructor (private readonly usersService: UsersService ) {} @Get () findAll ( ) { return this .usersService .findAll (); } }
프로퍼티 기반 주입 특수한 경우에만 사용합니다.
import { Controller , Get } from '@nestjs/common' ;import { Inject } from '@nestjs/common' ;import { UsersService } from './users.service' ;@Controller ('users' )export class UsersController { @Inject (UsersService ) private readonly usersService : UsersService ; @Get () findAll ( ) { return this .usersService .findAll (); } }
Provider 등록 서비스를 사용하려면 모듈의 providers 배열에 등록해야 합니다.
users.module.ts import { Module } from '@nestjs/common' ;import { UsersController } from './users.controller' ;import { UsersService } from './users.service' ;@Module ({ controllers : [UsersController ], providers : [UsersService ], exports : [UsersService ], }) export class UsersModule {}
Provider 등록 방식 1. 표준 방식 (권장) @Module ({ providers : [UsersService ], })
2. 클래스 Provider (상세 버전) @Module ({ providers : [ { provide : UsersService , useClass : UsersService , }, ], })
3. 값 Provider const mockUsersService = { findAll : () => [], }; @Module ({ providers : [ { provide : UsersService , useValue : mockUsersService, }, ], })
4. Factory Provider @Module ({ providers : [ { provide : UsersService , useFactory : (config: ConfigService ) => { return new UsersService (config.get ('database' )); }, inject : [ConfigService ], }, ], })
5. 비동기 Factory Provider @Module ({ providers : [ { provide : 'ASYNC_CONNECTION' , useFactory : async () => { const connection = await createConnection (); return connection; }, }, ], })
커스텀 Provider 문자열 토큰 사용 @Module ({ providers : [ { provide : 'USER_REPOSITORY' , useClass : UserRepository , }, ], }) export class UsersService { constructor ( @Inject ('USER_REPOSITORY' ) private readonly userRepository: UserRepository, ) {}}
인터페이스를 위한 토큰 export interface IUserRepository { findAll (): Promise <User []>; findOne (id : number ): Promise <User >; } export const USER_REPOSITORY = 'USER_REPOSITORY' ;@Injectable ()export class UserRepository implements IUserRepository { async findAll (): Promise <User []> { return []; } async findOne (id : number ): Promise <User > { return null ; } } @Module ({ providers : [ { provide : USER_REPOSITORY , useClass : UserRepository , }, ], }) export class UsersService { constructor ( @Inject (USER_REPOSITORY) private readonly userRepository: IUserRepository, ) {}}
Scope (스코프) Provider의 생명주기를 제어합니다.
DEFAULT (싱글톤) 애플리케이션 전체에서 하나의 인스턴스만 생성됩니다. (기본값)
@Injectable ()export class UsersService { }
REQUEST 각 요청마다 새로운 인스턴스를 생성합니다.
import { Injectable , Scope } from '@nestjs/common' ;@Injectable ({ scope : Scope .REQUEST })export class UsersService { constructor ( ) { console .log ('New instance created for this request' ); } }
TRANSIENT 주입될 때마다 새로운 인스턴스를 생성합니다.
import { Injectable , Scope } from '@nestjs/common' ;@Injectable ({ scope : Scope .TRANSIENT })export class LoggerService { }
실전 예제: 완전한 CRUD 서비스 dto/create-user.dto.ts export class CreateUserDto { name : string ; email : string ; age : number ; }
dto/update-user.dto.ts export class UpdateUserDto { name?: string ; email?: string ; age?: number ; }
entities/user.entity.ts export class User { id : number ; name : string ; email : string ; age : number ; createdAt : Date ; updatedAt : Date ; }
users.service.ts import { Injectable , NotFoundException } from '@nestjs/common' ;import { CreateUserDto } from './dto/create-user.dto' ;import { UpdateUserDto } from './dto/update-user.dto' ;import { User } from './entities/user.entity' ;@Injectable ()export class UsersService { private users : User [] = []; private idCounter = 1 ; findAll (): User [] { return this .users ; } findOne (id : number ): User { const user = this .users .find (u => u.id === id); if (!user) { throw new NotFoundException (`User with ID ${id} not found` ); } return user; } create (createUserDto : CreateUserDto ): User { const newUser : User = { id : this .idCounter ++, ...createUserDto, createdAt : new Date (), updatedAt : new Date (), }; this .users .push (newUser); return newUser; } update (id : number , updateUserDto : UpdateUserDto ): User { const user = this .findOne (id); const updatedUser = { ...user, ...updateUserDto, updatedAt : new Date (), }; const index = this .users .findIndex (u => u.id === id); this .users [index] = updatedUser; return updatedUser; } remove (id : number ): User { const user = this .findOne (id); this .users = this .users .filter (u => u.id !== id); return user; } findByEmail (email : string ): User | undefined { return this .users .find (u => u.email === email); } search (keyword : string ): User [] { return this .users .filter ( u => u.name .includes (keyword) || u.email .includes (keyword), ); } }
users.controller.ts import { Controller , Get , Post , Put , Delete , Body , Param , Query , HttpCode , HttpStatus , } from '@nestjs/common' ; import { UsersService } from './users.service' ;import { CreateUserDto } from './dto/create-user.dto' ;import { UpdateUserDto } from './dto/update-user.dto' ;@Controller ('users' )export class UsersController { constructor (private readonly usersService: UsersService ) {} @Get () findAll ( ) { return this .usersService .findAll (); } @Get ('search' ) search (@Query ('keyword' ) keyword: string ) { return this .usersService .search (keyword); } @Get (':id' ) findOne (@Param ('id' ) id: string ) { return this .usersService .findOne (+id); } @Post () @HttpCode (HttpStatus .CREATED ) create (@Body () createUserDto: CreateUserDto ) { return this .usersService .create (createUserDto); } @Put (':id' ) update (@Param ('id' ) id: string , @Body () updateUserDto: UpdateUserDto ) { return this .usersService .update (+id, updateUserDto); } @Delete (':id' ) @HttpCode (HttpStatus .NO_CONTENT ) remove (@Param ('id' ) id: string ) { return this .usersService .remove (+id); } }
서비스 간 의존성 서비스는 다른 서비스를 주입받을 수 있습니다.
email.service.ts import { Injectable } from '@nestjs/common' ;@Injectable ()export class EmailService { sendWelcomeEmail (email: string , name: string ) { console .log (`Sending welcome email to ${email} ` ); return { sent : true }; } sendPasswordResetEmail (email: string ) { console .log (`Sending password reset email to ${email} ` ); return { sent : true }; } }
users.service.ts (EmailService 사용) import { Injectable } from '@nestjs/common' ;import { EmailService } from '../email/email.service' ;import { CreateUserDto } from './dto/create-user.dto' ;@Injectable ()export class UsersService { constructor (private readonly emailService: EmailService ) {} async create (createUserDto: CreateUserDto ) { const newUser = { id : Date .now (), ...createUserDto, }; await this .emailService .sendWelcomeEmail ( newUser.email , newUser.name , ); return newUser; } }
users.module.ts import { Module } from '@nestjs/common' ;import { UsersController } from './users.controller' ;import { UsersService } from './users.service' ;import { EmailModule } from '../email/email.module' ;@Module ({ imports : [EmailModule ], controllers : [UsersController ], providers : [UsersService ], exports : [UsersService ], }) export class UsersModule {}
Optional Dependencies 선택적 의존성을 처리합니다.
import { Injectable , Optional , Inject } from '@nestjs/common' ;@Injectable ()export class UsersService { constructor ( @Optional () @Inject ('LOGGER' ) private readonly logger?: LoggerService, ) {} findAll ( ) { this .logger ?.log ('Finding all users' ); return []; } }
순환 의존성 해결 forwardRef() 사용 import { Injectable , Inject , forwardRef } from '@nestjs/common' ;import { PostsService } from '../posts/posts.service' ;@Injectable ()export class UsersService { constructor ( @Inject (forwardRef(() => PostsService)) private readonly postsService: PostsService, ) {}} import { Injectable , Inject , forwardRef } from '@nestjs/common' ;import { UsersService } from '../users/users.service' ;@Injectable ()export class PostsService { constructor ( @Inject (forwardRef(() => UsersService)) private readonly usersService: UsersService, ) {}}
테스트를 위한 Mock Provider users.service.spec.ts import { Test , TestingModule } from '@nestjs/testing' ;import { UsersService } from './users.service' ;import { EmailService } from '../email/email.service' ;describe ('UsersService' , () => { let service : UsersService ; let emailService : EmailService ; const mockEmailService = { sendWelcomeEmail : jest.fn (), }; beforeEach (async () => { const module : TestingModule = await Test .createTestingModule ({ providers : [ UsersService , { provide : EmailService , useValue : mockEmailService, }, ], }).compile (); service = module .get <UsersService >(UsersService ); emailService = module .get <EmailService >(EmailService ); }); it ('should be defined' , () => { expect (service).toBeDefined (); }); it ('should send welcome email on user creation' , async () => { const createUserDto = { name : 'John' , email : 'john@example.com' , age : 25 , }; await service.create (createUserDto); expect (mockEmailService.sendWelcomeEmail ).toHaveBeenCalledWith ( 'john@example.com' , 'John' , ); }); });
Repository 패턴 데이터 액세스 로직을 분리합니다.
user.repository.ts import { Injectable } from '@nestjs/common' ;import { User } from './entities/user.entity' ;@Injectable ()export class UserRepository { private users : User [] = []; async findAll (): Promise <User []> { return this .users ; } async findById (id : number ): Promise <User | undefined > { return this .users .find (u => u.id === id); } async create (user : Partial <User >): Promise <User > { const newUser = { id : this .users .length + 1 , ...user, } as User ; this .users .push (newUser); return newUser; } async update (id : number , user : Partial <User >): Promise <User | null > { const index = this .users .findIndex (u => u.id === id); if (index === -1 ) return null ; this .users [index] = { ...this .users [index], ...user }; return this .users [index]; } async delete (id : number ): Promise <boolean > { const index = this .users .findIndex (u => u.id === id); if (index === -1 ) return false ; this .users .splice (index, 1 ); return true ; } }
users.service.ts (Repository 사용) import { Injectable , NotFoundException } from '@nestjs/common' ;import { UserRepository } from './user.repository' ;import { CreateUserDto } from './dto/create-user.dto' ;import { UpdateUserDto } from './dto/update-user.dto' ;@Injectable ()export class UsersService { constructor (private readonly userRepository: UserRepository ) {} async findAll ( ) { return this .userRepository .findAll (); } async findOne (id: number ) { const user = await this .userRepository .findById (id); if (!user) { throw new NotFoundException (`User with ID ${id} not found` ); } return user; } async create (createUserDto: CreateUserDto ) { return this .userRepository .create (createUserDto); } async update (id: number , updateUserDto: UpdateUserDto ) { const user = await this .userRepository .update (id, updateUserDto); if (!user) { throw new NotFoundException (`User with ID ${id} not found` ); } return user; } async remove (id: number ) { const deleted = await this .userRepository .delete (id); if (!deleted) { throw new NotFoundException (`User with ID ${id} not found` ); } return { deleted : true }; } }
정리
서비스는 @Injectable() 데코레이터로 정의
비즈니스 로직을 컨트롤러에서 분리
의존성 주입으로 느슨한 결합 유지
Provider 등록 방식: 표준, 클래스, 값, Factory
Scope로 생명주기 제어 (DEFAULT, REQUEST, TRANSIENT)
Repository 패턴으로 데이터 액세스 로직 분리
Mock Provider로 테스트 용이성 확보
다음 단계 서비스와 의존성 주입을 이해했다면:
데이터베이스 연결 및 TypeORM 사용하기
파이프로 데이터 검증하기
미들웨어, 가드, 인터셉터 활용하기
예외 필터로 에러 처리하기