객체지향과 의존성 (Dependency)

DatQueue·2022년 9월 27일
2
post-thumbnail

이번 포스팅에선 타이틀에서도 보이듯이 “OOP(Object Oriented Promgramming __객체지향 )”측면에서 바라본 “의존성(Dependency)”에 관해 알아보고자 한다.

단순히 객체지향 측면이 아닌 프로그래밍의 큰 틀에서 바라본 “의존성”은 상당히 많은 부분에서 언급된다. 앞서 작성되었던 “믹스인(MIXIN)”관련 포스팅에서도 해당 “의존성”에 관해 언급하며 “컴파일 타임 의존성과 런타임 의존성의 차이와 특징”에 대해 알아보기도 하였다. 이처럼 “의존성”은 프로그래밍에 있어 굉장히 큰 부분을 차지하고 있다.

우리가 다룰 내용은 그 중에서도 “객체지향적 측면에서 바라본 의존성”이다.

객체지향의 대표적 언어인 “JAVA”를 비롯해 “C#”,”Python”, “JavaScript”의 부족한 객체지향을 보완하기 위한 “TypeScript”까지 , 클래스와 인터페이스를 다루는 언어들에 있어 “의존성”에 관한 개념은 꼭 짚고 갈 필요가 있다.

사실 구글링을 통해 주로 “JAVA”로 작성된 객체지향의 의존성과 관련된 내용들을 쉽게 찾을 수 있다. 아마 여러분들은 “의존성”에 대한 딥한 정의, 의존성의 여러 형태(ex 패키지 의존성, 클래스 의존성…), 의존성을 관리하기 위한 규칙등에 관해 접하게 될 것이다. “양방향 의존성은 피해라, 다중성이 적은 방향을 선택해라, 패키지 사이의 의존성 사이클을 제거하라 …” 등등의 내용들은 의존성을 이해하고 제대로 활용하기위해서 굉장히 중요한 내용이다.

하지만, “객체지향의 의존성”에 관해 처음 접하는 과정에 있어서 시작부터 이런 이질적 용어들부터 마주하면 금방 질릴것이라 생각된다. 그렇다고 몰라도 된다는 것은 전혀 아니다. 오히려 필수적으로 알아야한다. 그렇지만 어떻게하면 조금 더 와닿게 나같은 입문자(?)에게 이 내용을 쉽게 전달할 수 있을까 고민끝에 “딱딱해 보이는” 개념위주가 아닌 굉장히 쉬운 코드를 통해 “코드해석”위주로 진행해보고자 한다.

본 포스팅은 “객체지향의 의존성”에 관해 설명하는데 있어 “TypeScript”를 통해 작성하고자 한다.


객체지향(OOP)에서 바라본 의존성

아래 예시 코드를 살펴보자.

class SportsWear {
  public putOn() {
    console.log('put on SportsWear!');
  }
}

class Player {
  private sportsWear: SportsWear;

  constructor() {
    this.sportsWear = new SportsWear();
  }

  public putOn() {
    this.sportsWear.putOn();
  }
}

const player: Player = new Player();
player.putOn(); // put on SportsWear!

위 코드에서 Player 클래스는 SportsWear 클래스에 의존성을 띈다.

여기서 조금 더 들어가보자.

위의 예제에서 Player가 큰 틀의 SportsWear를 입는 것이 아닌 구체적인 브랜드를 입는다고 가정해보자. 예를들면 Nike, Addidas… 와 같은 브랜드들 말이다.

우린 아래와 같이 작성할 수 있을 것이다.

class SportsWear {
  public putOn() {
    console.log('put on SportsWear!');
  }
}

class Nike extends SportsWear {
  public putOn(): void {
    console.log('put on Nike!')
  }
}

class Player {
  private sportsWear: SportsWear;

  constructor() {
    this.sportsWear = new Nike();  // Nike를 생성자로써 받음
  }

  public putOn() {
    this.sportsWear.putOn();
  }
}

const player: Player = new Player();
player.putOn(); // put on Nike!

얼핏보기엔 아무 문제가 없어보인다. 하지만 잘 생각해보면 우리는 Player가 의존하는 클래스를 변경함과 동시에 Player 내부에서도 생성자를 수정하게 되었다. 지금이야 Nike를 의존하는 클래스가 Player 하나라서 상관이 없다. 그치만 Nike를 의존하는 클래스가 Player뿐만아니라 여러개, 아니 수십개라면 어떨까?

