본문 바로가기

데일리

[23.10.29] nodejs, DI, Ioc, nestjs

글의 목적

  • nodejs에서 DI(Dependency Injection)를 더 잘 이해하고 nestjs에서 DI, Ioc의 효율성에 대해서 정리하기 위함.

본론

 1. Dependency (의존성)

  • Dependency(의존성)란 용어가 좀 어렵게 느껴지실수도 있지만 expressjs나 nestjs로 간단한 todolist 사이드프로젝트를 진행했던 독자라면 모두 의존성을 가진 객체를 사용했을거라고 생각한다.
  • 아마도 controller가 service에 의존하고 있었을거다.
  • 혹시 의존이라는 단어가 어렵다면 controller를 사용하기 위해서는 service가 꼭 필요하다라고 생각하자!
  • Dependency에 대해 다시 정의를 내려보자면 클래스나, 함수, 즉 어떠한 A객체(js는 다 객체다)를 사용하기 위해서 B라는 객체를 참조한것을 말한다.
  • 우리는 여기서 "참조"에 주목할거다.
  • function
// usersService.js
const usersRepository = {
  async getUserById(userId) {
    const user = { name: "test", age: 20 };
    if (userId) {
      return user;
    }
  },
};

const usersService = {
  async getUser(userId) {
    return usersRepository.getUserById(userId);
  },
};

module.exports = usersService;

usersService
  .getUser(1)
  .then((user) => {
    console.log(user); // { name: 'test', age: 20 }
  })
  .catch((error) => {
    console.error(error);
  });
  • class
// usersService.js
class UsersRepository {
  async getUserById(userId) {
    const user = { name: "test", age: 20 };
    if (userId) {
      return user;
    }
  }
}

class UsersService {
  async getUser(userId) {
    const userRepository = new UsersRepository();
    return userRepository.getUserById(userId);
  }
}

module.exports = UsersService;

const usersService = new UsersService();

usersService
  .getUser(1)
  .then((user) => {
    console.log(user); // { name: 'test', age: 20 }
  })
  .catch((error) => {
    console.error(error);
  });
  • 확실히 직접 불러와서 사용하는것은 function이 편해보인다! 하지만
  • 여기서 중요한건 둘의 차이가 아니라 usersService를 직접 불러와서 사용하고 있다는점이다.
  • 이게 왜 문제일까? 한번 계속 고민을 가져보길 바란다. 물론 해결해줄거다!
  • class는 class에 대해서 조금이라도 알고 있다면 너무 이상하게 느껴질거지만... 강한결합을 보여주기위한 예시라고 생각하자!

 2.  Reference(참조)

  • 어떠한 객체가 다른 객체를 "참조"하고 있다면 그것은 두 객체가 "결합"되어 있다고 말해도 괜찮을 거다.
  • 그리고 위에 코드에서 처럼 직접 호출하여 사용한다는것은 "강하게 결합"되어 있다고 말할 수 있다.
  • 여기서 "강한 결합"이란 좋다고 느껴질수도 있지만 아키텍처 측면에서 생각했을 때, 테스트코드를 작성해야한다고 생각했을 때, "강한 결합은" 그렇게 좋은 결합이 아니다.
  • 그렇다면 이러한 결합도를 조금은 낮출수 있다면 더 좋지 않을까?

 3. Coupling(결합)

  • 결합에는 강한결합(Tight Coupling)과 느슨한결합(Loose Coupling)이 있다. 
  • 테스트코드에는 느슨한결합이 압도적으로 좋다.
    • 이것과 관련된 글은 너무 많기 때문에 따로 설명을 하진 않겠다. 
  • 근데 강한결합이 왜 안좋을까? 우린 여기에 초점을 맞추려고 한다.
  • 강하게 결합된 코드를 다시 살펴보면
class UsersRepository {
  async getUserById(userId) {
    const user = { name: "test", age: 20 };
    if (userId) {
      return user;
    }
  }
}

class UsersService {
  async getUser(userId) {
    const userRepository = new UsersRepository();
    return userRepository.getUserById(userId);
  }
}

module.exports = UsersService;

const usersService = new UsersService();

usersService
  .getUser(1)
  .then((user) => {
    console.log(user);
  })
  .catch((error) => {
    console.error(error);
  });
  • 지금은 괜찮아 보일수도 있지만 서비스가 커져서 다른 db를 사용하거나 여러 db를 분리해서 메모리와 서비스에 맞게 db를 사용한다고 하면 Repository만 고치기 보다는 service코드까지 수정을 해야할거다
  • 왜냐면 직접 Repository를 불러서 사용하고 있기 때문!
  • 하지만 추상화를 높이고 의존성을 주입하는 방식으로 코드를 수정해서 느슨한 결합을 만들어준다면?
  • 코드는 길어질수 있지만 SRP단일책임원칙을 지킬수 있게 되고
  • 이말은 결과적으로 service코드 자체도 mock작업을 하거나 통합테스트를 한다거나 하는것이 아니라 단위테스트를 가능하게 한다는 의미일것이다.
