Loading the catalog…
Loading the catalog…
NestJS를 사용하다 보면 기본적으로 Module , Controller , Service 라는 구조를 자주 사용하게 된다. 처음에는 단순히 파일을 나눠놓은 것처럼 보였지만, 각각의 역할이 다르고 NestJS의 DI(Dependency Injection)를 통해 서로 연결된다는 것을 알게 되었다. 이번에는 좌석 선점 API를 예시로 NestJS의 기본 구조와 DI가 어떻게 동작하는지 정리해보려고 한다. 1. NestJS의 기본 구조 좌석을 선점하는 다음 API가 있다고 해보자. POST /seats/10/hold 사용자가 요청을 보내면 전체적인 흐름은 다음과 같다. Client │ │ POST /seats/10/hold ▼ Controller │ │ Service 호출 ▼ Service │ ├── Redis └── MySQL │ ▼ Controller │ ▼ HTTP Response 간단하게 역할을 나누면 다음과 같다. Controller → HTTP 요청을 받음 Service → 실제 비즈니스 로직을 처리 Module → 관련 Controller와 Provider 등을 구성하고 등록 DI → 필요한 객체를 NestJS가 주입 각각 어떤 역할을 하는지 하나씩 살펴보자. 2. Controller Controller는 클라이언트로부터 들어오는 HTTP 요청을 받는 역할 을 한다. 예를 들어 좌석 선점 API를 다음과 같이 만들 수 있다. @Controller('seats') export class SeatController { constructor( private readonly seatService: SeatService, ) {} @Post(':seatId/hold') async holdSeat( @Param('seatId') seatId: string, @Body('userId') userId: number, ) { return this.seatService.holdSeat( Number(seatId), userId, ); } } @Controller('seats') 와: @Post(':seatId/hold') 가 합쳐지면 다음 요청을 처리하게 된다. POST /seats/:seatId/hold 따라서: POST /seats/10/hold 이라는 요청이 들어오면 seatId 에는 10 이 들어오게 된다. Controller는 이 값을 받아 직접 모든 작업을 처리하기보다는: this.seatService.holdSeat(...) 를 호출해 실제 처리를 Service에 맡긴다. 3. Service Service는 실제 비즈니스 로직을 처리하는 역할 을 한다. 좌석 선점 기능을 예로 들면 다음과 같은 로직이 Service에 들어갈 수 있다. @Injectable() export class SeatService { async holdSeat( seatId: number, userId: number, ) { const key = `seat:hold:${seatId}`; const acquired = await this.redis.set( key, String(userId), 'EX', 10, 'NX', ); if (!acquired) { throw new ConflictException( '다른 사용자가 선점 중입니다.', ); } const result = await this.prisma.seat.updateMany({ where: { id: seatId, status: 'AVAILABLE', }, data: { status: 'HOLDING', heldBy: userId, }, }); return result; } } 이 Service에서는: Redis를 통한 좌석 선점 ↓ DB 상태 확인 및 변경 ↓ 결과 반환 같은 실제 기능을 처리한다. 따라서 Controller와 Service의 역할을 나누면 다음과 같이 생각할 수 있다. Controller "어떤 HTTP 요청이 들어왔는가?" ↓ 요청을 받고 필요한 데이터를 Service에 전달 Service "그 요청을 실제로 어떻게 처리할 것인가?" ↓ 비즈니스 로직 수행 ↓ DB / Redis 등 사용 4. Controller와 Service를 왜 나눌까? 처음에는 Controller에서도 모든 로직을 작성할 수 있지 않을까라는 생각이 들 수 있다. 예를 들어: @Post(':seatId/hold') async holdSeat() { // Redis 처리 // DB 조회 // DB UPDATE // 좌석 선점 규칙 // 예외 처리 // 결과 반환 } 기술적으로는 이런 코드를 작성할 수도 있다. 하지만 기능이 많아질수록 Controller가 다음과 같이 여러 책임을 가지게 된다. Controller HTTP 요청 처리 + Redis 처리 + DB 처리 + 좌석 선점 규칙 + 예외 처리 + 기타 비즈니스 로직 결국 Controller가 너무 커지고 코드를 이해하거나 수정하기 어려워질 수 있다. 그래서 역할을 나눈다. Controller → HTTP 요청과 응답 처리 Service → 비즈니스 로직 처리 이렇게 책임을 분리하면 각 클래스가 담당하는 역할이 명확해지고 유지보수와 테스트도 쉬워진다. 5. Module Controller와 Service를 만들었다고 해서 NestJS가 자동으로 모든 것을 사용하는 것은 아니다. NestJS에서는 Module을 통해 관련된 구성요소를 하나의 기능 단위로 구성하고 등록 한다. 좌석 기능을 예로 들면 다음과 같은 구조를 만들 수 있다. seat/ ├── seat.module.ts ├── seat.controller.ts └── seat.service.ts 그리고 SeatModule 을 다음과 같이 작성한다. @Module({ controllers: [SeatController], providers: [SeatService], }) export class SeatModule {} 여기서: controllers: [SeatController] 는 이 Module에서 사용할 Controller를 등록한다. 그리고: providers: [SeatService] 는 SeatService 를 Provider로 등록한다. 전체적으로 보면: SeatModule │ ├── controllers │ └── SeatController │ └── providers └── SeatService 와 같은 구조가 된다. 6. DI(Dependency Injection) NestJS의 구조를 공부하면서 중요하게 등장하는 개념이 DI(Dependency Injection), 의존성 주입 이다. 예를 들어 SeatController 는 SeatService 가 필요하다. DI를 사용하지 않고 직접 만든다고 생각하면 다음과 같이 작성할 수도 있다. const seatService = new SeatService(); 하지만 실제 NestJS에서는 보통 이렇게 하지 않는다. Controller의 constructor에서: constructor( private readonly seatService: SeatService, ) {} 처럼 작성한다. 여기서는: new SeatService() 를 직접 호출하지 않았다. 그런데도: this.seatService.holdSeat(...) 처럼 SeatService 를 사용할 수 있다. NestJS가 필요한 SeatService 를 생성하고 Controller에 주입해주기 때문 이다. 이것이 의존성 주입이다. 7. @Injectable()은 무엇일까? Service를 보면 다음과 같은 코드가 붙어 있다. @Injectable() export class SeatService { } @Injectable() 은 해당 클래스가 NestJS의 DI 시스템에서 관리될 수 있는 Provider라는 것을 나타낸다. 그리고 Module에서는: @Module({ controllers: [SeatController], providers: [SeatService], }) export class SeatModule {} providers 에 SeatService 를 등록한다. 그러면 Controller에서는: constructor( private readonly seatService: SeatService, ) {} 를 통해 SeatService 를 주입받아 사용할 수 있다. 흐름을 정리하면 다음과 같다. SeatService ↓ @Injectable() ↓ NestJS의 DI 시스템에서 관리할 수 있는 Provider ↓ Module의 providers에 등록 ↓ Controller에서 SeatService 필요 ↓ constructor를 통해 요청 ↓ NestJS가 SeatService를 주입 따라서 Controller가 직접 Service를 생성하고 관리할 필요가 없다. 8. 왜 직접 new를 사용하지 않을까? 처음에는 다음과 같이 직접 생성해도 큰 차이가 없어 보일 수 있다. const seatService = new SeatService(); 하지만 SeatService 도 다른 객체를 필요로 한다면 이야기가 달라진다. 예를 들어: @Injectable() export class SeatService { constructor( private readonly prisma: PrismaService, private readonly redis: RedisService, ) {} } 라고 해보자. 직접 생성한다면: const prisma = new PrismaService(); const redis = new RedisService(); const seatService = new SeatService( prisma, redis, ); 처럼 필요한 객체를 직접 생성해야 한다. 그리고 RedisService 가 또 다른 객체를 필요로 한다면 직접 관리해야 하는 의존성은 계속 증가한다. SeatController ↓ SeatService ↙ ↘ Prisma Redis Service Service ↓ Config 이러한 객체 생성과 의존 관계를 직접 관리하는 대신 NestJS의 DI 시스템에 맡기는 것이다. "SeatController는 SeatService가 필요하다." "SeatService는 PrismaService와 RedisService가 필요하다." ↓ NestJS가 필요한 Provider를 찾아 연결 따라서 클래스에서는 자신에게 필요한 의존성을 선언하고 NestJS가 이를 주입해준다. 9. Module의 imports, controllers, providers, exports Module에는 자주 사용하는 네 가지 속성이 있다. @Module({ imports: [], controllers: [], providers: [], exports: [], }) export class SeatModule {} 각각의 역할은 다음과 같다. 속성 역할 controllers HTTP 요청을 처리할 Controller 등록 providers Service 등의 Provider 등록 exports Provider를 다른 Module에서도 사용할 수 있도록 공개 imports 다른 Module을 가져와 해당 Module이 공개한 Provider 사용 controllers 와 providers 는 같은 Module 안에서 Controller와 Service를 구성할 때 사용했다. 그렇다면 imports 와 exports 는 언제 필요한지 살펴보자. 10. 다른 Module의 Service를 사용하려면? 예를 들어 다음과 같은 구조가 있다고 해보자. UserModule └── UserService OrderModule └── OrderService 그런데 OrderService 에서 사용자 정보를 확인하기 위해 UserService 가 필요해졌다. OrderService │ └──────→ UserService 이때 UserModule 에서 UserService 를 외부에 공개해야 한다. @Module({ providers: [UserService], exports: [UserService], }) export class UserModule {} 여기서: exports: [UserService] 는: "UserService를 다른 Module에서도 사용할 수 있도록 공개하겠다." 라는 의미로 볼 수 있다. 11. imports 이제 OrderModule 에서는 UserModule 을 가져온다. @Module({ imports: [UserModule], providers: [OrderService], }) export class OrderModule {} UserModule 이 UserService 를 exports 했고, OrderModule 이 UserModule 을 imports 했기 때문에 OrderService 에서 다음과 같이 사용할 수 있다. @Injectable() export class OrderService { constructor( private readonly userService: UserService, ) {} async createOrder() { const user = await this.userService.findUser(); } } 전체 흐름은 다음과 같다. UserService ↓ UserModule의 providers에 등록 ↓ UserModule에서 exports ↓ OrderModule에서 UserModule imports ↓ OrderService ↓ constructor에서 UserService 주입 imports 와 exports 가 처음에는 헷갈릴 수 있지만 어느 Module의 입장에서 바라보는지 생각하면 이해하기 쉽다. exports 내 Module에 있는 Provider를 밖으로 내보낸다 → -------------------------------- imports 다른 Module을 내 Module로 가져온다 ← 12. 전체 흐름으로 다시 보기 지금까지의 내용을 좌석 선점 기능으로 다시 연결해보자. 클라이언트가 다음 요청을 보낸다. POST /seats/10/hold 먼저 Controller가 요청을 받는다. Client ↓ SeatController Controller는 실제 좌석 선점 로직을 직접 수행하지 않고 SeatService 를 호출한다. Client ↓ SeatController ↓ SeatService Service에서는 Redis와 DB 등을 이용해 실제 비즈니스 로직을 수행한다. Client ↓ SeatController ↓ SeatService │ ├── Redis │ └── MySQL 그리고 이 Controller와 Service는 SeatModule 에서 등록한다. SeatModule │ ├── controllers │ └── SeatController │ └── providers └── SeatService Controller가 Service를 직접 생성하는 것이 아니라 NestJS의 DI를 이용해 주입받는다. SeatController constructor( SeatService ) ↑ NestJS가 주입 ↑ SeatModule providers: [SeatService] 다른 Module에서도 SeatService 가 필요하다면: SeatModule exports: [SeatService] ↓ 다른 Module imports: [SeatModule] ↓ constructor에서 SeatService 주입 이라는 흐름으로 연결할 수 있다. 13. 공부하면서 헷갈렸던 부분 처음에는 @Injectable() , providers , constructor 가 모두 비슷한 역할처럼 느껴졌다. 하지만 각각의 역할은 조금씩 다르다. @Injectable() → 이 클래스가 NestJS의 DI 시스템에서 관리될 수 있는 Provider임을 나타냄 providers: [SeatService] → 이 Module에서 SeatService를 Provider로 등록 constructor( private readonly seatService: SeatService ) → 현재 클래스가 SeatService를 필요로 한다는 것을 선언 또한 imports 와 exports 도 처음에는 헷갈릴 수 있었다. 하지만 Module을 기준으로 생각하면: exports → 내가 가진 것을 밖으로 공개 imports → 다른 Module을 가져와 사용 이라고 생각할 수 있었다. 14. 정리하면서 배운 점 NestJS의 Module , Controller , Service 는 단순히 파일을 나누기 위한 구조가 아니었다. 클라이언트의 요청이 들어오면: HTTP Request ↓ Controller ↓ Service ↓ DB / Redis ↓ 결과 반환 이라는 흐름으로 처리할 수 있다. Controller는 HTTP 요청을 받고 Service를 호출하며, Service는 실제 비즈니스 로직을 수행한다. 그리고 Module은 이러한 Controller와 Provider를 하나의 기능 단위로 구성하고 NestJS에 등록한다. 또한 Service 같은 의존성을 직접 new 로 생성하는 대신 NestJS의 DI를 통해 필요한 객체를 주입받을 수 있다. 전체적으로 정리하면 다음과 같은 흐름으로 이해할 수 있었다. NestJS 기능 구현 ↓ Module ↓ Controller 등록 ↓ Service를 Provider로 등록 ↓ Controller에서 Service 필요 ↓ constructor를 통한 DI ↓ Service에서 비즈니스 로직 수행 그리고 다른 Module의 Service가 필요한 경우에는: Provider ↓ 원래 Module에서 exports ↓ 사용할 Module에서 imports ↓ constructor에서 DI 의 흐름으로 사용할 수 있다. 결국 NestJS의 기본 구조를 이해할 때 중요한 것은 각각의 문법을 따로 외우는 것이 아니라, 요청을 받는 Controller, 비즈니스 로직을 담당하는 Service, 이들을 구성하는 Module, 그리고 필요한 객체를 연결해주는 DI가 서로 어떻게 연결되는지 이해하는 것 이라고 생각한다.
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
NestJS 기본 구조 이해하기 - Module, Controller, Service, DI. NestJS를 사용하다 보면 기본적으로 Module , Controller , Service 라는 구조를 자주 사용하게 된다. 처음에는 단순히 파일을 나눠놓은 것처럼 보였지만, 각각의 역할이 다르고 NestJS의 DI(Dependency Injection)를 통해 서로 연결된다는 것을 알게 되었다. 이번에는 좌석 선점 API를 예시로 NestJS의 기본 구조와 DI가 어떻게 동작하는지 정리해보려고 한다. 1. NestJS의 기본 구조 좌석을 선점하는 다음 API가 있다고 해보자. POST /seats/10/hold 사용자가 요청을 보내면 전체적인 흐름은 다음과 같다. Client │ │ POST…
Open source