Skip to content

Conversation

@yoonc01
Copy link
Owner

@yoonc01 yoonc01 commented Dec 8, 2025

NestJS 핵심 개념

NestJS의 핵심 개념인 IoC, 의존성 주입, 그리고 아키텍처 구조를 학습하고 실제 Post CRUD API를 구현했습니다.

📌 학습 내용

1. IoC (Inversion of Control, 제어의 역전)

IoC란?

  • 프로그램의 흐름을 개발자가 직접 제어하는 것이 아니라 프레임워크나 컨테이너가 대신 제어하는 디자인 패턴
  • "누가 객체를 만들고 관리할까?"에 대한 제어권이 프레임워크로 역전됨

IoC가 아닌 패턴 (전통적인 방식)

class Database {
  connect() {
    console.log("DB 연결!");
  }

  query(sql: string) {
    return `${sql} 실행 결과`;
  }
}

class Logger {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}

class UserService {
  private database: Database;
  private logger: Logger;

  constructor() {
    // ❌ UserService가 직접 의존성을 생성하고 제어함
    this.database = new Database();
    this.logger = new Logger();
  }

  getUser(id: number) {
    this.logger.log(`유저 ${id} 조회`);
    return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
  }
}

// 사용
const userService = new UserService(); // UserService가 모든 것을 직접 생성
userService.getUser(1);

문제점:

  • UserService가 Database와 Logger를 직접 생성하고 강하게 결합됨
  • 테스트할 때 Mock 객체로 교체하기 어려움
  • Database 구현체를 바꾸려면 UserService 코드를 수정해야 함

IoC 패턴 (의존성 주입)

// 인터페이스 정의
interface IDatabase {
  connect(): void;
  query(sql: string): string;
}

interface ILogger {
  log(message: string): void;
}

// 구현체
class MySQLDatabase implements IDatabase {
  connect() {
    console.log("MySQL 연결!");
  }

  query(sql: string) {
    return `MySQL: ${sql} 실행 결과`;
  }
}

class ConsoleLogger implements ILogger {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}

class UserService {
  // ✅ 의존성을 외부에서 주입받음 (생성자 주입)
  constructor(
    private database: IDatabase,
    private logger: ILogger
  ) {}

  getUser(id: number) {
    this.logger.log(`유저 ${id} 조회`);
    return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
  }
}

// 사용 - 직접 주입
const database = new MySQLDatabase();
const logger = new ConsoleLogger();
const userService = new UserService(database, logger);
userService.getUser(1);

// 테스트 시 - Mock으로 쉽게 교체 가능
class MockDatabase implements IDatabase {
  connect() {}
  query(sql: string) { return "mock data"; }
}

const mockDb = new MockDatabase();
const mockLogger = new ConsoleLogger();
const testService = new UserService(mockDb, mockLogger);

NestJS에서의 IoC (프레임워크가 제어)

// database.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class DatabaseService {
  connect() {
    console.log("DB 연결!");
  }

  query(sql: string) {
    return `${sql} 실행 결과`;
  }
}

// logger.service.ts
@Injectable()
export class LoggerService {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}

// user.service.ts
@Injectable()
export class UserService {
  // ✅ NestJS가 자동으로 의존성을 주입해줌!
  constructor(
    private database: DatabaseService,
    private logger: LoggerService
  ) {}

  getUser(id: number) {
    this.logger.log(`유저 ${id} 조회`);
    return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
  }
}

// user.module.ts
@Module({
  providers: [UserService, DatabaseService, LoggerService],
  controllers: [UserController],
})
export class UserModule {}

NestJS의 IoC 컨테이너가 하는 일:

  1. @Injectable() 데코레이터를 보고 클래스를 등록
  2. 의존성 그래프를 분석
  3. 필요한 인스턴스를 자동으로 생성하고 주입
  4. 싱글톤으로 관리 (기본)

2. 의존성 주입 (Dependency Injection)

"의존성(Dependency)"이란?

의존성 = A가 B를 사용할 때, "A는 B에 의존한다"

class Car {
  private engine: Engine;  // Car는 Engine에 의존함

  drive() {
    this.engine.start();  // Engine이 없으면 Car는 작동 불가!
  }
}
  • CarEngine을 사용함
  • Engine 없이는 Car가 제대로 작동할 수 없음
  • 따라서 "Car는 Engine에 의존한다" = Engine은 Car의 의존성

"주입(Injection)"이란?

주입 = 외부에서 안으로 넣어주는 것

// ❌ 주입이 아님 - 스스로 생성
class Car {
  private engine: Engine;

  constructor() {
    this.engine = new Engine();  // 내가 직접 만듦
  }
}

// ✅ 주입 - 외부에서 받음
class Car {
  private engine: Engine;

  constructor(engine: Engine) {  // 외부에서 주입받음!
    this.engine = engine;
  }
}

// 사용
const engine = new Engine();
const car = new Car(engine);  // Engine을 Car 안으로 "주입"

