믹스인(MIXIN | TypeScript)

DatQueue·2022년 8월 10일
3
post-thumbnail

해당 내용은 타입스크립트 공식 사이트 번역본을 참고하여 진행한다.
타입스크립트에서 믹스인 기능 구현은 어떻게 하는가를 비롯해 해당 로직이 전체 구조에 있어 어떠한 효과를 기대하는지를 "의존성"이란 객체지향적 측면에서 바라보고자 한다.

TypeScript 한글 문서

소개

전통적인 객체지향 계층과 함께, 재사용 가능한 컴포넌트로부터 클래스를 빌드하는 또 다른 일반적인 방법으로, 간단한 부분클래스를 결합하여 빌드하는 것이다.

TypescriptJavascript 구분없이 설명 .

Javascript(or Typescript)는 단일상속만을 허용하는 언어이다. 객체엔 단 하나의 [[Prototype]]만 있을 수 있고, 클래스는 클래스 하나만 상속받을 수 있다.

간단한 예시를 통해 확인해보자.

Example클래스를 정의하는데 있어 Disposable클래스와 Activatable이란 클래스를 둘 다 확장시키고(상속받고)자 한다.

하지만 위와 같이 두 클래스를 나열해서 적으면 에러표시가 뜨며, ts(or js) 환경에서 클래스는 단일 클래스만 확장 가능하다 알려준다.

그렇지만 이런 제약은 분명 “한계”로 다가올 수 있다.

예를 들어보자. 클래스 StreetSweeper(도시의 거리를 청소하는 차량)와 클래스 Bicycle이 있는데, 이 둘을 섞어 StreetSweepingBicycle를 만들고 싶다고 해보자.

또는 클래스 User와 이벤트를 생성해주는 코드가 담긴 클래스 EventEmitter가 있는데, EventEmitter의 기능을 User에 추가해 사용자가 이벤트를 내뿜을 수 있게(emit)해주고 싶다고 해보자.

이럴 때 우린 “믹스인(mixin)”을 사용해 도움을 받을 수 있다.

Wikipedia에선 “믹스인(mixin)”을 다른 클래스를 상속받을 필요 없이 이들 클래스에 구현되어있는 메서드를 담고 있는 클래스라고 정의한다.

원리는 단순한데, 클래스 A가 클래스 B를 확장해서 그 기능을 받는 것이 아니라 함수 B가 클래스 A를 받고 기능이 추가된 새 클래스를 반환하는 것이다. 함수 B가 믹스인인 것이다.

믹스인은 다음과 같은 함수이다.
- 생성자(constructor)를 받음
- 생성자를 확장하여 새 기능을 추가한 클래스 생성
- 새 클래스 반환

그럼 이제 본격적으로 예시를 통해 “믹스인(mixin)”을 파헤쳐보자.


예시 파헤치기

예시 코드는 mixin을 수행하는 두 클래스로 시작한다. 두 클래스는 각각 부분적인 기능에 집중되어 있음을 알 수 있다. 이후에는 두 기능을 모두 사용하여 새로운 클래스를 만들기 위해 이들을 혼합(mix)할 것이다.

export {}

class Disposable {
  isDisposed! : boolean;
  dispose() {
    this.isDisposed = true;
  } 
}

class Activatable {
  isActive! : boolean;
  activate() {
    this.isActive = true;
  }
  deactivate() {
    this.isActive = false;
  }
}

다음으로, 두 mixin의 결합을 처리 할 클래스를 만든다.

class SmartObject {
  //...
}

interface SmartObject extends Disposable , Activatable {}

분석해보자.

첫 번째 사항(특징)은 SmartObject클래스에서 DisposableActivatable을 확장하는 대신 SmartObject 인터페이스에서 확장한다는 것이다.

인터페이스는 다중 확장(multiple extends)이 가능하기 때문에 위와 같은 구현이 가능케 되는 것이다.

이제 여기서 중요한 개념이 나온다. 바로 “Declaration Merging” 이다.

”Declaration Merging”(선언 병합)에 관한 자세한 내용은 추후 따로 다룰 예정

