처음부터 차근차근

[NestJS] 제어 역전과 의존성 주입 본문

FrameWork/NestJS

[NestJS] 제어 역전과 의존성 주입

HangJu_95 2023. 11. 29. 00:19
728x90

Spring, NestJS 등 객체 지향 프레임워크를 사용하면 항상 나오는 제어 역전과 의존성 주입은 어떻게 동작하는 것이고, 왜 중요한 것일까요?

NestJS와 Spring의 IoC는 비슷한 것 같으니, NestJS를 통해 정리해보겠습니다.

1. Inverse of Control

제어 역전을 한 마디로 표현해보겠습니다.

나 대신 프레임워크가 코드를 동작시키고 제어한다.

입니다.

단순한 Javascript 예시를 통해 알아보겠습니다.

// 객체를 생성하는 Class
class IoCExample {
  print() {
    console.log("hello world");
  }
}
// 객체 생성 (개발자가 직접 코드를 구현)
const object = new IoCExample();
// 생성한 객체의 메소드를 호출 (개발자가 직접 코드를 구현)
object.print();

해당 코드는 IoCExample이라는 클래스를 생성하고, 이를 통해 인스턴스를 생성하고 메서드를 호출하는 코드입니다.

여기서는 개발자가 직접 인스턴스를 생성하고, 이러한 메서드를 호출하고 있습니다.

즉, 코드를 동작시키는 주체는 개발자, 즉 본인입니다.

 

이제 NestJS 공식 문서에 있는 코드를 통해 살펴보겠습니다..

// cats.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}
// cats.provider.ts
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

@Get의 findAll을 한번 살펴보겠습니다.

만약 우리가 직접 코드를 동작시킨다면, new 메서드를 통해 CatsService의 인스턴스를 생성하고, 이를 통해 findAll 메서드를 실행해야 합니다.

그러나, NestJS Application은 Get 요청이 온다면, 이를 알아서 Provider의 findAll 메서드를 실행시켜주고, 해당하는 객체를 반환합니다.

즉, Nest FrameWork로 인해 기존에 개발자가 스스로 처리하는 방식에서, Framework가 스스로 알아서 처리를 진행해줍니다.

 

그리고, 첫번째 예시에서는 개발자가 스스로 객체를 인스턴스화 시키고 메소드를 호출했습니다.

하지만, 두번째 예시에서는 new를 통해 인스턴스화 시키는 것이 보이지 않습니다.

 

이는 NestJS의 IoC Container를 통해 CatsService 객체를 생성 및 다른 객체에 주입시켜주고, 생성한 인스턴스의 생명주기를 관리하며 제어 흐름을 관리해주기 때문에 그렇습니다.

 

즉, 제어의 역전이란 말 그대로 역으로 제어하는 것을 의미하며, 다음과 같습니다.

  • 기존 = 구현 객체 스스로, 개발자가
  • 역전된 주체 = 외부 조립기, NestJS에서는 IoC Container
  • 무엇을 제어 = 구현한 객체(CatsService의 인스턴스)의 생성 및 연결, 생명주기 관리, 제어 흐름에 대한 권한

추가로 IoC Container는 이런 역할을 맡습니다.

  • IoC Container는 Provider를 등록하고 관리하는 객체이다.
  • Provider는 NestJS의 Life Cycle과 동기화된 Scope를 가지며, 프로그램이 시작될 때 모든 종속성을 처리한다.
  • Provider는 의존성 주입을 통해 다른 클래스와 관계를 맺을 수 있는데, IoC Container는 Provider의 metadata를 분석하여 의존성 그래프를 생성한다.
  • IoC Container는 의존성 그래프에 따라 필요한 Provider를 인스턴스화하고 주입하며, 이 과정에서 @Injectable 데코레이터가 사용된다. 또한 인스턴스화된 Provider를 저장하고, 참조할 수 있게 해주는데, 이때는 @Inject 데코레이터가 사용된다.
즉, IoC Container는 Provider를 컨테이너에 등록하고, 필요할 때마다 Provider 인스턴스를 생성하여 생성과 관리를 개발자가 아닌 Framework가 수행하는 역할을 하게 만듭니다.

2. Dependency Injection 의존성 주입

의존성 주입을 Wikipidia에서 찾아봤습니다.

소프트웨어 엔지니어링에서 의존성 주입(dependency injection)은 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉이다. "의존성"은 예를 들어 서비스로 사용할 수 있는 객체이다. 클라이언트가 어떤 서비스를 사용할 것인지 지정하는 대신, 클라이언트에게 무슨 서비스를 사용할 것인지를 말해주는 것이다. "주입"은 의존성(서비스)을 사용하려는 객체(클라이언트)로 전달하는 것을 의미한다.

 

간단하게 말하자면, "너가 필요한 클래스 등을 너 대신 내가 관리할게!"라는 의미입니다.

 