우린 SportsWear를 의존하고 있었던 수 많은 클래스들이 Nike를 의존하도록 하기 위해 일일이 해당 클래스에 찾아가서 변경을 해주어야 할 것이다.


개방 폐쇄 원칙(OCP)과 의존성

당연히 변경하기 위해 수정해주어야하는게 아닌가? 라고 생각할 수도 있지만 사실 이는 객체 지향의 원칙중 “개방 폐쇄 원칙(Open-Closed Principle, OCP)”에 어긋난다.

간단히 “개방 폐쇄 원칙”에 대해 알아보자면 (자세한 건 SOLID에 관해 찾아보길 바랍니다) 개방 폐쇠 원칙은 확장에 대해 열려있고 수정에 대해서는 닫혀있어야 한다는 원칙이다.

확장에 대해 열려있도록 : 요구사항이 변경될 때 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있다.

수정에 대해 닫혀있도록 : 기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있어야 한다.

우리 코드에 위의 개념을 적용시켜볼 수 있다.

기존에 Player 클래스가 SportsWear 클래스를 의존하였지만 Nike를 의존하도록 새로 요구되었다. 이에 따라 우린 Nike 클래스를 작성함에 따라 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있었다.

하지만 우린 기존의 Player 클래스에 관한 코드를 수정함에 따라 새로운 의존성에 관한 기능을 변경할 수 있었다. 즉, “수정에 대해 닫혀있어야한다.”라는 원칙을 위배하였다.

이처럼 객체와 객체, 클래스와 클래스간에 긴밀한 의존성은 오히려 애플리케이션을 작성하는데 있어서 독이 될 수 있다. 의존성의 단점이라고 볼 수 있는 것이다.


의존성 주입(Dependency Injection)의 등장


class SportsWear {
  public putOn() {
    console.log('put on SportsWear!');
  }
}

class Nike extends SportsWear {
  public putOn(): void {
    console.log('put on Nike!')
  }
}

class Player {
  private sportsWear: SportsWear;
	
	// 의존하고 있는 클래스를 생성자로 받아옴
  constructor(sportsWear : SportsWear) {
    this.sportsWear = sportsWear;
  }

  public putOn() {
    this.sportsWear.putOn();
  }
}

const player: Player = new Player(new Nike());
player.putOn(); // put on Nike!

위의 코드를 보면 우리가 처음 의존성을 그대로 사용하는 코드와는 조금 다른 것을 느낄 것이다.

Player 클래스의 내부에서 애초에 의존하고 있는 클래스 SportsWear를 생성자로부터 받아옴에 따라 요구되는 클래스가 Nike로 변경이되어도 별도 내부 수정없이 인스턴스 생성자에 추가함으로써 해당 로직을 충족시킬 수 있다.

해당 코드는 “의존성 주입(Dependency Injection)”을 실행시켰다고 할 수 있다.

갑자기 뜬금없이 코드 조금 수정하고 “의존성 주입”되었다는 것이 어이없을 것이다. 어떻게 설명해야 잘 설명할 수 있을지 모르겠지만 “아주 약간(?) 변경시킨” 위의 코드엔 굉장히 중요한 개념들이 내포되어있다는 것을 미리 말한다.

조금 어렵게 들릴수도 있지만 우리는 위의 로직에서 “의존 관계”를 “외부”에서 결정하게 되었다.

가장 먼저 작성하였던 기존 코드를 보면 Player가 Nike를 의존하도록 요구됨에 따라 의존 관계를 Player 클래스 “내부”에서 직접 수정하여 결정하였다. 하지만 우리가 새롭게 “의존성 주입”을 통해 작성한 코드에선 클래스 내부가 아닌 “외부” 인스턴스 실행을 통해서 의존 관계를 결정한 것을 확인할 수 있다.

이처럼 그 의존 관계를 외부에서 결정하고 주입하는 것이 DI(의존성 주입)인 것이다. 또한 해당 의존성 주입엔 “의존 역전 원칙(DIP)”라는 절대 빼놓을 수 없는 개념이 등장한다.


의존 관계의 역전___ 의존 역전 원칙(Dependency Inversion Principle, DIP)


“의존 역전 원칙 ”

