배경
프리코스 미션을 구현하면서 클래스에서 다른 클래스의 인스턴스를 생성하고 사용하는 방법에 대해 고민하게 되었다. 우선 두 가지 방법으로 구현하게 되었는데, 둘 다 찝찝한 구석이 있어 좋은 방법이 없는지 궁금해졌다.
- 생성자에서는 초기화만 하고, init 메서드에서 값을 넣어준다. (Calculator) → 이거 init 으로 계속 초기화되는데 사용할 일이 있나?
- 클래스 내부에서 그냥 생성한다. (RacingGame) → run 실행될때마다 계속 인스턴스 만드는건데 비효율적인듯. 그런데 생성자에서는 아직 입력값을 받기 전이라 인스턴스를 만들 수 없다.
class Calculator {
customSeparator;
numbers;
constructor() {
this.customSeparator = [];
this.numbers = "";
}
init(customSeparator, numbers) {
this.customSeparator = customSeparator;
this.numbers = this.splitNumbers(numbers);
}
/* ... */
}
class App {
constructor() {
this.calculator = new Calculator();
}
async run() {
/* ... */
this.calculator.init(customSeparator, numbers);
/* ... */
}
}
class RacingGame {
repeatCount;
cars;
constructor(repeatCount, carNames) {
this.repeatCount = repeatCount;
this.cars = RacingGame.formatNamesToCars(carNames);
}
/* ... */
}
class App {
/* ... */
async run() {
/* ... */
const game = new RacingGame(repeatCount, cars);
game.start();
}
}
그래서 찾아낸 개념이...
의존성과 의존성 관계 주입(Dependency Injection, DI)
의존성이란
- A가 B를 사용하고 있는 경우, A가 B에 의존성이 있다고 표현
- 즉 B가 변경되면 A에 영향을 미친다는 의미
- 소비자(A)가 생산자(B)에게 의존성이 있다.
의존성 관계 주입(DI)이란
- 의존성이 강할 때 나타나는 문제점을 해결하기 위해서 의존성 주입을 사용
- 외부에서 생성된 객체를 인터페이스를 통해 넘겨받는다.
- 결합도를 낮출 수 있다.
- 런타임 시에 의존 관계가 결정되기 때문에 유연한 구조를 가진다. → 느슨한 결합
- DI 패턴의 종류
- 생성자 주입
- 수정자 주입
내가 짠 코드는 무엇이었나
메인로직 - 뷰 간의 의존성은 차치하고, App 에서부터 둘 모두 DI하지 않고 있었다. 직접적으로 클래스 내부에서 인스턴스를 생성하여 연결되었기 때문.
제어 역전 (IoC, Inversion of Control)
개발자가 직접 제어하는 것이 아니라, 외부의 프레임워크나 라이브러리가 제어 흐름을 대신하게 되는 것을 말한다.
// 일반 흐름 (개발자가 제어)
class App {
constructor() {
this.calculator = new Calculator();
}
}
// 제어 역전 (사용하는 측에서 제어)
const app = new App(new Calculator());
IoC 구현방법
- Factory Pattern
- Template Method Pattern
- Service Locator Pattern
- Dependency Injection (DI)
즉 DI는 제어 역전(IoC)을 위한 한가지 방법이다.
DI 패턴을 통해 IoC 구현하기
이전의 App - 메인로직 에서 강한 결합을 가지고 있던 두 코드를 DI 패턴으로 수정해 보자.
생성자 주입
class App {
game;
constructor(input, game) {
this.game = game;
}
async run() {
/* ... */
game.setGame(repeatCount, cars);
game.start();
}
}
const app = new App(new RacingGame()); // 클래스 외부에서 인스턴스 생성
app.run();
- 생성자 주입의 경우 App 객체는 받는 인스턴스가 무엇인지 모른다.
- 즉 RacingGame 이 아닌 뜬금없는 객체가 들어와도 아무 의심 없이 동작한다.
수정자 주입
class App {
calculator;
setCalc(calculator) {
this.calculator = calculator;
}
async run() {
/* ... */
}
}
const app = new App();
const calculator = new Calculator(); // 클래스 외부에서 인스턴스 생성
app.setCalc(calculator);
app.run();
- 수정자 주입의 경우 OOP의 5가지 개발 원칙 중 OCP(Open-Closed Principal, 개방-폐쇄 원칙)를 위반하게 된다.
"소프트웨어 엔티티는 확장을 위해 열려야 하지만 수정을 위해 닫혀 있어야 한다"
+) 의존 역전 원칙 (DIP, Dependency Inversion Principle)
"구체화(concretion)가 아닌 추상화(abstractions)에 의존해야 한다"
한 모듈이 다른 모듈에 직접적으로 의존해서는 안 되며, 둘 다 공통된 추상화에 의존해야 한다.
class Game {
run() {
throw new Error("");
}
}
class RacingGame extends Game {
run() {
// 게임 시작하기
}
}
class App {
game;
constructor(game) {
this.game = game;
}
async run() {
/* ... */
}
}
const app = new App(new RacingGame())
app.run();