Nest를 배우다 되면 자주 접하는 말들이 있다.
바로 DI와 IOC다. 이 두 개는 자바 스프링과 Angular 등의 프레임워크에도 자주 나오고 중요한 개념이기에 알아보고자 한다.
DI (Dependency Injection)
DI는 Dependency Injection, 의존성주입이라는 뜻이다.
의존성 주입이 뭐지? 잉?이라고 생각할 수 있다.
DI를 처음 접했을 땐 Import와 무엇이 다른지도 모를 정도로 무지했다.(부끄..)
그리고 사실 Nest로 프로젝트를 진행하면서 DI에 대해 완벽히 이해하고 짜진 않았다.(시간의 압박..)
위키피디아에 나와있는 5살에게 DI 설명하기를 한 번 보자
<5살에게 DI 설명하기>
너가 스스로 냉장고로 가서 무언가를 꺼내오려고 할 때 문을 닫지 않고 열어두거나
엄마나 아빠가 너가 안먹었으면 좋겠다고 생각하는 것들을 갖게 되는 문제가 생길 수 있어.
아니면 유통기한이 지난 것이나 갖고 있지 않는 것을 찾을 수 있지.
그럴 때 너가 해야할 일은 "점심 먹을 때 마실 게 필요해요"라고 우리에게 말한다면
너가 점심을 먹으려고 할 때 우유를 가져다 줄게. - 위키피디아 -
즉, DI는 우리가 어떤 것을 필요로 할 때 필요하다고 말(선언)을 하면 프로그램은 그것을 가져다주는 것으로,
프로그램에게 어떠한 객체를 사용하겠다는 요청을 통해 객체 간의 의존성을 낮출 수 있는 것이다.
1. 의존성이 줄어든다.
2. 재사용성이 높은 코드가 된다.
3. 테스트하기 좋은 코드가 된다.
4. 가독성이 높아진다.
Nest에서는 @Injectable 등의 데코레이터를 통해 DI를 할 수 있다.
@Injectable()
export class ArticlesService {
constructor(
@InjectRepository(ArticleRepository)
private articleRepository: ArticleRepository,
(...생략)
마지막으로 DI의 목적을 더 잘 이해하기 위해선 소프트웨어 설계의 'SOLID 법칙'을 알면 더욱 좋다.
1. 단일 책임 원칙(SRP; Single Responsibility Principle)
객체는 단 하나의 책임만을 가져야한다.
어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
어느 한 객체가 책임을 많이 진다는 것은 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합될 가능성이 높아지는 것이다.
이는 곧 변경이 일어날 때 영향범위가 커지게 되는 것으로 나쁜 설계로 볼 수 있다.
설계할 때의 기본 원칙은 응집도는 높이고 결합도는 낮추는 것으로 세우는 것이 좋다. 관련된 것들을 한 곳에 두어 응집도를 높이면 결합도는 낮아진다.
2. 개방-폐쇄 원칙(OCP; Open-Closed Principle)
기존의 코드를 변경하지 않으면서(closed) 기능을 추가(open)할 수 있어야 한다.
소프트웨어 엔티티가 확장에 대해서는 개방(open)되어야 하지만, 변경에 대해서는 폐쇄(closed)되어야 한다.
클래스 자체를 변경하지 않고도(closed) 그 클래스를 둘러싼 환경을 바꿀 수 있어야 한다.
OCP를 위반하지 않는 설계를 위해서는,무엇이 변하는 것인지, 무엇이 변하지 않는 것인지를 구분해야 한다.
변해야 하는 것은 쉽게 변할 수 있게 하고, 변하지 않아야 할 것은 변하는 것에 의해 영향을 받지 않게 해야 한다.
3. 리스코프 치환 원칙(LSP; Liskov Substitution Principle)
부모 클래스와 자식 클래스 사이의 행위가 일관성이 있어야 한다.
부모 클래의 인스턴스를 자식 클래스의 인스턴스로 대체해도 프로그램의 의미는 변화되지 않는다.
자식클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다는 뜻으로, 가장 직접적이고 직관적인 방법은
부모 클래스에서 상속받은 메소드들이 자식클래스에 오버라이드(재정의) 되지 않도록 하면 된다.
피터 코드의 상속 규칙이라는 것이 있는데, 상속의 오용을 막기 위해 상속을 사용해서 안되는 5가지 규칙을 만들었는데
그 중 "자식 클래스가 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행해야 한다"라는 원칙이 있는데,
이것이 바로 LSP를 만족시키는 방법이다.
4. 인터페이스 분리 원칙(ISP; Interface Segregation Principle)
인터페이스를 클라이언트에 특화되도록 분리시켜라.
클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안된다.
클라이언트와 무관하게 발생한 변화로 클라이언트 자신이 영향을 받지 않으려면, 범용(general)의 인터페이스보다는
특화된 인터페이스를 사용해야 한다. ISP는 즉, 인터페이스를 클라이언트에 특화되도록 분리시키라는 의미이다.
5. 의존 역전 원칙(DIP; Dependency Inversion Principle)
의존 관계를 맺을 때, 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라.
자주 변경되는 구체 클래스 대신 인터페이스나 추상 클래스에 의존하라.
- 변하기 어려운 것
정책, 전략과 같은 어떤 큰 흐름이나 개념 같은 추상적인 것
- 변하기 쉬운 것
구체적인 방식, 사물 등
DIP를 만족하면, 의존성 주입(Dependency Injection)을 통해 변화를 쉽게 수용할 수 있는 코드를 작성할 수 있다.
의존성 주입을 이용하면, DIP를 만족하는 변화에 유연한 시스템이 된다.
- 만약 어떤 클래스에서 상속받아야 한다면, 부모 클래스를 추상 클래스로 만든다.
- 어떤 클래스의 참조(reference)를 가져야 한다면, 참조 대상이 되는 클래스를 추상 클래스로 만든다.
- 어떤 메소드를 호출해야 한다면, 호출되는 메소드를 추상 메소드로 만든다.
Service에 필요한 Method들을 Repository에 연결하여 구현하며, Injectable 데코레이터를 통해 Singleton의 Dependency가 생긴다.
@Injectable()
export class LikesService {
constructor(
@InjectRepository(LikesRepository)
private likesRepository: LikesRepository,
@InjectRepository(ArticleRepository)
private articleRepository: ArticleRepository,
) {}
async likeUnlike(likesDto: LikesDto): Promise<any> {
...
}
Controller 소스에서 LikesService를 likesService에 주입하여 내부 메소드를 사용할 수 있게된다.
@Controller('likes')
export class LikesController {
// 의존성(Dependency)
constructor(private likesService: LikesService) {}
@Post()
@UseGuards(AuthGuard)
likeUnlike(
@Body() likesDto: LikesDto,
): Promise<object> {
return this.likesService.likeUnlike(likesDto);
}
}
(...생략)
constructor(
private articlesService: ArticlesService,
(...생략)
IoC (Inversion of Control)
' Don't call us, we'll call you '
(우리한테 연락하지마세요. 우리가 당신에게 연락할게요.)
IoC는 Inversion of Control(제어반전)이라는 의미로,
프로그래머가 작성한 프로그램이 재사용 라이브러리의 흐름 제어를 받게 되는 소프트웨어 디자인 패턴을 말한다.(위키피디아)
IoC = IoC Container는 객체를 관리하고 객체의 생성을 책임지고, 의존성을 관리하는 컨테이너로 아주 훌륭한 친구라고 할 수 있다.
즉, 우리가 해야 할 일들을 프로그램이 대신하게 만들어주는 것이다.
---해당 내용은 계속 공부하며 보완할 예정!---
끗!
Reference