두번째 코드를 다시 보자면, Controller에 필요한 클래스, 객체를 Service에서 대신 하고 있습니다.

이는 Controller의 객체에 Service 객체의 의존성 주입을 통해 사용하려는 객체를 전달하는 것입니다.

 

왜 그렇다면 의존성 주입이라는 용어가 만들어졌을까요? 간단한 예시를 통해 알아보겠습니다.

// List 전체 조회
router.get('', authMiddleware, async (req, res) => {
  console.log('List 전체 조회 API에 접속했습니다.');
  try {
    const { userId } = res.locals.user;
    // 1. page, pageSize 받기 via req.query
    const page = parseInt(req.query.page) || 1;
    const pageSize = parseInt(req.query.pageSize) || 10;
    // 2. ToDolist 총 데이터 항목 수(totalDatas)
    const totalDatas = await Lists.count({ where: { userId }, raw: true });
    // 3. 전체 페이지 수(totalPages)
    const totalPages = Math.ceil(totalDatas / pageSize);

    // 3-1 totalData가 0인 경우
    if (totalDatas === 0) {
      return res.status(200).json({
        lists: [],
        totalDatas,
        totalPages,
      });
    }

    // 3-2 totalPage수 보다 큰 page 수를 입력한 경우
    if (page > totalPages)
      return res.status(404).json({ errorMessage: '없는 페이지입니다.' });

    // 4. sequelize를 통해 전체 조회(LIMIT, OFFSET 적용)
    const items = await Lists.findAll({
      // 5. 데이터 손질
      where: { userId: userId },
      attributes: { exclude: ['userId', 'content'] },
      limit: pageSize,
      offset: (page - 1) * pageSize,
      order: [['listId', 'DESC']],
    });
    // 6. response로 보내기
    res.status(200).json({
      lists: items,
      totalDatas,
      totalPages,
    });
  } catch (err) {
    console.log(err);
    res.status(400).json({ errorMessage: '리스트 조회에 실패하였습니다.' });
  }
});

미니프로젝트를 통해 함수형 router를 만들었던 시간이었습니다.

단 하나의 route에 많은 로직이 들어가 있습니다. 현재는 간단한 코드이지만, 기능을 추가하다보면 엄청난 양의 코드가 될 것 입니다.

만약 코드줄이 100줄이 넘어간다면, 누군가 유지보수를 할 때 유지보수할 로직을 찾기가 굉장히 힘들 것입니다.

또한 동일한 로직이 다른 router에도 있다면, 그 router도 수정해야됩니다.

 

역할을 분담하기 위해 계층형 아키텍처 패턴이 등장하게 되었고, Class를 통해 해당하는 router를 구현하는 방법이 등장하였습니다. (계층형 아키텍처 패턴은 나중에 다룹니다.)

이제 NestJS의 예제를 다시 한번 보겠습니다.

// cats.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}
// cats.provider.ts
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  findAll(): Cat[] {
    return this.cats;
  }
}
  1. Get() handler를 호출하면, findAll() 메서드는 내부의 CatsService의 findAll() 메서드를 호출합니다.
  2. CatsService의 findAll() 메서드는 Cats[]를 반환합니다.
  3. 이것을 response로 반환합니다.

즉, CatsController 클래스는 외부의 CatsService에 의존하고 있습니다.

이때 Constructor를 통해 해당하는 CatsService 객체의 인스턴스 변수에 주입하고 있습니다.

 

만약, Get() findAll() 에서 cats를 찾아오는 로직을 변경하고 싶다면, CatsService만 변경하면 됩니다.

이는 CatsController 대상 객체를 변경하지 않고도 외부에서 대상 객체의 외부 의존 객체를 변경, 수정할 수 있게 해줍니다.

이를 통해 Controller의 역할은 HTTP 메서드에 응답 요청, Service의 역할은 비즈니스 로직을 맡게 되었으며, 비즈니스 로직만 수정해야 할 경우에는 Service단만 수정하면 됩니다.

만약 다른 Service를 연결해야 하는 경우에, CatsService -> 원하는 Service를 변경하여 의존성을 주입하면 됩니다.

 

즉, 의존성 주입이란 말 그대로 클래스 외부에서 의존되는 것을 대상객체의 인스턴스 변수에 주입하는 기술입니다.

이는 대상 객체를 변경하지 않고도 외부에서 대상 객체의 외부 의존 객체를 바꿀 수 있습니다.

참조

https://dodokwon.tistory.com/17

https://www.wisewiredbooks.com/nestjs/overview/04-provider.html

https://docs.nestjs.com/providers

'FrameWork > NestJS' 카테고리의 다른 글

[NestJS] GraphQL Resolvers  (0) 2023.11.29
[NestJS] NestJS에서 GraphQL 초기 설정  (1) 2023.11.29
[NestJS] Custom decorator  (0) 2023.11.28
[NestJS] Exception filter  (0) 2023.11.28
[NestJS] Pipe  (0) 2023.11.28