"의존성 주입(Dependency Injection)" 전체 의미

"객체가 필요로 하는 것(의존성)을 외부에서 넣어주는(주입) 것"

// 의존성들
class Engine {
  start() {
    console.log("엔진 시작!");
  }
}

class Tire {
  roll() {
    console.log("타이어 굴러감!");
  }
}

class Radio {
  play() {
    console.log("음악 재생!");
  }
}

// ❌ 의존성 주입 없음 - Car가 모든 것을 직접 생성
class CarWithoutDI {
  private engine: Engine;
  private tire: Tire;
  private radio: Radio;

  constructor() {
    // 내가 필요한 것들을 내가 직접 만듦
    this.engine = new Engine();
    this.tire = new Tire();
    this.radio = new Radio();
  }

  drive() {
    this.engine.start();
    this.tire.roll();
    this.radio.play();
  }
}

// ✅ 의존성 주입 - 필요한 것들을 외부에서 받음
class CarWithDI {
  constructor(
    private engine: Engine,   // 의존성 1: 주입받음
    private tire: Tire,       // 의존성 2: 주입받음
    private radio: Radio      // 의존성 3: 주입받음
  ) {}

  drive() {
    this.engine.start();
    this.tire.roll();
    this.radio.play();
  }
}

// 사용
const engine = new Engine();
const tire = new Tire();
const radio = new Radio();

// Car에게 필요한 것들을 "주입"
const car = new CarWithDI(engine, tire, radio);

왜 외부에서 주입받는 게 좋을까?

// ❌ 의존성 주입 없음 - 수정 어려움
class CarWithoutDI {
  constructor() {
    this.engine = new GasolineEngine();  // 휘발유 엔진 고정!
  }
}
// 전기차로 바꾸려면? Car 클래스 내부 코드를 수정해야 함!

// ✅ 의존성 주입 - 쉽게 교체 가능
class CarWithDI {
  constructor(private engine: Engine) {}
}

// 휘발유차
const gasCar = new CarWithDI(new GasolineEngine());

// 전기차 - Car 클래스는 수정 안 해도 됨!
const electricCar = new CarWithDI(new ElectricEngine());

// 테스트용 - Mock 엔진
const testCar = new CarWithDI(new MockEngine());

NestJS에서의 의존성 주입

// user.service.ts
@Injectable()
export class UserService {
  // UserService가 필요로 하는 것들 (의존성들)
  constructor(
    private database: DatabaseService,    // 의존성 1
    private logger: LoggerService,        // 의존성 2
    private emailSender: EmailService     // 의존성 3
  ) {
    // NestJS가 이 3개를 자동으로 "주입"해줌!
  }

  createUser(data: CreateUserDto) {
    this.logger.log('유저 생성 시작');          // 의존성 사용
    const user = this.database.save(data);     // 의존성 사용
    this.emailSender.sendWelcome(user.email);  // 의존성 사용
    return user;
  }
}

정리:

용어 의미 비유
의존성 내가 필요로 하는 것 커피를 만드는데 필요한 커피머신
주입 외부에서 넣어주는 것 누군가가 커피머신을 건네줌
의존성 주입 필요한 것을 외부에서 받음 커피머신을 직접 만들지 않고 받아서 사용

핵심: "내가 필요한 것을 내가 만들지 않고, 외부에서 받아 쓴다!"


3. NestJS 아키텍처 구조

요청 → Controller → Service → Repository → Database
                      ↓
응답 ← Controller ← Service ← Entity (데이터 형태)

Controller (컨트롤러)

역할: HTTP 요청을 받고 응답을 반환하는 입구

import { Controller, Get, Post, Body, Param } from '@nestjs/common';

@Controller('users')  // /users 경로
export class UserController {
  constructor(private userService: UserService) {}

  // GET /users
  @Get()
  getAllUsers() {
    return this.userService.findAll();
  }

  // GET /users/123
  @Get(':id')
  getUser(@Param('id') id: string) {
    return this.userService.findOne(+id);
  }

  // POST /users
  @Post()
  createUser(@Body() createUserDto: CreateUserDto) {
    return this.userService.create(createUserDto);
  }
}

특징:

  • 라우팅 처리 (@Get(), @Post(), @Put(), @Delete())
  • 요청 데이터 추출 (@Body(), @Param(), @Query())
  • 비즈니스 로직은 작성하지 않음 → Service에 위임

Service (서비스)

역할: 실제 비즈니스 로직을 처리하는 곳

import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {
  constructor(private userRepository: UserRepository) {}

  async findAll() {
    // 비즈니스 로직: 모든 유저 조회
    return this.userRepository.find();
  }

  async findOne(id: number) {
    // 비즈니스 로직: 유저 조회 + 없으면 에러
    const user = await this.userRepository.findOne({ where: { id } });
    if (!user) {
      throw new NotFoundException(`User #${id} not found`);
    }
    return user;
  }

  async create(createUserDto: CreateUserDto) {
    // 비즈니스 로직: 유저 생성 (예: 비밀번호 해싱)
    const hashedPassword = await bcrypt.hash(createUserDto.password, 10);

    const user = this.userRepository.create({
      ...createUserDto,
      password: hashedPassword,
    });

    return this.userRepository.save(user);
  }
}

