[Javascript] 객체 지향의 의존성

지난 시간 객체 지향의 핵심에 대해서 알아보았는데, 더 나아가 의존성에 대해서 깊게 공부해보고자 합니다!

객체 지향의 핵심에 대해서는 아래 포스팅을 참고해주세요!

 

[Javascript] 객체 지향 (Object Oriented)의 핵심, JS의 다중 패러다임

저는 요즘 클린 아키텍처(Clean Architecture) 책을 읽고 있어요. 이 책에서는 ‘좋은 아키텍처를 만드는 일은 객체 지향 원칙을 이해하고, 응용하는 것에서 출발한다’ 라고 말합니다. 왜 그런 것일

lurgi.tistory.com

❓의존성이란?

의존성은 객제 지향에서 한 객체가 다른 객체와의 상호 관계를 나타내는 것입니다. 객체를 분리하고, 의존성을 가지게 됨으로써 복잡한 메서드를 모듈화 하여 빠르게 코드를 이해할 수 있고 재사용성도 높아지게 됩니다!

하지만 이 의존성이 너무 높아지게 된다면, 메서드를 지속적으로 사용하는 것이 어려워집니다. 한 가지 예시를 통해 강한 의존성을 알아보겠습니다.

const user = {
  name: "lurgi",
  callName() {
    console.log(this.name);
  },
};

user.callName(); //"lurgi"

user 객체의 callName 메서드는 user 객체에 강하게 의존되어 있습니다. 이 메서드는 언제나 user객체의 name을 출력하는 메서드일 것입니다.

❓의존성 주입(Dependency Injection)?

하지만 위의 코드처럼 의존성이 강하다면 callName 메서드는 재사용하기 어려울 것입니다. 이를 해결하기 위한 개념으로 등장한 것이 바로 의존성 주입(Dependency Injection)입니다. 예시를 한번 볼께요.

const user = {
  name: "lurgi",
  callName(name) {
    console.log(name);
  },
};

user.callName(user.name); //lurgi

callName()메서드를 user객체 내부에 있지만, 인자로 출력할 값을 받아 user.name에 의존성을 띄지 않는 정적 메서드로 만들었습니다. 이를 이용한다면

메서드를 분리할 수도 있겠네요!

function callName(name) {
    console.log(name);
}

const user = {
  name: "lurgi",
};

callName(user.name); //lurgi

너무나 당연한 이야기 같지만, 코드가 복잡해짐에 따라 의존성 주입을 간과하는 코드들이 나오기 마련입니다. 의존성 주입이 중요한 이유는, TDD에서의 테스트 수행 용이성과 코드의 재사용성과 같은 유지 보수 측면에서 정말 중요한 개념입니다!

❓의존성 역전(Dependency Inversion)?

의존성 역전이 참 헷갈리는 부분이네요. 이것의 정의부터 알아볼까요?

‘상위 수준의 모듈이 하위 수준의 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 한다는 원칙을 의미합니다.’

음.. 무슨 말인지 잘 모르겠네요. 상위 수준과 하위 수준은 무엇이고, 추상화는 무엇일까요? 일단 예를 들어 보겠습니다.

class Car {
  name;
  constructor(name) {
    this.name = name;
  }
}

class Race {
  vihicles;
  constructor(string) {
    this.vihicles= string.split(",").map((name) => new Car(name));
  }
}

const race = new Race("car1,car2,car3");
race.vihicles.forEach((vihicle) => console.log(vihicle.name));

Race의 vihicles 이라는 변수에는 Car 객체가 있습니다. 여기서 Race 객체는 Car 클래스에 의존적입니다. 이 상황에서 만약 Car 객체가 아닌 motorcycle이란 객체가 등장한다면 어떨까요?

class Motorcycle{
	name;
  constructor(name) {
    this.name = name;
  }
}

class Car {
  name;
  constructor(name) {
    this.name = name;
  }
}

class Race {
  vihicles;
  constructor(string) {
    this.vihicles= string.split(",").map((name) => new Car(name));
  }
}

const race = new Race("car1,car2,car3");
race.vihicles.forEach((vihicle) => console.log(vihicle.name));

Race 객체가 Car객체에 의존적이기 때문에 Motorcycle 객체를 넣기 위해서는 직접 Race 객체를 수정해주어야 합니다. 이 상황을 ‘고수준의 모듈(Race)이 저수준의 모듈(Car)에 의존하고 있는 상황” 이라고 합니다. 만약 이 상황이 매우 복잡한 코드에서 일어난 상황이라면 골치 아프겠네요. 그렇다면 Race 객체의 의존성을 없애 보겠습니다.

class Motorcycle {
  type = "Motorcycle";
  name;
  constructor(name) {
    this.name = name;
  }
}

class Car {
  type = "Car";
  name;
  constructor(name) {
    this.name = name;
  }
}

class Race {
  vihicles = [];
  constructor(object) {
    for (const { name, type } of object) {
      this.vihicles.push(new type(name));
    }
  }
}

const race = new Race([
  { name: "car1", type: Car },
  { name: "motorcycle1", type: Motorcycle },
  { name: "car2", type: Car },
]);
race.vihicles.forEach((vihicle) => console.log(vihicle.name, vihicle.type));
/*
car1 Car
motorcycle1 Motorcycle
car2 Car
*/

기존의 Race 객체에서 Car와의 의존성을 없애 Motorcycle 객체를 집어넣을 수 있게 되었습니다.

이 상황은 고수준의 모듈과 저수준의 모듈이 모두 추상화에 의존하고 있다고 볼 수 있습니다.

추상화라는 것이 상대적이기 때문에, 만약 Race객체가 CarRace, 즉 Car객체만 가질 수 있는 것이라면 이렇게 코드를 만들지 않더라도 의존성 역전 법칙을 어기지 않게 되는 것이라 생각됩니다!

😀결론..!

이렇게 의존성에 대해서 알아봤는데요, 객체 지향 5대 원칙 S.O.L.I.D중 D (Dependency Inversion Principle)에 해당하는 내용인 의존성 역전에 대해서 더 알아가는 시간을 가졌습니다. 추상화를 통해 효율적이고 지속 가능한 프로그램을 만들어 나갈 수 있겠습니다!