의존성 주입을 JS로 이해해보기

ss_kim·2025년 1월 10일

객체 지향 프로그래밍(OOP)

먼저 작은 문제들을 해결할 수 있는 객체들을 만든 뒤, 이 객체들을 조합해서 큰 문제를 해결하는 상향식(Bottom-up) 해결법

가장 큰 특징 3가지는 아래와 같다.

  1. 은닉성
    외부로의 노출을 최소화하여 모듈 간의 결합도를 떨어뜨려 유연함과 유지 보수성을 높임

  2. 상속
    자식 클래스가 부모 클래스의 특성과 기능을 그대로 물려받는 것을 말하며, 오버라이딩으로 수정 가능

  3. 다형성
    객체 간 결합력을 낮추면 다양한 기능을 분리하고 연결하는 것이 용이함

이 중 다형성과 관련하여 의존성 주입에 대해 알아보자.


의존성 주입(Dependency Injection)

객체가 필요로 하는 어떤한 기능이나 데이터를 외부에서 생성한 객체(의존성)로 전달하는 것

의존성 주입 방법

  • constructor 주입
    클래스가 생성될 때 1회 호출

     // camera.js
     class Camera {
       #lens;
    
       constructor() {
         this.#lens = new Lens();
       }
     }
     
     const camera = new Camera();
  • setter 주입
    setter 함수를 통해서 주입

     // camera.js
     class Camera {
       #lens;
    
       setLens(lens) {
        this.#lens = lens;
       }
     }
     
     camera.setLens(new Lens());
     camera.setLens(new Lens());

등등이 있다.
비유를 들자면, 내가 사용하고 싶은 객체(카메라)의 추가 기능(줌인)을 위해서 외부에서 생성한 객체(렌즈)를 연결하고 싶다. constructor 주입은 카메라가 만들어질 때 렌즈가 내장되어 있는 것이고, setter는 여러 렌즈를 갈아 끼우는 것으로 간단히 비유할 수 있다.

이 때, 메서드 오버라이딩으로 의존성 객체에 기능을 구현할 수 있다.

class Lens {
  zoomIn(level) {
    console.log(`Zoomed in by ${level} levels.`);
  }
}

class Camera {
  #lens;

  constructor() {
    this.#lens = new Lens();
  }

  setLens(lens) {
    this.#lens = lens;
  }

  zoomIn(level) {
    this.#lens.zoomIn(level);
  }
}

camera.zoomIn(2); // "Zoomed in by 2 levels."

JS의 취약점

그런데 여기서 JS의 취약점이 드러난다. 타입 검사를 실행하지 않기 때문에 Lens 클래스가 아니더라도 오류가 발생하지 않고 주입이 되며, zoomIn이라는 기능을 가진 아무 객체를 전달하면 메서드가 실행이 된다.

camera.setLens({
  zoomIn: () => {
   console.log('fake');
  },
});

symbol 생성자를 이용한 메서드 오버라이딩으로 메서드 호출은 방지할 수 있지만 주입하는 것을 원천적으로 방지하는 것은 어렵기 때문에 Typescript를 사용하는 것이 좋다.

const ILens = {
  zoomIn: Symbol(),
  zoomOut: Symbol(),
};

class Lens {
  [ILens.zoomIn](level) {
    console.log(`Zoomed in by ${level} levels.`);
  }
}

class Camera {
  #lens;

  constructor() {
    this.#lens = new Lens();
  }

  setLens(lens) {
    this.#lens = lens;
  }

  zoomIn(level) {
    this.#lens.[ILens.zoomIn](level);
  }
}

interface와 implements

Typescript의 interface는 데이터의 기능을 정의할 때 쓰는 type이다.

interface는 접점 즉, 어떤 객체 간 연결점이다. 어떤 기능을 가진 객체를 연결하고자 할 때, 정해진 형식을 미리 정해놓고 연결할 객체(의존성)는 형식을 따라서 기능을 구현한 뒤 주입하는 것

interface ILens {
  zoomIn(level: number): void;
  zoomOut(level: number): void;
}

class Lens implements ILens {
  zoomIn(level: number): void {
    console.log(`zoom in`);
  }

  zoomOut(level: number): void {
    console.log(`zoom out `);
  }
}


정리

  • OOP의 특징
    은닉성, 상속, 다형성

  • 의존성 주입의 장점
    객체를 재결합하고 재사용하기가 쉽다.
    객체 간 결합도가 낮아 유지보수가 용이하다.

profile
프론트엔드 개발자

0개의 댓글