개발이 취미인 사람

[Nest.js] 심화 - 제어의 역전 IoC(Inversion of Control)와 의존관계 주입 DI(Dependency Injection) 본문

백앤드(Back-End)/Nest.js

[Nest.js] 심화 - 제어의 역전 IoC(Inversion of Control)와 의존관계 주입 DI(Dependency Injection)

RyanSin 2022. 5. 15. 23:05
반응형

- 개요

이번 시간에는 제어의 역전 그리고 의존관계 주입에 대한 개념과 Nest.js 프레임워크에서 어떻게 사용할 수 있는지에 대해 알아보겠습니다.

 

혹시 Nest.js 프레임워크를 사용하지 않으신 분들은 아래 링크를 통해 학습하고 오시는 걸 추천드리겠습니다.

 

[Nest.js] Nest.js 개념 및 프로젝트 생성

 

[Nest.js] Nest.js 개념 및 프로젝트 생성

- 개요 안녕하세요. 이번 시간에는 Nest.js 개념 및 프로젝트를 생성해 보는 시간을 가져보겠습니다. - Nest.js 개념 Nest.js 프레임워크가 무엇일까요? 저는 처음에 Next.js(React 프레임워크)로 착각했습

any-ting.tistory.com

 

- 기본개념

제어의 역전과 의존관계 주입에 대한 기본 개념을 모르시는 분들은 아래 링크를 통해 학습하고 오시는 걸 추천드리겠습니다.

 

[OOP] 제어의 역전 IoC(Inversion of Control)와 의존관계 주입 DI(Dependency Injection)

 

[OOP] 제어의 역전 IoC(Inversion of Control)와 의존관계 주입 DI(Dependency Injection)

- 개요 안녕하세요. 이번 시간에는 제어의 역전 IoC와 의존관계 주입 DI에 대해 알아보겠습니다. 객체 지향 프로그래밍 공부를 하면 반드시 나오는 하나의 개념입니다. 이해하시는데 도움이 되면

any-ting.tistory.com

 

Nest.js 프레임워크에는 제어의 역전과 의존관계 주입에 대한 기술을 지원합니다.

 

공식 홈페이지에서도 아주 잘 소개하고 있습니다.

 

기초적인 부분을 구현하는 데 있어서는 굳이 알지 않아도 되는 개념일 수 있지만 프로젝트 규모가 점점 커지고 프레임워크에 동작 방식에 대해서 이해하지 못하고 사용한다면... 분명 나중에 문제가 생길 위험이 큽니다!

 

Nest.js 프레임워크에서 제어의 역전과 의존관계 주입을 담당하는 기술은 @Module 데코레이터입니다.

 

@Module 데코레이터는 특정 도메인에 대한 Controller Service를 관리하며 각 Controller와 Service에 필요한 의존관계를 주입합니다.

 

아무리 말을 해도 이해가 가지 않을 겁니다.(저도 실제 예제를 만들고 실행하면서 알았거든요 ㅋㅋ...)

 

소스 코드를 통해 바로 알아보겠습니다. 프로젝트 구조는 아래와 같습니다.

 

프로젝트 구조

게시판 도메인을 생성했습니다. 게시글 정보를 저장하는 방식은 실제 데이터베이스가 아닌 메모리에 저장되는 방식으로 임시로 구현했습니다. (중요한 부분은 저장되는 방식이 아니니깐요!)

 

위 프로젝트에서 핵심은 repository입니다. 우리는 OOP 프로그램에서 SOLID원칙에 따라 추상화에 의존하기 위해 interface를 하나 생성했습니다. (board.repository.interface.ts)

 

그리고 해당 인터페이스를 implements를 통해 disk.repository와 memory.repository에 연결했습니다.

 

board.repository.interface.ts

import { BoardEntity } from 'src/entity/board.entity';

export interface BoardRepositoryInterface {
  save(id: number, name: string): number;
  findAll(): BoardEntity[];
}

 

board-disk.repository.ts

import { Logger } from '@nestjs/common';
import { BoardEntity } from 'src/entity/board.entity';
import { BoardRepositoryInterface } from './interface/board.repository.interface';

export class BoardDiskRepository implements BoardRepositoryInterface {
  private boardList: BoardEntity[] = [];