class UserRepositoryInterface {
  async getUserById(userId) {
    const user = { name: "test", age: 20 };
    if (userId) {
      return user;
    }
  }
}

class MysqlUsersRepository extends UserRepositoryInterface {
  // MySQL 데이터베이스에서 사용자 데이터를 가져올 수 있는 코드를 추가할 수 있음
}

class MongoUsersRepository extends UserRepositoryInterface {
  // MongoDB에서 사용자 데이터를 가져올 수 있는 코드를 추가할 수 있음
}

class UsersService {
  constructor(usersRepository) {
    this.usersRepository = usersRepository;
  }

  async getUser(userId) {
    return this.usersRepository.getUserById(userId);
  }
}

const mysqlRepository = new MysqlUsersRepository();
const mongoRepository = new MongoUsersRepository();

const mysqlService = new UsersService(mysqlRepository);
const mongoService = new UsersService(mongoRepository);

mysqlService.getUser(1).then((user) => {
  console.log("MySQL User:", user);
});

mongoService.getUser(1).then((user) => {
  console.log("MongoDB User:", user);
});
  • UsersService코드가 변경되었는가?

 4. Dependency Injection(의존성 주입)

  • 의존성 주입이란 결국 강한 결합들을 느슨한 결합으로 풀어주는 방법이다.
  • 근데 의존성 주입도 단점보단 장점이 훨씬 많지만 단점이 없는것은 아니다. 대표적으로 dependency를 위한 코드가 많이 생성이된다는것이고 이를 관리하는것은 조금 까다로운 작업이 될수 있기 때문. 
  • 여기서 IoC, 정확히는 IoC 컨테이너라고 하는 의존성주입을 도와주는 도구가 나오게 된다.
  • 그러면서 의존성주입의 장점만을 유지시켜주는 프레임워크도 등장하게 되고 발전하게 된다.(nestjs, spring)

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

  • 이걸 정확하게 node에서 표현하기는 쉽지가 않다. 따라서 여기서부터는 nestjs로 예시를 대체할거다.
  • nestjs에서는 IoC가 객체의 생명주기를 관리해주기 때문에 개발자는 좀더 비즈니스로직의 집중할 수 있게 해준다.
  • 예를들어보자
  • Service객체와 Repository객체가 있고 S가 R에 의존하고 있다고 해보자
  • 우리는 이 객체를 사용할때 생명주기를 생각하고 사용하는가? 이 생명주기관리를 IoC 컨테이너가 한다고 생각하면 된다. (최근에는 객체의 메모리관리를 가비지 컬렉터가 알아서 한다 ㅎㅎ)
  • 먼저 IoC를 사용하지않은 코드를 한번 봐보자.
export interface UserRepository {
  getUser: () => string;
}

@Injectable()
export class Mysql implements UserRepository {
  getUser() {
    return 'Mysql user'
  }
}

@Injectable()
export class MongoDB implements UserRepository {
  getUser() {
    return 'Mongodb user'
  }
}

class App {
  private db: UserRepository;
  constructor() {
    this.db = new Mysql();
  }
}
  • UserRepository interface를 구현하는 2개의 class Mysql, Mongodb가 있고 각각의 class의 메서드는 구현이 다르게 되어있습니다. 
  • 이 상황에서 App class는 UserRepository 타입의 멤버 변수를 가지고 생성자에서 구현체를 만들고 있음.
  • IoC를 사용한 코드로 수정해보면
class App {
  constructor(@Inject('UserRepository') private db: UserRepository)
}

// module 
@module({
  controllers: [UsersController],
  providers: [
    UsersService,
    {
      provide: 'UserRepository',
      useClass: Mysql
    }
  ]
})

//module
@module({
  controllers: [UsersController],
  providers: [
    UsersService,
    {
      provide: 'UserRepository',
      useClass: Mongodb
    }
  ]
})

결론

  • 뭔가 정리를 하면서도 예시코드들을 만들어내는것이 너무 어려웠다.
  • 뭔가 정리되지 않은 머리속에 개념들을 억지로 정리하는느낌....
  • 그렇지만 확실히 정리가 도움이 되었다.
  • IoC쪽은 nestjs를 많이 사용해서 더 느껴봐야겠다.