낙서장이자 오답 노트이자 컨닝 페이퍼
패스트캠퍼스 환급챌린지 4일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기 본문
패스트캠퍼스 환급챌린지 4일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기
NangIn 2025. 4. 4. 20:22본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.
강의 내용 정리
Dependency Injection (DI) & Inversion of Control (IoC)
Dependency Injection (의존성 주입)
- 클래스 A가 클래스 B를 사용해야 할 때, 일반적으로 내부에서 new B()로 인스턴스를 직접 생성
- → 강한 결합(Tight Coupling), 유지보수 어려움
- DI 방식:
class A { constructor(private b: B) {} } - 클래스 A는 B의 인스턴스를 생성자(constructor)를 통해 외부로부터 주입받음
- 장점:
- 느슨한 결합(Loose Coupling)
- 테스트 용이 (Mock 객체 주입 가능)
- 중복 인스턴스 없이 공통 인스턴스 재사용 가능
Inversion of Control (제어의 역전)
- 객체 생성과 주입의 제어권을 개발자가 아닌 프레임워크(NestJS)에 넘기는 개념
- NestJS 내부의 IoC 컨테이너가 필요한 클래스를 자동으로 생성하고 주입해줌
- → 개발자는 생성에 신경 쓸 필요 없이 타입만 선언하면 됨
NestJS에서의 적용 흐름
- @Injectable()을 클래스에 붙이면 IoC 컨테이너에 등록됨
- 필요할 때 constructor(private b: BService)로 주입 선언
- NestJS가 해당 클래스 인스턴스를 자동 생성하고 필요한 곳에 주입
- 동일한 인스턴스를 여러 곳에서 공유하여 사용 (싱글톤처럼 작동)
DI와 IoC 관계
- DI는 IoC를 구현하는 방식 중 하나
- NestJS는 IoC 컨테이너를 기반으로 DI를 자동으로 수행함
Dependency injection and Inversion of Control NestJS 코드에서 확인해보기
AppService 클래스 구조 이해
- app.service.ts에 정의된 클래스는 @Injectable() 데코레이터가 붙어 있음
- → NestJS의 IOC 컨테이너가 이 클래스를 관리함
- @Injectable()의 역할:
- → "이 클래스를 IoC 컨테이너에서 관리하고, 필요한 곳에 주입해줘" 라는 뜻
AppController에서 서비스 주입 확인
- app.controller.ts를 보면, 생성자 안에 AppService가 의존성으로 주입됨
constructor(private readonly appService: AppService) {}
- 즉, NestJS는 AppController를 생성할 때, AppService 인스턴스를 자동으로 생성하고 주입해줌
- → Dependency Injection (DI)
그럼 누가 이걸 인스턴스화하나?
- main.ts에는 직접 인스턴스 생성 코드 없음 → NestJS가 대신 수행
- 실질적인 인스턴스 생성과 의존성 주입은 NestJS의 IoC 컨테이너에서 자동 수행됨
AppModule에서 구조적으로 선언
- app.module.ts에서 다음과 같이 구성되어 있음:
@Module({
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
- 여기서 선언만 해주면, NestJS는:
- AppController에 정의된 내용을 기반으로 엔드포인트 생성
- AppService를 인스턴스화하여 필요한 곳에 주입
IoC 컨테이너의 역할 요약
- 클래스가 @Injectable()로 정의되어 있고 모듈의 providers 배열에 등록되어 있다면 NestJS의 IoC 컨테이너는
- 인스턴스를 자동 생성
- 필요한 컨트롤러 또는 다른 서비스의 생성자에 자동 주입
Express와의 차이점
- Express에서는 서비스 인스턴스 생성, 의존성 주입, 연결 로직을 직접 구현해야 함
- NestJS에서는:
- 위의 모든 것을 프레임워크가 자동으로 처리
- 단, 구조만 맞추면 됨 (모듈 → 컨트롤러 → 서비스)
서비스 분리와 책임 분리 (Controller vs Service)
왜 서비스로 로직을 분리해야 할까?
- NestJS의 아키텍처는 역할 분리를 명확히 하는 것이 원칙
- → 유지보수를 쉽게 하고, 에러의 원인을 빠르게 파악하기 위해 로직을 분리
- Controller의 책임
- 요청 데이터 수신 (Param, Query, Body 등)
- 타입 및 유효성 검증
- Service의 책임
- 실제 비즈니스 로직 수행
- 데이터 가공, 처리, 반환
코드 분리 흐름
- 기존 컨트롤러에 있던 공통 데이터(movies, idCounter, Movie 인터페이스)를 서비스로 이동
- 인터페이스는 export 키워드를 붙여 외부에서 사용할 수 있도록 설정
- 각 요청 유형에 맞게 서비스 메서드 정의
// **GET 전체/조건 조회: getManyMovies(title?: string)**
getManyMovies(title?: string): Movie[] {
if (!title) return this.movies;
return this.movies.filter((m) => m.title.startsWith(title));
}
// **GET 단건 조회: getMovie(id: number)**
getMovieById(id: number): Movie {
const movie = this.movies.find((m) => m.id === id);
if (!movie) throw new NotFoundException('존재하지 않는 영화입니다.');
return movie;
}
// **POST 생성: createMovie(title: string)**
createMovie(title: string): Movie {
const movie = { id: this.idCounter++, title };
this.movies.push(movie);
return movie;
}
// **PATCH 수정: updateMovie(id: number, title: string)**
updateMovie(id: number, title: string): Movie {
const movie = this.movies.find((m) => m.id === id);
if (!movie) throw new NotFoundException('존재하지 않는 영화입니다.');
Object.assign(movie, { title });
return movie;
}
// **DELETE 삭제: deleteMovie(id: number)**
deleteMovie(id: number): { id: number } {
const index = this.movies.findIndex((m) => m.id === id);
if (index === -1) throw new NotFoundException('존재하지 않는 영화입니다.');
this.movies.splice(index, 1);
return { id };
}
- 컨트롤러에서는 각 요청에 대해 서비스 메서드를 위임 호출만 담당
// Controller
@Get()
getMovies(@Query('title') title?: string) {
return this.appService.getManyMovies(title);
}
@Get(':id')
getMovie(@Param('id') id: string) {
return this.appService.getMovieById(+id);
}
@Post()
createMovie(@Body('title') title: string) {
return this.appService.createMovie(title);
}
@Patch(':id')
updateMovie(@Param('id') id: string, @Body('title') title: string) {
return this.appService.updateMovie(+id, title);
}
@Delete(':id')
deleteMovie(@Param('id') id: string) {
return this.appService.deleteMovie(+id);
}
서비스 주입 확인 (DI + IoC 작동 확인)
- AppService에 @Injectable() 선언 → IoC 컨테이너가 인스턴스 관리
- app.module.ts에서 AppService를 providers에 등록
- NestJS는 AppController를 생성할 때 자동으로 AppService 주입
모듈의 providers에 넣는 이름은 왜 service인가?
- Service는 Provider의 일종이다.
- Service는 주로 비즈니스 로직을 처리하는 클래스
- @Injectable()로 선언 → DI 대상이 됨
- Provider는 더 넓은 개념이다.
- DI 컨테이너가 주입할 수 있는 모든 대상
- 예: Service, Repository, Guard, Pipe, Factory 등
- 왜 이름을 'Service'라고 지을까?
- 역할에 따라 명명
- 로직 처리용 클래스는 관례적으로 SomethingService로 이름 짓는 것
핵심 포인트 요약
- 컨트롤러는 요청만 받음 → 로직은 서비스로 위임
- NestJS는 @Injectable()과 providers 등록만 해주면 → IoC 컨테이너가 자동 인스턴스 생성 + 주입
- 구조가 명확하면 유지보수도 쉽고, 테스트도 용이
학습 후기
NestJS를 처음 접했을 때 가장 헷갈렸던 개념이 바로 서비스와 프로바이더의 관계, 그리고 DI와 IoC이었습니다. 이번 학습을 통해 단순히 코드를 따라치는 것이 아니라, NestJS가 설계된 아키텍처 철학 자체를 이해할 수 있었던 점이 가장 큰 수확이었습니다.
특히 인상 깊었던 건, NestJS에서는 비즈니스 로직을 서비스로 분리하고, 컨트롤러는 오직 요청만 담당하는 구조를 강제한다는 점입니다. 이 덕분에 유지보수성과 테스트 가능성이 눈에 띄게 높아지고, 에러의 원인도 쉽게 추적할 수 있었습니다. 과거에는 하나의 파일에서 모든 로직을 처리하면서 생겼던 복잡함이 NestJS의 구조 덕분에 말끔히 정리되는 느낌이었습니다.
DI와 IoC의 원리를 실제 코드에서 확인할 수 있었던 것도 매우 유익했습니다. @Injectable()로 선언된 클래스가 IoC 컨테이너에 등록되고, 모듈에서 providers에만 추가해주면 어디서든 자동으로 주입된다는 구조는 처음에는 신기했고, 익숙해지니 매우 효율적이라는 걸 느꼈습니다. 특히 인스턴스를 직접 관리하지 않아도 된다는 점은 생산성 측면에서 큰 장점이었습니다.
추가로, 서비스는 단지 NestJS의 관례일 뿐, 본질적으로는 Provider의 하위 개념이라는 점도 명확히 이해할 수 있었습니다. 이처럼 개념과 구조가 정리되니 코드의 방향성과 역할이 눈에 들어오기 시작했고, 자연스럽게 NestJS를 ‘프레임워크’가 아닌 ‘설계 철학’으로 받아들일 수 있었습니다.
이번 학습을 통해 NestJS의 기반이 되는 구조적 사고방식을 익힐 수 있었고, 이는 앞으로의 프로젝트 설계에 큰 자산이 될 거라 확신합니다. NestJS는 단순한 툴이 아니라, 협업과 유지보수를 고려한 체계적인 백엔드 아키텍처라는 점에서 매우 훌륭한 프레임워크임을 다시 한번 느낄 수 있었습니다.
학습 인증샷




https://abit.ly/lisbva
Abit.ly 다운받기
abit.ly