  /**
   * @author Ryan
   * @description 게시판 등록
   *
   * @param id 게시판 아이디
   * @param name 게시판 이름
   *
   * @returns 게시판 아이디
   */
  save(id: number, name: string): number {
    Logger.log('디스크 DB -> 데이터 저장');

    const board = new BoardEntity();

    board.id = id;
    board.name = name;

    this.boardList.push(board);

    return id;
  }

  /**
   * @author Ryan
   * @description 게시글 전체 조회
   *
   * @returns 게시글 리스트
   */
  findAll(): BoardEntity[] {
    Logger.log('디스크 DB -> 데이터 조회');

    return this.boardList;
  }
}

 

board-memory.repository.ts

import { Logger } from '@nestjs/common';
import { BoardEntity } from 'src/entity/board.entity';
import { BoardRepositoryInterface } from './interface/board.repository.interface';

export class BoardMemoryRepository implements BoardRepositoryInterface {
  private boardList: BoardEntity[] = [];

  /**
   * @author Ryan
   * @description 게시판 등록
   *
   * @param id 게시판 아이디
   * @param name 게시판 이름
   *
   * @returns 게시판 아이디
   */
  save(id: number, name: string): number {
    Logger.log('메모리 DB -> 데이터 저장');

    const board = new BoardEntity();

    board.id = id;
    board.name = name;

    this.boardList.push(board);

    return id;
  }

  /**
   * @author Ryan
   * @description 게시글 전체 조회
   *
   * @returns 게시글 리스트
   */
  findAll(): BoardEntity[] {
    Logger.log('메모리 DB -> 데이터 조회');

    return this.boardList;
  }
}

 

두 소스 코드 차이는 사실 없습니다. 로직은 동일합니다. 하지만 Logger.log 내용을 통해 우리는 의존관계 주입이 어떤 식으로 이뤄지는 확인 하겠습니다.

 

board.module.ts

import { Module } from '@nestjs/common';
import { BoardService } from './board.service';
import { BoardController } from './board.controller';
import { BoardMemoryRepository } from 'src/repository/board-memory.repository';
import { BoardDiskRepository } from 'src/repository/board-disk.repository';

/**
 * @author Ryan
 * @description providers에 정의한 키와 useClass에 정의한 값을 통해 IoC Container에 등록되 사용된다.
 *
 * 중요한 포인트는 providers는 Key, useClass는 Value를 뜻 한다.(useClass는 당연히 Class 정보를 등록한다.)
 * 단순 상수를 등록한다면 useValue를 선언해서 사용한다.
 */
@Module({
  providers: [
    BoardService,
    {
      provide: 'BoardRepository', //Inject() 이름을 설정한다.
      useClass: BoardMemoryRepository, // 메모리 데이터베이스 사용
      // useClass: BoardDiskRepository, // 디스크 데이터베이스 사용
    },
  ],
  controllers: [BoardController],
})
export class BoardModule {}

 

여기서부터 핵심입니다!! Nest.js 프레임워크는 providers안에 클래스 또는 값을 설정하면 Nest.js 내부 IoC Container에 등록이 됩니다

 

우리가 BoardService를 생성하면 자동으로 providers에 등록됩니다. 그렇기 때문에 따로 new 키워드를 통해 BoardService 클래스를 생성하지 않아도 이미 해당 도메인 모듈에 생성이 되어있기 때문에 우리는 호출해서 사용할 수 있는 겁니다. (BoardController에서 바로 사용)

 

providers: [] 안에 특정 키를 생성하지 않고 그냥 등록하게 되면 자동으로 해당 클래스 이름으로 IoC Container에 등록됩니다. 그렇기 때문에 @Controller에서 Service를 호출해도 문제가 없는 겁니다.

 

board.controller.ts

import { Body, Controller, Get, Post } from '@nestjs/common';
import { BoardService } from './board.service';
import { CreateBoardDto } from './dto/create-board.dto';

@Controller('board')
export class BoardController {
  //BoardSerive 호출
  constructor(private readonly boardService: BoardService) {}
  
}

 

그럼 아래와 같이 선언한 클래스는 어떻게 호출할 수 있을까요?