선언 병합은 동일한 이름의 타입을 가진 경우에 “인터페이스와 인터페이스” , “열거형과 열거형” , “네임스페이스와 네임스페이스” 등의 다양한 결합을 가능케 한다.

하지만, 선언 병합은 “클래스 병합”을 허용하지 않는다!!!

즉, 위 예제에 만들어준 SmartObject클래스와 SmartObject의 인터페이스의 선언 병합은 허용하지 않는다는 말이다.

결국, 이것이 우리가 클래스 병합에 있어 “mixin”을 제공해야하는 이유이다.

그 이외에는 mixin을 피할 수 있다.

다시 코드로 넘어가 클래스 구현에서 ,mixin혼합해보자.

applyMixins(SmartObject, [Disposable , Activatable]);

applyMixins라는 “함수”를 만들어 해당 클래스들을 혼합해주는 과정이다.

그리고 이 함수 applyMixinsmixin이 되는 것이다.

마지막으로, mixin을 구현해줄 , 위에서 정의한 함수 applyMixins를 만들어보자.

function applyMixins(derivedCtor : any , baseCtors: any[]) {
  baseCtors.forEach((baseCtor) => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
      Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null));
    })  
  })
}

Typescript 4.7 이후, 믹스인(MIXIN)을 구현하는데 제공하는 베이스 코드이다.

(즉, 모든 믹스인 적용예제 동일하게 제공할 수 있는 구문이다.)

평소에 코드를 작성하는데 있어, 잘 사용하지 않는 getOwnPropertyNames , defineProperty , getOwnPropertyDescriptor등을 사용해서 그런지 굉장히 복잡해 보인다.

먼저 , mixin인 함수 applyMixins의 첫 번째 파라미터로 “파생 클래스(derived constructor)”를 뜻하는 derivedCtor를, 두 번째 파라미터로 파생 클래스가 참조하는 “기본 클래스(base constructor)”를 뜻하는 baseCtors를 받아준다.

이때 , baseCtors은 파생 클래스(Smartobject)가 확장할 모든 상위 클래스를 포함하는 배열이다.

이제부터, 본격적으로 배열 baseCtors를 순회할 것이다.

앞서, 해당 baseCators를 프린트해보니

console.log(baseCtors); //

당연히 두 클래스인 DisposableActivatable을 요소로 가지는 것을 확인할 수 있다.

즉, baseCtors.forEach()의 콜백 파라미터로 받아준

baseCators.forEach((baseCtor) => {...})

baseCtor이 해당 두 기본 클래스인 것이다.

다음으로

Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {...})

이 부분을 살펴보면 Object.getOwnPropertyNames를 사용하는 부분에서 baseCtor이 아닌 baseCtor.prototype을 받아온 것을 알 수 있다.

그냥 baseCtor은 Class 자체이고, baseCtor.prototype은 Class.prototype일 것이다.

“이 둘은 어떤 차이가 있는 것일까?”


자바스크립트 Class와 prototype - 중요 개념

사실 해당 개념(Class와 Prototype)은 너무 중요하면서도 자바스크립트에있어 “근본적” 내용이라 깊게 다루고자하면 상당히 길어질 것이다.

해당 내용은 추후 따로 다루도록 하고, 중요한 포인트 위주로 알아보자.