이름만 들어도 상당히 난해해보이고 낯설게 느껴진다. 솔직히 드는 생각은 뭔가 엄청난 원칙처럼(예를들면 만류인력 법칙? …) 이름이 거창해보이기도 한다.

앞서 언급하였던 “개방 폐쇄 원칙 ”과 같이 해당 “의존 역전 원칙”또한 OOP의 5대 원칙(SOLID)중 하나이다. 간단히 알아보자.

의존 역전 원칙이란 고수준 모듈저수준 모듈의 구현에 의존하는 것이 아닌, 저수준 모듈이 고수준 모듈에서 정의한 추상 타입(추상 클래스, 추상 인터페이스 …)에 의존해야 한다는 것이다. 객체 지향 프로그래밍에서는 객체들 사이에 메세지를 주고 받기 위해 의존성이 생기는데, 의존성 역전 원칙은 올바른 의존 관계를 위한 원칙에 해당한다.

고수준 모듈 : 변경이 없는 추상화된 클래스(or 인터페이스)

저수준 모듈 : 변하기 쉬운 구체 클래스

갑자기 “고수준 모듈”, “저수준 모듈” 등에 관한 키워드로 혼란스러울 수 있다. 어렵게 생각하지 않아도 된다.


우리 코드를 다시 가져와 보자.

class SportsWear {
  public putOn() {
    console.log('put on SportsWear!');
  }
}

class Nike extends SportsWear {
  public putOn(): void {
    console.log('put on Nike!')
  }
}

class Player {
  private sportsWear: SportsWear;
	
	// 의존하고 있는 클래스를 생성자로 받아옴
  constructor(sportsWear : SportsWear) {
    this.sportsWear = sportsWear;
  }

  public putOn() {
    this.sportsWear.putOn();
  }
}

const player: Player = new Player(new Nike());
player.putOn(); // put on Nike!

조금은 과장일 수 있지만 위의 코드를 하나의 “애플리케이션”이라고 생각해보자. 그렇다면 위의 코드에서 Player 클래스는 애플리케이션이 동작하는데 있어 어쩌면 “절대적”이라 할 수 있다. 옷을 입는 가장 중요한 주체가 되는 Player 클래스이기 때문에 불변의 클래스이다.

반면에 Nike 클래스는 클라이언트의 요구에따라 Adidas라는 클래스로 변경될 수도 있고, Umbro라는 클래스로 변경될 수도 있다.

즉, 우리는 Player를 “고수준 모듈(혹은 상위 수준 모듈)”이라 할 수 있고, Nike를 “저수준 모듈(혹은 하위 수준 모듈)”이라 할 수 있다.

또한, 위에서 정의하였던 “고수준 모듈에서 정의한 추상 타입”이 우리 코드에선 “SportsWear” 클래스가 되는 것이다. 위에서는 클래스로써 정의하였지만 통상적으로 인터페이스 구조로써 많이 정의한다.

최종적으로 짧게 정리하자면 다른 객체(클래스)를 의존하는데 있어, 구체화된 클래스(Nike)에 의존하는 것이 아닌 추상화된 인터페이스(SportsWear)에 의존시켜 “컴파일 시점”에 의존을 가능케하는 것을 “의존 역전 원칙”이라 한다. 그리고 이러한 원칙이 “의존성 주입”을 가능케하는 것이다.

다음 포스팅에선 …

사실 이번 포스팅을 작성한것은 다음 포스팅에 앞선 “발판”이라고 볼 수 있다. 물론 이번 개념이 객체 지향에 있어 굉장히 중요한 내용은 틀림없다. 하지만 사실 위와 같은 방법으로 의존성 주입을 시킨 코드를 사용할 일은 많지 않을 것이다. 위의 방식또한 그리 좋은 코드진행이라 볼 수 없다.

의존 역전 원칙도 했고, 의존성 주입까지 만들어냈는데 뭐가 또 좋지 않다는 건지 머리가 복잡해 할 필요는 없다. 다음 포스팅에선 “제어권의 역전(InverSion of Control, IoC)”이라는 새로운 개념을 통해 조금 더 훌륭한, 조금 더 실용적인 “의존성 주입”을 해보고자 한다.

profile
You better cool it off before you burn it out / 티스토리(Kotlin, Android): https://nemoo-dev.tistory.com

0개의 댓글