{
  provide: 'BoardRepository', //Inject() 이름을 설정한다.
  useClass: BoardMemoryRepository, // 메모리 데이터베이스 사용
  // useClass: BoardDiskRepository, // 디스크 데이터베이스 사용
},

 

@Service 코드를 확인하면 한눈에 알 수 있습니다.

import { Inject, Injectable } from '@nestjs/common';
import { BoardRepositoryInterface } from 'src/repository/interface/board.repository.interface';
import { CreateBoardDto } from './dto/create-board.dto';

@Injectable()
export class BoardService {
  constructor(
    //board.module에서 선언한 provide Inject 값을 가져온다.
    @Inject('BoardRepository')
    private boardRepository: BoardRepositoryInterface,
  ) {}

  /**
   * @author Ryan
   * @description 게시글 등록
   *
   * @param createBoardDto 게시글 등록 DTO
   * @returns 게시글 고유 아이디
   */
  createBoard(createBoardDto: CreateBoardDto): number {
    const { id, name } = createBoardDto;

    const boardId = this.boardRepository.save(id, name);

    return boardId;
  }

  /**
   * @author Ryan
   * @description 게시글 전체 조회
   *
   * @returns 게시글 정보 리스트
   */
  getBoardList() {
    return this.boardRepository.findAll();
  }
}

@Inject 데코레이터에서 BoardRepository라는 키 값을 통해 우리가 IoC에 등록한 Repository를 가져왔습니다.

 

계속 IoC에 대한 설명만 한 것 같아요... 이제 대만에 의존관계 주입에 대해 설명하겠습니다.

 

지금 프로젝트는 의존관계 주입을 예상하고 만들었습니다.

 

의존 관계 주입을 할 때 두 가지 방식이 있습니다. 생성자 주입 방식과 속성 주입 방식이 있습니다.

 

방식보다는 의존 관계 주입 개념에 대해 알아야 합니다. 그래야 우리는 프레임워크가 컴파일 단계에서 클래스를 생성하고 실제 싱글톤에 등록해서 사용됩니다.

 

@Module 데코레이터 안을 확인해보면 구현 클래스를 import 해서 설정하고 있습니다.

BoardRepository 키를 설정하고 useClass 사용될 클래스를 설정합니다. (이 부분이 의존 관계 주입입니다.)

@Module({
  providers: [
    BoardService,
    {
      provide: 'BoardRepository', //Inject() 이름을 설정한다.
      useClass: BoardMemoryRepository, // 메모리 데이터베이스 사용
      // useClass: BoardDiskRepository, // 디스크 데이터베이스 사용
    },
  ],
  controllers: [BoardController],
})

 

위에서 구현체 클래스를 주입했습니다. 그럼 서비스에서는 어떤 식으로 알 수 있을까요?

 

사실 BoardService에는 어떤 구현 클래스가 주입되는지 알지 않아도 됩니다.


BoardService는 구현 클래스에 의존하지 않고 추상화인 인터페이스에 의존하고 있기 때문이죠.

@Injectable()
export class BoardService {
  constructor(
    //board.module에서 선언한 provide Inject 값을 가져온다.
    @Inject('BoardRepository')
    private boardRepository: BoardRepositoryInterface,
  ) {}
}

 

이런 식으로 소스 코드를 작성하면 우리는 BoardService 코드를 수정하지 않고 @Module에서 우리가 필요한 구현체를 주입해 비즈니스 코드를 수정하지 않고 기존 로직을 동일하게 사용할 수 있습니다.

 

실제 소스 코드를 실행하면 아래와 같이 실행이 됩니다.

 

실행 결과

실행 결과를 확인해보면 비즈니스 로직은 변경하지 않고 실제 @Module에서 의존관계 주입 구현체를 수정해서 구현한 걸 확인할 수 있습니다.

 

이번 시간에는 Nest.js 프레임워크에 제어의 역전(IoC)과 의존 관계 주입(DI)에 대해 알아봤습니다.

 

궁금한 부분은 댓글을 남겨주세요.

 

소스 코드

GitHub: https://github.com/Ryan-Sin/adavnced-nest/tree/IoC_DI

 

GitHub - Ryan-Sin/adavnced-nest

Contribute to Ryan-Sin/adavnced-nest development by creating an account on GitHub.

github.com