“자바스크립트”에서 “클래스”는 보통의 객체 지향 프로그래밍언어(Java , C# , Python , …) 들과 다르다. 더이상 말하면 입이 아프겠지만, “자바스크립트”에서 “클래스”는 엄밀히 말하면 “함수”의 한 종류이고 , “prototype” 원리로써 구현된다.

우리가 작성한 코드로 알아보자.

(참고로, 우리는 지금 “믹스인(mixin)”을 알아보는 중이다. 하지만 해당(자바스크립트에서 Class의 특징과 prototype)개념은 믹스인을 이해하는데 연관성이 충분히 있으므로 알아보는 것이니 길을 잃고 혼란스러워하지 말자.)

class Disposable {
  isDisposed! : boolean;
  dispose() {
    this.isDisposed = true;
  } 
}

해당 클래스 Disposable내에서 정의한 메소드 dispose를 불러오기 위해

Disposable.dispose();

다음과 같이 입력하면 에러를 표시하게 된다. Disposabledispose메소드가 있기 때문에 불러올 수 있을 것 같지만 자바스크립트 클래스의 특성상 불가하다.

자바스크립트는 dispose와 같이 클래스 내에서 정의한 메소드를 Disposable.prototype에 저장하게 된다. 즉, 아래와 같이 prototype을 연결시켜주어 작성한 후

Disposable.prototype.dispose;

콘솔로 해당 값을 확인해보면

우리가 원하는 dispose메서드를 불러올 수 있다.

물론, 해당 메서드 dispose불러오기 위해선 prototype을 이용할 수도 있지만 “인스턴스” 를 이용할 수도 있다.

const disposable = new Disposable();
console.log(disposable.dispose); //결과는 동일

“””

책갈피 — 정리

잠시 정리하자면 생성자의 프로토타입 객체에 접근해 멤버를 추가하거나 삭제하려면 반드시 생성자의 prototype 속성을 이용해야 한다.

instance.prototype // (x)과 같이 인스턴스에서 직접 프로토타입 객체에 접근하는 방법은 없지만

인스턴스를 통해서 생성자별로 정의되는 프로토타입 멤버에는 접근할 수 있다.

“””

다시 해당 코드로 돌아와

Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {...})

위의 개념을 이해했으므로 콘솔 창에 baseCtor과 baseCtor.prototype을 프린트해보자.

console.log(baseCtor);
console.log(baseCtor.prototype);

확인해보면 다음과 같다.

baseCtor 단독으로는 어떠한 것도 가공할 수 없다는 것을 볼 수 있다. 이것이 우리가 믹스인에서 클래스를 병합하는 과정에 있어 prototype 프로퍼티를 사용하는 “이유”이다.

Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {...}

Object.getOwnPropertyNames()()안에 전달된 객체의 열거형 및 열거할 수 없는 속성들을 문자열 배열로 반환한다.

잠시 언급하자면, Object.keys()를 쓰면 되지 왜 굳이 Object.getOwnPropertyNames()를 사용하냐 할 수도 있는데, Object.keys()는 열거형 속성들에 한에서만 가능하다. 물론 열거형 속성들을 문자열로 배열할 시에는 Object.keys()를 더욱 제안하고 있다.

console.log(Object.getOwnPropertyNames(baseCtor.prototype)); //['constructor', 'dispose'] , ['constructor', 'dispose' , 'deactivate']
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {console.log(name});

name 요소를 프린트하면

아래와 같다.

지금부터 알아볼 부분들은 낯설게 느껴질 것이다.

먼저 Object.defineProperty() 이다.

“””

Object.defineProperty()

Object.defineProperty() 정적 메서드는 객체에 새로운 속성을 직접 정의하거나 이미 존재하는 속성을 수정한 후, 해당 객체를 반환한다.

구문

Object.defineProperty(obj, prop, descriptor)
  • obj 속성을 정의할 객체 .
  • prop 새로 정의하거나 수정하려는 속성의 이름 또는 Symbol .
  • descriptor 새로 정의하거나 수정하려는 속성을 기술하는 객체 .

“””

우리 코드에서 알아보자.

function applyMixins(derivedCtor : any , baseCtors: any[]) {
  baseCtors.forEach((baseCtor) => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
      Object.defineProperty(
        derivedCtor.prototype,
        name,
        Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null));
    })  
  })
}

전체 믹스인 함수 코드를 통해 보자.

위에서 프린트함으로써 알아보았던 클래스를 통해 얻어온 두 배열을

['constructor', 'dispose'] , ['constructor', 'dispose' , 'deactivate']

forEach를 이용하여 순회하게 되고 그때 callback 구문으로써 Object.defineProperty()를 정의하였다.

첫 번째 파라미터 obj로는 derivedCtor.prototype을 받아주었다.

위에서 정의하였듯이, 첫 번째 파라미터로 오는 obj는 “속성을 정의할 객체” 이다.

즉, 믹스인 과정을 통해 클래스 병합을 이루게 될 클래스 SmartObject가 오게 된다.

(derivedCtor == SmartObject)