특징:

  • @Injectable() 데코레이터 필수
  • 비즈니스 로직, 데이터 검증, 에러 처리
  • 재사용 가능 (다른 Service에서도 사용 가능)

Repository (레포지토리)

역할: 데이터베이스와 직접 통신하는 계층

import { Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './user.entity';

@Injectable()
export class UserRepository extends Repository<User> {
  constructor(
    @InjectRepository(User)
    private repository: Repository<User>,
  ) {
    super(repository.target, repository.manager, repository.queryRunner);
  }

  // 커스텀 쿼리 메서드
  async findByEmail(email: string) {
    return this.repository.findOne({ where: { email } });
  }

  async findActiveUsers() {
    return this.repository.find({ where: { isActive: true } });
  }
}

특징:

  • DB CRUD 작업 (find, save, update, delete)
  • TypeORM의 Repository 상속
  • SQL 쿼리를 직접 작성하지 않고 메서드로 처리

Entity (엔티티)

역할: 데이터베이스 테이블 구조를 정의하는 클래스

import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';

@Entity('users')  // DB 테이블 이름: users
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  email: string;

  @Column()
  name: string;

  @Column()
  password: string;

  @Column({ default: true })
  isActive: boolean;

  @CreateDateColumn()
  createdAt: Date;
}

특징:

  • DB 테이블과 1:1 매핑
  • 컬럼 타입, 제약조건 정의
  • 데이터의 "형태"를 나타냄

Module (모듈)

역할: 관련된 기능들을 하나로 묶는 컨테이너

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
import { User } from './user.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),  // Entity 등록
  ],
  controllers: [UserController],       // 컨트롤러 등록
  providers: [UserService, UserRepository],  // Service, Repository 등록
  exports: [UserService],              // 다른 모듈에서 사용 가능하게
})
export class UserModule {}

특징:

  • imports: 다른 모듈 가져오기
  • controllers: 컨트롤러 등록
  • providers: Service, Repository 등록 (DI 컨테이너에 등록)
  • exports: 다른 모듈에 제공

비교 정리

구성요소 역할 비유
Controller HTTP 요청/응답 처리 식당의 웨이터 (주문 받고 음식 전달)
Service 비즈니스 로직 식당의 주방장 (요리 만들기)
Repository DB 작업 식당의 창고 관리자 (재료 가져오기/저장)
Entity 데이터 구조 재료 목록표 (무엇이 있는지 정의)
Module 기능 묶음 식당 전체 (모든 구성원 관리)

왜 이렇게 나눌까?

// ❌ 모든 걸 Controller에 작성하면?
@Controller('users')
export class BadController {
  @Post()
  async create(@Body() data: any) {
    // DB 연결
    const connection = await createConnection();
    // 비즈니스 로직
    if (!data.email) throw new Error('Email required');
    // 저장
    await connection.query('INSERT INTO users...');
    // 이메일 발송
    await sendEmail(data.email);
  }
}
// 문제: 테스트 어려움, 재사용 불가, 코드 복잡

// ✅ 역할 분리
Controller  Service  Repository
// 장점: 테스트 쉬움, 재사용 가능, 코드 깔끔

- IoC(Inversion of Control) 개념 학습 및 적용
- 의존성 주입(Dependency Injection) 이해
- NestJS 아키텍처 구조 학습 (Controller, Service, Repository, Entity, Module)

구현 내용:
- Post Entity 및 DTO(create, update) 정의
- PostRepository: 인메모리 CRUD 구현
- PostService: 비즈니스 로직 처리
- PostController: REST API 엔드포인트 구현
- PostModule: 모듈 구성 및 의존성 등록
- class-validator, class-transformer 추가
- API 테스트용 test-api.http 파일 생성

학습 문서:
- docs/nestjs-fundamentals.md 추가 (IoC, DI, 아키텍처 상세 설명)

Monorepo 개선:
- api 패키지에 dev 스크립트 추가
- 패키지별 의존성 분리 전략 적용

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@yoonc01 yoonc01 self-assigned this Dec 8, 2025
@yoonc01 yoonc01 added the enhancement New feature or request label Dec 8, 2025
@yoonc01 yoonc01 merged commit 4c08855 into main Dec 8, 2025
@yoonc01 yoonc01 changed the title feat: NestJS Post CRUD API 구현 및 핵심 개념 학습 feat: NestJS 핵심 개념 학습 Dec 8, 2025
@yoonc01 yoonc01 deleted the nestjs-post-crud-implementation branch December 11, 2025 10:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants