🚀 NestJS란?
NestJS는 TypeScript로 작성된 Node.js 기반 백엔드 프레임워크입니다.
Express를 기반으로 동작하며, 모듈화(Modular), 의존성 주입(Dependency Injection), 데코레이터 기반 프로그래밍을 지원합니다.
🔹 NestJS의 기본 개념
📌 1. 모듈(Module) 기반 구조
NestJS는 애플리케이션을 모듈 단위로 구성하여 코드 재사용성을 높이고 유지보수를 쉽게 만듭니다.
import { Module } from '@nestjs/common';
@Module({
imports: [],
controllers: [],
providers: [],
})
export class AppModule {}
📌 2. 컨트롤러(Controller) – 요청 처리
컨트롤러는 클라이언트의 요청을 받아 처리하는 역할을 합니다.
import { Controller, Get } from '@nestjs/common';
@Controller('users') // '/users' 경로를 처리
export class UserController {
@Get()
findAll() {
return "모든 유저 조회";
}
}
📌 3. 서비스(Service) – 비즈니스 로직 담당
서비스는 컨트롤러에서 호출되며, 실제 비즈니스 로직을 수행합니다.
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
getUsers() {
return ['User1', 'User2', 'User3'];
}
}
📌 4. 의존성 주입(Dependency Injection, DI)
NestJS는 의존성 주입(DI)을 기본적으로 지원하여, 객체 간의 결합도를 낮추고 관리하기 쉽게 합니다.
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
findAll() {
return this.userService.getUsers();
}
}
🏗️ NestJS의 싱글톤 패턴과 주의할 점
🎯 싱글톤(Singleton) 패턴이란?
싱글톤 패턴은 하나의 클래스에서 단 하나의 인스턴스만 생성하도록 제한하는 패턴입니다.
주로 전역 상태를 유지하거나 공통 리소스를 관리할 때 사용됩니다.
✅ 싱글톤 특징
- 1클래스 1기능을 수행하며, 객체가 한 번 생성되면 해당 인스턴스를 계속 재사용합니다.
- 인스턴스를 여러 개 만들지 않고 단 하나만 유지하는 구조입니다.
- NestJS에서는 @Injectable({ providedIn: 'root' }) 또는 module 단위로 providers에 등록하여 싱글톤으로 관리합니다.
✅ NestJS의 싱글톤 인스턴스 관리
NestJS에서는 개발자가 직접 인스턴스를 생성하는 것이 아니라 모듈을 통해 자동으로 싱글톤 인스턴스를 관리합니다.
즉, NestJS에서 provider는 기본적으로 싱글톤으로 생성됩니다.
그러나 특정 조건에서는 싱글톤이 아닌 새로운 인스턴스가 생성될 수 있습니다!
❗ 실수 사례: WebSocket 응답이 두 번 오는 문제
🔹 문제 상황
NestJS에서 WebSocket을 이용한 메시지 전송 시, 응답이 두 번 오는 문제가 발생했습니다.
코드를 확인해도 로직상 이벤트를 중복으로 처리할 이유가 없었는데, 서버에서 동일한 응답이 두 번 전달되었습니다.
🔹 원인 분석
@Module({
imports: [ChatModule],
providers: [ChatGateway],
})
export class AppModule {}
위와 같이 AppModule에서 ChatModule을 import하고, 동시에 providers에 ChatGateway를 추가하면 인스턴스가 중복 생성될 수 있습니다.
🚨 결과적으로 WebSocket 응답도 두 번 전달됨 🚨
디버깅 결과:
constructor() {
console.log('ChatGateway 인스턴스 생성!');
}
→ console.log가 두 번 찍혔음 → 즉, 인스턴스가 두 개 생성됨!
🔹 해결 방법
✔️ 동일한 provider를 여러 모듈에서 중복 주입하지 않기
@Module({
imports: [],
controllers: [],
providers: [ChatGateway],
exports: [ChatGateway], // 다른 모듈에서 재사용 가능하도록 exports 설정
})
export class ChatModule {}
@Module({
imports: [ChatModule], // ChatGateway는 ChatModule에서 제공
})
export class AppModule {}
이렇게 하면 ChatGateway 인스턴스가 단 하나만 생성되며, 중복 응답 문제를 방지할 수 있습니다.
📌 NestJS의 싱글톤 인스턴스 생성 원리
✔️ 기본적으로 provider는 싱글톤으로 동작 ✔️ provider를 여러 곳에서 중복 정의하면 새로운 인스턴스가 생성됨 ✔️ 모듈에서 provider를 exports 하고 imports를 통해 참조하면 안전 ✔️ 전역적으로 관리하고 싶은 provider는 Global 모듈로 선언 가능
📖 공식 문서 참고: NestJS Injection Scopes
🎯 결론: 올바른 주입 방법
1️⃣ 가능하면 모듈 단위로 imports 하여 provider를 공유 2️⃣ 글로벌하게 사용해야 할 경우 @Global() 데코레이터를 활용 3️⃣ 반드시 providers에 직접 주입해야 하는 경우를 신중하게 판단
NestJS는 강력한 DI 시스템을 제공하지만, 무조건 모든 provider가 싱글톤이 되는 것은 아니다! 라는 점을 기억해야 합니다.
👉 올바른 provider 주입 구조를 설계하여 예기치 않은 버그를 방지하세요! 🚀
🔹 싱글톤을 쓰면 안 되는 경우
싱글톤이 적절하지 않은 경우도 존재합니다.
1️⃣ 팩토리 패턴(Factorial Pattern)이 필요한 경우
- 매번 새로운 객체를 생성해야 하는 경우
- 예: 사용자 요청마다 다른 설정을 가진 객체를 만들어야 할 때
- 싱글톤은 하나의 인스턴스를 공유하지만, 팩토리 패턴은 새로운 객체를 만들 수 있습니다.
- NestJS에서는 useFactory를 활용하여 동적으로 객체를 생성할 수 있습니다.
- 관련 공식 문서: NestJS Factory Providers
2️⃣ 의존성 주입(DI)이 필요한 경우
- 싱글톤 객체는 주입받은 의존성을 변경하기 어렵습니다.
- 상태를 변경하는 객체(예: 요청마다 다른 데이터를 가져와야 하는 객체)는 싱글톤으로 만들면 의존성 주입을 통해 동적으로 변경하기 어렵습니다.
- 즉, 의존성을 다르게 설정해야 하는 경우 싱글톤을 사용하면 비효율적입니다.
🔄 팩토리 패턴 (Factory Pattern)
팩토리 패턴은 객체의 생성을 담당하는 별도의 클래스나 함수를 두어 객체를 유연하게 생성하는 패턴입니다.
NestJS에서는 useFactory를 사용하여 이를 구현할 수 있습니다.
✅ 팩토리 패턴을 사용해야 하는 경우
- 각 요청마다 새로운 객체를 생성해야 하는 경우
- 예: 사용자별 커스텀 설정이 필요한 서비스
- 객체 생성 과정이 복잡할 때
- 여러 개의 의존성을 조합해야 하는 경우
- 동적으로 인스턴스를 생성해야 하는 경우
- 데이터베이스 연결을 요청마다 새로 생성해야 하는 경우
🚫 팩토리 패턴을 사용하면 안 되는 경우
- 전역적으로 하나의 객체만 유지하면 되는 경우
- 예: 로그 관리, 캐싱 시스템, 설정값 관리
- 객체 생성 비용이 크지 않고, 재사용이 필요한 경우
- 빈번한 인스턴스 생성이 불필요한 리소스 낭비로 이어질 수 있음
🔹 NestJS에서 싱글톤과 팩토리 패턴 활용 예시
✅ 싱글톤 서비스 (Repository 패턴 활용)
import { Injectable } from '@nestjs/common';
@Injectable()
export class SingletonService {
private data: string = '초기 데이터';
getData() {
return this.data;
}
setData(newData: string) {
this.data = newData;
}
}
해당 서비스는 @Injectable()로 등록되며, 애플리케이션 내에서 하나의 인스턴스만 유지됩니다.
✅ 팩토리 패턴 활용 (동적 객체 생성)
import { Injectable } from '@nestjs/common';
@Injectable()
export class DynamicService {
constructor(private readonly config: any) {}
getConfig() {
return this.config;
}
}
import { Module } from '@nestjs/common';
@Module({
providers: [
{
provide: 'DYNAMIC_SERVICE',
useFactory: () => {
return new DynamicService({ key: 'value' });
},
},
],
exports: ['DYNAMIC_SERVICE'],
})
export class DynamicModule {}
useFactory를 이용하면 매번 다른 설정값을 가진 인스턴스를 생성할 수 있습니다.
🔹 3레이어 구조 + DB 레포지토리 적용
3레이어 아키텍처(NestJS 스타일)에서 싱글톤과 레포지토리를 활용하는 방식입니다.
📌 구조
1️⃣ Controller - 요청을 받고 응답을 처리
2️⃣ Service - 비즈니스 로직 처리
3️⃣ Repository - 데이터베이스 관련 로직 처리
✅ 레포지토리 (Repository)
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserRepository {
private users = [];
findAll() {
return this.users;
}
save(user) {
this.users.push(user);
}
}
UserRepository는 데이터 저장소 역할을 하며, 싱글톤으로 유지됩니다.
✅ 서비스 (Service)
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
createUser(user) {
this.userRepository.save(user);
}
getAllUsers() {
return this.userRepository.findAll();
}
}
UserService는 UserRepository를 주입받아 비즈니스 로직을 처리합니다.
✅ 컨트롤러 (Controller)
import { Controller, Get, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
createUser(@Body() user) {
this.userService.createUser(user);
return { message: 'User created' };
}
@Get()
getAllUsers() {
return this.userService.getAllUsers();
}
}
UserController는 클라이언트의 요청을 받아 UserService를 호출하는 역할을 합니다.
🔹 정리
✅ 싱글톤 패턴
- 전역적으로 하나의 인스턴스만 유지해야 하는 경우 적합
- 로그, 설정값, 공통 캐시 관리 등에 유리
✅ 팩토리 패턴
- 매번 새로운 객체가 필요한 경우 적합
- 의존성을 다르게 주입해야 하는 경우 유용
✅ NestJS에서 싱글톤과 팩토리 적용
- Repository는 싱글톤으로 사용
- 유동적인 설정이 필요한 객체는 팩토리 패턴을 활용
📌 NestJS에서 서비스(Service)와 레포지토리(Repository) 예제
NestJS에서 3레이어 구조(Controller → Service → Repository) 를 적용하여 서비스를 구성하는 예제입니다.
이 예제에서는 사용자(User) 정보를 관리하는 기능을 구현합니다.
✅ 1. Repository 파일 (UserRepository)
Repository는 데이터베이스와 직접적으로 상호작용하는 역할을 합니다.
NestJS에서는 @Injectable()을 사용하여 싱글톤으로 관리할 수 있습니다.
📌 user.repository.ts
import { Injectable } from '@nestjs/common';
interface User {
id: number;
name: string;
email: string;
}
@Injectable()
export class UserRepository {
private users: User[] = []; // 임시 데이터 저장소 (DB 대신 사용)
// 모든 사용자 조회
findAll(): User[] {
return this.users;
}
// 특정 사용자 조회
findById(id: number): User | undefined {
return this.users.find(user => user.id === id);
}
// 사용자 추가
save(user: User): User {
this.users.push(user);
return user;
}
// 사용자 삭제
delete(id: number): boolean {
const index = this.users.findIndex(user => user.id === id);
if (index !== -1) {
this.users.splice(index, 1);
return true;
}
return false;
}
}
🛠 설명:
- findAll(): 모든 사용자 리스트 반환
- findById(id): 특정 ID의 사용자 찾기
- save(user): 새로운 사용자 추가
- delete(id): 특정 사용자를 삭제
✅ 2. 서비스 파일 (UserService)
Service는 비즈니스 로직을 처리하는 곳으로, Repository를 사용하여 데이터를 조작합니다.
📌 user.service.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
interface User {
id: number;
name: string;
email: string;
}
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
// 모든 사용자 조회
getAllUsers(): User[] {
return this.userRepository.findAll();
}
// 특정 사용자 조회
getUserById(id: number): User | undefined {
return this.userRepository.findById(id);
}
// 사용자 생성
createUser(name: string, email: string): User {
const newUser: User = {
id: Date.now(), // 간단한 ID 생성
name,
email,
};
return this.userRepository.save(newUser);
}
// 사용자 삭제
deleteUser(id: number): boolean {
return this.userRepository.delete(id);
}
}
🛠 설명:
- getAllUsers(): 모든 사용자 조회
- getUserById(id): 특정 사용자 조회
- createUser(name, email): 새로운 사용자 추가
- deleteUser(id): 사용자 삭제
✅ 3. 모듈 파일 (UserModule)
Repository와 Service를 UserModule에 등록하여 사용합니다.
📌 user.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
@Module({
providers: [UserService, UserRepository],
exports: [UserService], // 다른 모듈에서 사용할 수 있도록 내보내기
})
export class UserModule {}
🛠 설명:
- providers: UserService와 UserRepository를 NestJS의 DI 컨테이너에 등록
- exports: UserService를 내보내서 다른 모듈에서 사용할 수 있도록 함
✅ 4. 컨트롤러 파일 (UserController)
클라이언트 요청을 받아 Service를 호출하는 역할을 합니다.
📌 user.controller.ts
import { Controller, Get, Post, Delete, Param, Body } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
// 모든 사용자 조회
@Get()
getAllUsers() {
return this.userService.getAllUsers();
}
// 특정 사용자 조회
@Get(':id')
getUserById(@Param('id') id: string) {
return this.userService.getUserById(Number(id));
}
// 사용자 생성
@Post()
createUser(@Body() body: { name: string; email: string }) {
return this.userService.createUser(body.name, body.email);
}
// 사용자 삭제
@Delete(':id')
deleteUser(@Param('id') id: string) {
return this.userService.deleteUser(Number(id));
}
}
🛠 설명:
- @Get() → 모든 사용자 조회
- @Get(':id') → 특정 사용자 조회
- @Post() → 사용자 추가
- @Delete(':id') → 사용자 삭제
✅ 5. 메인 모듈 파일 (app.module.ts)
모든 모듈을 AppModule에서 가져와서 사용할 수 있도록 등록합니다.
📌 app.module.ts
import { Module } from '@nestjs/common';
import { UserModule } from './user/user.module';
import { UserController } from './user/user.controller';
@Module({
imports: [UserModule],
controllers: [UserController],
})
export class AppModule {}
UserModule을 AppModule에 등록하여 NestJS가 해당 모듈을 인식할 수 있도록 합니다.
✅ 6. 실행 방법
1️⃣ NestJS 프로젝트에서 실행
npm run start
2️⃣ API 테스트 (Postman 또는 curl 사용)
- 모든 사용자 조회
curl -X GET http://localhost:3000/users
- 특정 사용자 조회 (ID: 1)
curl -X GET http://localhost:3000/users/1
- 새 사용자 추가
curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name": "John Doe", "email": "john@example.com"}'
- 사용자 삭제 (ID: 1)
curl -X DELETE http://localhost:3000/users/1
✅ 정리
- Repository (user.repository.ts) → DB와 직접 연결 (데이터 저장/조회/삭제)
- Service (user.service.ts) → 비즈니스 로직 담당 (데이터 가공 및 처리)
- Controller (user.controller.ts) → HTTP 요청을 처리하고 Service 호출
- Module (user.module.ts) → 서비스와 레포지토리를 NestJS에 등록
NestJS의 기본적인 3레이어 구조를 적용한 예제입니다. 🚀
이제 원하는 기능을 추가하거나, DB와 연동하는 방식으로 확장할 수 있습니다.
참고
https://jun-choi-4928.medium.com/nest-js-behind-the-curtain-712b39abd49c
https://jay-ji.tistory.com/106
https://velog.io/@kdhn712/NestJs-%EB%B0%9C%ED%91%9C
https://velog.io/@pear/Query-String-%EC%BF%BC%EB%A6%AC%EC%8A%A4%ED%8A%B8%EB%A7%81%EC%9D%B4%EB%9E%80
팩토리얼패턴
https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory
'내일배움 정리 > JS 문법 공부' 카테고리의 다른 글
NestJs - HTTP 예외 처리 클래스 (0) | 2025.02.14 |
---|---|
NestJs 컨트롤러 - req 데이터 사용 (0) | 2025.02.13 |
NestJS에서 TypeORM을 사용할 때 Repository에서 제공하는 주요 메서드 (0) | 2025.02.13 |
NestJS - IsDate() vs @IsDateString() (0) | 2025.02.13 |
NestJS 파일 생성 (0) | 2025.02.13 |