더하여 프로토타입 객체에 접근하고자 하므로 그냥 derivedCtor이 아닌 derivedCtor.prototype으로써 적어준다.

두 번째 파라미터 prop으로는 name을 받아주었다.

prop은 “새로 정의하거나 수정하려는 속성의 이름 또는 Symbol”을 나타낸다. 앞서 우린 name을 프린트해보았고 , (constructor , dispose) , (constructor , activate , deactivate) 을 얻을 수 있었다.

즉, 해당 속성들을 SmartObject 클래스내에서 가공한다는 뜻이다.

세 번째 파라미터 descriptor로는 Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null));를 받아주었다.

두 번째 파라미터 prop에서 (즉, name) 가공할 속성의 이름을 제시하였다면 해당 세 번째 파라미터 descriptor에서 가공할 속성을 “기술”하는 것이다.

여기서 또 낯선 두 정적 메서드를 보게된다.

바로 Object.getOwnPropertyDescriptor()Object.create()이다.

“””

Object.getOwnPropertyDescriptor() - 속성 설명자

Object.getOwnPropertyDescriptor()메서드는 주어진 객체 자신의 속성(즉, 객체에 직접 제공하는 속성, 실은 객체의 프로토타입 체인을 따라 존재하는 덕택에 제공하는 게 아닌)에 대한 속성 설명자(descriptor)를 반환한다.

구문

Object.getOwnPropertyDescriptor(obj , prop)
  • obj 속성을 찾을 대상 객체.
  • prop 설명이 검색될 속성명.

반환값

객체에 존재하는 경우 주어진 속성의 속성 설명자, 없으면 undefined.

“””

우리 코드에서 알아보자.

Object.getOwnPropertyDescriptor(baseCtor.prototype, name)

baseCtor.prototype =⇒ obj : 속성을 찾을 대상 객체

name =⇒ prop : 설명이 검색될 속성명

“””

Object.create()

Object.create()는 어원 그대로 지정된 프로토타입 객체 및 속성(property)을 갖는 새 객체를 만든다.

구문

Object.create(proto[, propertiesObject]) 
  • proto 새로 만든 객체의 프로토타입이어야 할 객체
  • propertiesObject — optional 내용 생략 — MDN 참고

해당 정적 메서드에 관한 깊은 내용은 생략하겠다.

“””

우리 코드 에선 아래와 같이 Object.create(null)을 사용하였고 해당 구문을 사용함으로써

새 객체 생성에 있어 프로토타입이 없는 빈 객체 또한 defineProperty메서드의 descriptor로 받아줄 수 있다는 것을 명시하게 된다. (솔직히 잘 모르겠음)

Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null);

전체 코드 정리 및 기능 분석

기본 예시와 믹스인 함수 코드 베이스에 약간의 기능 구현을 추가해보았다.

<전체 코드>

export {}

class Disposable {
  isDisposed! : boolean;
  dispose() {
    this.isDisposed = true;
  } 
}

class Activatable {
  isActive! : boolean;
  activate() {
    this.isActive = true;
  }
  deactivate() {
    this.isActive = false;
  }
}

class SmartObject {                 // 병합된 클래스
  constructor() {
    setInterval(() => {
      console.log(this.isActive + " : " + this.isDisposed);
    },500);
  }
  interact() {                      // interact() 메소드를 통해 간단한 기능 구현
    this.activate();
    this.dispose();
  }
}

interface SmartObject extends Disposable , Activatable {}
applyMixins(SmartObject, [Disposable , Activatable]);

let smartObj = new SmartObject();
setTimeout(()=>{
  smartObj.interact()
},1000)

function applyMixins(derivedCtor : any , baseCtors: any[]) {  // mixin 코드 베이스
  baseCtors.forEach((baseCtor) => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
      Object.defineProperty(
        derivedCtor.prototype,
        name,
        Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null));
    })  
  })
}

해당 믹스인(mixin) 구현을 통해 SmartObject 클래스에서 this 키워드를 이용하여 Disposable 클래스와 Activatable클래스의 메소드들을 받아올 수 있다.

먼저 해당 코드를 실행하였을때 결과를 확인해보자.

위의 결과는 어떻게 실행된 것일까?

아래 SmartObject클래스의 생성자 실행구문을 살펴보자.

let smartObj = new SmartObject();
setTimeout(()=>{
  smartObj.interact()
},1000)

첫번째 줄의 let smartObj = new SmartObject();에 의해 SmartObject 클래스가 먼저 실행된다.

오로지 **new SmartObject()**에 의해서 해당 클래스의 생성자가 실행되므로 SmartObject본체

constructor() {
    setInterval(() => {
      console.log(this.isActive + " : " + this.isDisposed);
    },500);
  }

constructor 가 생성되며 실행하게 된다.

그렇지만 this.isActivethis.isDisposed를 오로지 생성자 실행만으로는 찾을 수가 없다.

class Disposable {
  isDisposed! : boolean;
  dispose() {
    this.isDisposed = true;
  } 
}

class Activatable {
  isActive! : boolean;
  activate() {
    this.isActive = true;
  }
  deactivate() {
    this.isActive = false;
  }
}

여기서 보다시피 isDisposed , isActive는 클래스 안에서도 dispose() , activate() , deactivate()와 같은 메소드안에 존재하기 때문이다.

즉, let smartObj = new SmartObject();해당 구문의 실행으로는

“undefined”가 출력되게 된다.

2 ~ 4번째 줄의 경우를 실행하였을때는 어떨까?

let smartObj = new SmartObject();
setTimeout(()=>{
  smartObj.interact()
},1000)

SmartObject 클래스의 인스턴스인 smartObj를 통해 interact()메서드를 실행하였고 해당구문을 setTimeout 안에 담아주었다.

smartObj.interact()를 해주었을 때

class SmartObject {                 // 병합된 클래스
  constructor() {
    setInterval(() => {
      console.log(this.isActive + " : " + this.isDisposed);
    },500);
  }
  interact() {                      // interact() 메소드를 통해 간단한 기능 구현
    this.activate();
    this.dispose();
  }
}

해당 SmartObject 클래스 내의 interact가 실행이 된다. 이때, this.activate() , this.dispose()가 실행이되는데 이때, vsCode의 유용한 기능 중 ctrl + click을 통해서 해당 this의 정의문을 알아보면

SmartObject클래스인 동시에, SmartObject 인터페이스또한 동일하게 해당된다는 것을 알 수 있다.

SmartObject 인터페이스는 Disposable , Activatable 두 클래스를 확장시킨 경우이므로 해당 두 클래스의 메서드를 불러올 수 있다는 것을 의미한다.

즉, SmartObject 클래스의 어떤 메서드(여기선 interact) 내부에서 this.activate() , this.dispose() 해 주었는데도 병합을 한 두 클래스(Disposable , Activatable)의 메서드를 불러올 수 있게 된 것이다.

이러한 과정으로

다음과 같은 결과를 얻을 수 있는 것이다. (setTImeout 초깃값 1s로 인해 처음 출력문 후 1초 뒤 실행)


해당 로직의 구조적 특징과 의존성 — ✔✔

이렇게 우린 MIXIN(믹스인) 함수를 통해 클래스 병합을 구현해보았다. 위의 예제는 타입스크립트 공식 문서에서 제시한 방법으로 런타임(runtime)과 유형 계층(type hierachies)을 별도로 만든 다음 마지막에 병합하는 믹스인 작성법이다.

제목에서도 알 수 있듯이 우리가 작성한 믹스인 구현 로직의 구조적 특징 (바로 위에 밑줄로 표시한 부분이 구조적 특징이라 할 수 있다)과 “의존성(dependency)”과의 연관성에 대해 알아 보고자 한다.

여기서 언급할 “의존성”이란 컴파일 타임 의존성런타임 의존성을 의미하고, 알아보고자 할 “연관성”이란 두 의존성(컴파일 의존성, 런타임 의존성)과 우리가 구현한 믹스인 로직과 어떠한 관계가 있는가이다.

“””

본격적으로 들어가기에 앞서 잠깐 하고싶은 얘기가 있다.

우리는 앞서 “예시 파헤치기” 부분에서 믹스인 구현 코드에 관해 하나하나 뜯어가며 아주 세세히 알아보았다. 그렇게 믹스인 구현을 성공시켰지만 우리는 개발을 공부하는 사람들로써 한 발걸음 더 뻗어볼 필요가 있다.

작성자 본인이 말하고자 하는 한 걸음은 “왜 다음과 같은 방법(위의 믹스인 로직)을 제시하였는가?” 이다.

사실 믹스인 구현에 관한 로직은 우리가 작성한 로직 이외에도 몇 가지 존재한다.

해당 코드들을 이 글에 전부 다루기엔 너무 루즈해질것 같아서 따로 다룰 예정이다. 우리는 타입스크립트에서 왜 위의 코드 방법론을 제시하였고 이 방법이 어떠한 장점이 있길래 제시한 것인가를 알아야 할 필요가 있는 것이다.

긴 글이 되겠지만 꼭 읽어주시길 바란다.

“””

들어가 보자.

타입스크립트의 컴파일 과정

개발자가 타입스크립트 코드를 작성한다 —> 타입스크립트 코드를 AST(추상구문트리)라는 자료구조로 변환한다 —> 타입 검사기(TypeChecker)가 코드의 타입 안정성을 검증한다 (컴파일 에러를 찾음)

*** 지금부터는 타입스크립트의 타입 정보가 사용되지 않는다.

타입스크립트 AST —> 자바스크립트 소스로 변환 (컴파일러를 통해) —> 자바스크립트 AST로 변환 —> AST 에서 ByteCode로 변환 —> Runtime에 ByteCode를 입력해 평가하고 결과를 받는다 (런타임 에러를 찾음)

간단히 타입스크립트에서 컴파일과 런타임의 시점에 대해 알아보기 위해 작성하였다.

의존성 (Dependency)

OOP(객체 지향 프로그래밍)에서 특히, 객체의 세계에서 협력은 필수적이며, 객체가 협력한다는 것은 객체 간의 의존성이 존재한다는 것이다. 그리고 의존성은 파라미터나 리턴값 또는 지역변수 등으로 다른 객체를 참조하는 것을 의미한다.

만약, 클래스 B가 클래스 A에 대해 의존성을 띄고 있다고 하자. 그런데 클래스 B가 갑자기 A가 아닌 A를 상속받은 AA에 대해 의존하고자 한다면 어떨까?

그렇게 될 경우 클래스 AA를 명시함과 동시에(class AA extends A) 클래스 A를 의존하고 있는 클래스 B를 수정해주어야한다.

위의 글을 읽고 다음과 같은 생각을 할 수도 있다.

“당연히 의존하는 클래스를 변경하였으니까… 참조받을 클래스에도 수정을 해주는 것 아닌가?”

어쩌면 당연한 생각이다.

그런데 만약 AA를 의존하는 B와 같은 클래스가 여러가지 있다면 어떨까?

여러가지를 넘어 한 50~100가지가 있다고 가정하자. 그러면 일일이 AA를 의존하는 모든 클래스를 수정해주어야할 것이다.

여기서 우린 “의존성 전이”라는 개념을 알게 된다. 한 객체가 다른 객체에 의존한다는 것은 다른 객체가 변할 때 변경이 전파될 수 있다는 것을 의미한다. 의존성은 객체 협력을 위해 필수적이지만 의존성은 위험하므로 최소화 되어야한다.

사실 타입스크립트에서는 “Dependency Injection(의존성 주입)”을 통해 위와 같은 문제를 해결하는 방법을 제시한다.

그렇지만, 우리가 지금 알아볼 것은 아니고 그 전에 더 중요한 개념이 있다.

이 부분을 다루고 싶어서 지금까지 주저리 주저리 말했다고 보아도 과언이 아니다.

바로 “컴파일 타임 의존성 ”“런타임 의존성 ”이다.

컴파일 타임 의존성

컴파일 타임 의존성이란 코드를 컴파일하는 시점에 결정되는 의존성이며, 클래스 사이의 의존성에 해당한다. 일반적으로 추상화된 클래스나 인터페이스가 아닌 구체 클래스에 의존하면 컴파일타임 의존성을 갖게된다.

즉 , 어떠한 의존성을 띄는 클래스가 의존할 클래스를 직접적으로 바로 나타내는 것이다. 그러므로 컴파일타임 의존성은 결합도가 높다.

결합도가 높다는 것은 결코 좋은 현상이 아니다. 위에서도 언급하였지만, 의존성이 높을수록 위험하다. 우린 의존성을 최소화하는 방향으로 설계를 해야하고 결국, 결합도를 낮게 해야한다.

그 방법으로 제시하는 것이 바로 “런타임 의존성”이다.

런타임 의존성

런타임 의존성이란 코드(애플리케이션)을 실행하는 시점에 결정되는 의존성이며, 객체 사이의 의존성에 해당한다. 일반적으로 추상화된 클래스나 인터페이스에 의존할 때 런타임 의존성을 갖게 되며, 이러한 이유로 런타임 의존성과 컴파일 의존성은 다를 수 있다.

interface SmartObject extends Disposable , Activatable {}
applyMixins(SmartObject, [Disposable , Activatable]);

우리 코드에서도 바로 확인이 가능하다. SmartObject라는 인터페이스를 만들어주었고 클래스 SmartObject는 해당 SmartObject 인터페이스를 의존하도록 해주었다. 물론 의존하는 기능 구현에 있어 믹스인 함수(applyMixins)를 통한 선언병합을 사용하고 있지만, 이 또한 컴파일러를 최소한 덜 의존하고자 런타임 의존성으로 구현했다고 볼 수 있다.

applyMixins함수를 실행 시에 JS 런타임에서 실행이 일어나게 되고 즉, 해당 믹스인 기능이 일어나게 된다.

알다시피, TS에서 JS로 컴파일되게 된 후엔 인터페이스와 같은 TS만의 선언문은 사라지게 된다. 즉, 컴파일에 의존하는 것이 아닌, 실행 후 JS 런타임에 더욱 의존하게 되므로 런타임 의존성이라 보게 되는 것이다.


생각정리

이렇게 타입스크립트에서 MIXIN(믹스인) 기능 구현부터, 해당 MIXIN이 OOP적인 측면에서 Depedency(의존성)와 어떠한 관계가 있는지까지 알아보았다.

포스팅을 작성하면서 나의 생각과 겸비해 작성을 하다보니 여태컷 적었던 포스팅 중 가장 길게 작성하지 않았나 싶다. 사실, 뭐 하나 빼먹고 싶지 않은 부분이었다.

물론, 누군가는 MIXIN을 설명하는데 굳이 컴파일과 런타임에 관한 내용까지 언급하냐 할 수 있다.

단순히 MIXIN을 설명하는 것은 사실 공식문서를 통해 충분히 구조를 파악할 수 있다. 하지만, 난 나같이 개발을 공부한지 그리 오래되지 않은 입문자의 느낌에서, 특히 Java와 같은 OOP 언어에 관해 익숙치 않은 사람이 처음 MIXIN을 접했을 때, 단순히 코드 구현하는 법만 알고 넘어가는 것은 나중에 발걸음을 나아가는데있어 좋지 않다고 생각했다.

난 “MIXIN”을 공부하면서 “선언 병합”이라는 것이 뭔지 알게 되었고, 더 나아가 “의존성 전이” , “컴파일 의존성”과 “런타임 의존성”의 차이까지 공부할 수 있었다.

이처럼 개발 공부는 서로 간의 체이닝을 통해 밀접하게 연결되어있고, 우린 나중에 어떠한 개념에 대해 공부를 하게 될때, 분명 오늘 공부한 내용이 한 번은 연관되어있어 오늘 배운 지식을 그때 연결시킬 수 있게 된다.

곧 있으면 타입스크립트에서 “Dependency Injection(의존성 주입)” , “Decorator(데코레이터)” 이러한 개념들을 공부하게 될건데 분명히 오늘 귀찮게 주절주절 알게 된 저 지식들이 도움이 될 것이라 장담한다.

어찌됬건 나는 이러한 식으로 개발공부를 하고있고, 이틀에 걸쳐 작성한 해당 포스팅이 꼭 누군가에게 도움이 되었으면 한다.

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

0개의 댓글