객체 합성이라는 숨겨진 보물

The Hidden Treasures of Object Composition

Posted by mido on 2018-04-23

이 글은 Eric Elliottmedium에서 연재하는 Composing Software 시리즈를 번역한 것입니다. [원문보기]

Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)

참고 : 이 글은 JavaScript ES6+의 함수형 프로그래밍 및 소프트웨어 합성 방법론을 기초부터 다루는 "소프트웨어 합성"시리즈의 일부 입니다. 앞으로 계속하여 연재될 것입니다.
<이전 | << Part 1에서 다시 시작 | 다음>

“객체 합성, 보다 복잡한 동작을 하기 위해 객체를 조립 또는 합성하는 것”~ 4 강 “디자인 패턴 : 재사용 가능한 객체 지향 소프트웨어의 요소”

“클래스 상속보다는 객체 합성을 우선해라”, Gang of Four,

소프트웨어를 개발하는 과정에서 가장 흔히 볼 수 있는 실수 중 하나는 클래스 상속을 과도하게 사용하는 경향입니다. 클래스 상속은 기본 클래스와 하위 클래스 사이에 는-이다(is-a) 관계를 만들어 코드를 재사용하는 메커니즘입니다. is-a 관계 (e.g. 오리는 새이다)를 사용하여 도메인을 모델링하는 과정에서 문제가 발생할 수 있습니다. 그 이유는 클래스 상속이 객체 지향 디자인에서 사용할 수 있는 가장 단단한 형태의 결합이기 때문입니다. 결국 다음과 같은(그 외에도) 많은 문제들을 일으킵니다 :

  • 깨지기 쉬운 기본 클래스 문제
  • 고릴라 / 바나나 문제
  • 중복 필요성 문제

상속은 하위 클래스가 상속, 추가 및 오버라이딩할 수 있는 기본 클래스를 공용 인터페이스로 추상화하여 코드를 재사용합니다. 추상화에는 두 가지 중요한 특징이 있습니다.

  • 일반화Generalization 일반적인 사용사례에 해당하는 공유 속성 및 동작들을 추출하는 과정
  • 전문화Specialization 특수한 경우를 처리하는 데 필요한 구현 세부 사항을 제공하는 과정

코드를 일반화 및 전문화하는데는 여러 가지 방법이 있습니다. 클래스 상속대신 사용할 수 있는 좋은 대안으로는 단순 함수, 고차 함수 및 객체 합성이있습니다.

불행히도, 많은 사람들이 객체 합성에 대해 오해하고 있으며 이러한 관점으로 생각하기 어려워 합니다. 주제에 대해 좀 더 깊이 알아봐야 할 필요가 있습니다.

객체 합성Object Composition이란 무엇입니까?

"컴퓨터 과학에서의 복합 자료형Composite datatype이란 프로그래밍 언어의 원시 자료형 및 기타 복합 유형을 사용하여 구성 할 수있는 모든 데이터 유형입니다. […] 복합 유형을 구성하는 행위는 합성Composition으로 알려져 있습니다. "~ Wikipedia

기본형들을 조립해 복합 객체를 만드는 모든 행위가 객체 합성입니다. 그러나 상속은 마치 객체 합성과는 전혀 관련이 없으며 심지어 정반대의 기술인 것처럼 논의되고 있습니다. 이러한 편견이 생기는 이유는 객체 합성의 문법과 의미간에 차이가 있기 때문입니다.

지금부터 알아볼 객체 합성과 클래스 상속에 관한 논의는 특정 기법에 관한 것이 아닙니다. 구성 요소들 사이의 의미론적 관계와 결합 정도에 대한 것입니다. 우리는 문법 이 아니라 의미 에 대해 말하고 있습니다. 사람들은 종종 이 둘을 구별하지 못하고 세부적인 문법에만 집중합니다. 숲을 보지 못하고 나무만 보고있는 셈입니다.

객체는 여러가지 다른 방법으로 합성됩니다. 합성 형태에 따라 복합체의 구조와 객체간의 관계가 달라집니다. 어떤 객체가 다른 객체에 종속되면 한 객체가 변경되었을 때 다른 객체가 손상 될 수 있습니다.

"클래스 상속보다는 객체 합성을 우선해라"라는 조언은 우리가 객체를 바라보는 방식을 거대한 기본 클래스로부터 상속받는 것이 아닌 작고 느슨하게 결합 된 요소들의 합성으로 생각하라는 뜻입니다. GoF는 단단히 결합 된 객체를 "모놀리틱[1] 시스템"이라고 설명합니다. 이 시스템에서 어떤 클래스를 변경하거나 제거하려면 다른 수많은 클래스를 이해하고 변경해야 합니다. 결국 이러한 시스템은 이해하고, 이식하고, 유지하기 힘든 조밀한 덩어리가 됩니다. "

객체 합성의 세 가지 다른 형태

"Design Patterns"에서 Gang of Four는 "객체 합성은 여러 디자인 패턴에 다양하게 적용될 것 입니다"라고 말하고 계속해서 집합위임 을 포함한 다양한 유형의 구성 관계를 설명합니다.

"Design Patterns"의 저자는 주로 C ++ 및 Smalltalk (이후 Java) 환경에서 작업했습니다. 해당 언어로 런타임에 객체 관계를 정의하고 변경하는 작업은 JavaScript에서보다 훨씬 복잡하므로 해당 주제를 깊게 다루진 않을 것 입니다. 그러나 JavaScript의 객체 합성에 대해 논의하기 위해 먼저 동적 객체 확장 ( 일명, concatenation )에 대한 논의가 우선되어야 합니다.

각 용어들은 "Design Patterns"에 나오는 정의와 약간 차이가 있는데, 이는 JavaScript에 적용 하기 위함이며 좀 더 명확하게 일반화된 뜻을 다루기 위함입니다. 예를 들어 집합Aggregation이 하위 객체들의 수명주기를 책임져야 함을 뜻하지 않습니다. 동적으로 객체를 확장시킬 수 있는 언어에서는 그렇지 않습니다.

잘못된 공리 및 정의는 일반화를 힘들게 만들며 같은 개념의 특수한 사례를 다른 이름으로 부르게 만듭니다. 개발자는 불팔요한 반복을 좋아하지 않습니다.

  • 집합Aggregation 객체가 열거 가능한 하위 객체 모음으로 구성되는 경우. 즉, 다른 객체들을 포함 하는 객체입니다. 각 하위 객체는 자신의 레퍼런스를 유지하므로 얼마든지 집합체에서 분리, 해체될 수 있습니다.
  • 접합Concatenation [2] 기존 객체에 새 속성을 추가하여 객체를 형성하는 경우. 속성을 한 번에 하나씩 연결하거나 기존 개체를 통채로 복사 할 수도 있습니다. 예를 들어, jQuery 플러그인은 프로토타입으로 연결된 jQuery.fn에 새 메서드를 연결하여 만들어집니다.
  • 위임Delegation 객체를 다른 객체로 전달하거나 특정 기능을 위임 하는 경우. 예를 들어 Ivan Sutherland의 Sketchpad (1962)에는 공유 속성을 "마스터"에게 위임하는 인스턴스가 있습니다. Photoshop에 포함된 "스마트 오브젝트"는 외부 리소스에게 동작을 위임하는 로컬 프록시가 있습니다. JavaScript의 프로토타입은 위임으로 작동합니다. Array 인스턴스로 배열 메소드를 호출하면 Array.prototype에게 전달되고, Object 메소드들은 Object.prototype에게 전달됩니다.

이러한 다른 형태의 구성은 서로 배타적이지 않습니다. 접합으로 위임을 구현할 수 있으며 JavaScript에서 클래스 상속은 위임으로 구현됩니다. 많은 소프트웨어 시스템은 객체를 합성할 때 하나 이상의 방법을 사용합니다. 예를 들어, jQuery의 플러그인은 프로토타입 프로퍼티인 jQuery.fn에 다른 메소드들을 접합하는 방식으로 확장합니다. 클라이언트 코드가 플러그인 메소드를 호출하면 프로토타입에 연결된 메소드에 위임됩니다.

앞으로 나올 예제들에서 아래 코드를 계속 사용할 것 입니다.

1
2
3
4
5
const objs = [  
{ a: 'a', b: 'ab' },
{ b: 'b' },
{ c: 'c', b: 'cb' }
];

집합 Aggregation

집합은 객체가 열거 가능한 하위 객체 모음으로 구성되는 경우입니다. 즉, 다른 객체들을 포함 하는 객체입니다. 각 하위 객체는 자신의 레퍼런스를 유지하므로 얼마든지 집합체에서 분리, 해체될 수 있습니다. 다양한 구조로 표현 될 수 있습니다.

예제들

  • Array
  • Map
  • Set
  • Graph
  • Tree
  • DOM node (DOM 노드는 자식 노드를 포함 )
  • UI component (컴포넌트는 하위 컴포넌트들을 포함 )

언제 사용 하는가?

스택, 큐, 트리, 그래프, 상태 머신 또는 컴포지트 패턴에서처럼 공통 작업을 공유해야하는 객체 모음이있는 경우 (수많은 아이템들이 동일한 인터페이스를 공유하려는 경우)

참고사항

집합을 사용하면 각 멤버에게 함수를 적용하고( e.g. array.map(fn) ) 단일 값인 것처럼 벡터를 변환하는 등 보편적인 추상화를 적용할 수 있습니다. 그러나 수십, 수백만개의 하위 객체를 다뤄야할 경우 스트림으로 처리하는 것이 더 효율적일 수 있습니다.

코드 예제

Array 집합 :

1
2
3
4
5
6
7
8
9
10
11
const collection = (a, e) => a.concat([e]);

const a = objs.reduce(collection, []);

console.log(
'collection aggregation',
a,
a[1].b,
a[2].c,
`enumerable keys: ${ Object.keys(a) }`
);

다음처럼 됩니다.

1
2
3
4
collection aggregation   
[{"a":"a","b":"ab"},{"b":"b"},{"c":"c","b":"cb"}]
b c
enumerable keys: 0,1,2

pair를 사용하는 연결리스트Linked-list 집합 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const pair = (a, b) => [b, a];

const l = objs.reduceRight(pair, []);

console.log(
'linked list aggregation',
l,
`enumerable keys: ${ Object.keys(l) }`
);

/*
linked list aggregation
[
{"a":"a","b":"ab"}, [
{"b":"b"}, [
{"c":"c","b":"cb"},
[]
]
]
]
enumerable keys: 0,1
*/

연결리스트는 배열, 문자열, 트리 및 다양한 종류의 데이터 구조 및 집합의 기초가 됩니다. 훨씬 많은 예가 있으며 여기서 그것들을 모두 다루진 않겠습니다.

접합, 연결 Concatenation

접합이란 기존 객체에 새 속성을 추가하여 객체를 형성하는 것 입니다.

예제들

  • jQuery.fn에 접합하는 방식으로 플러그인을 추가합니다.
  • State reducers, 상태 관리 유틸리티 (예 : Redux)
  • 함수형 믹스인

언제 사용하는가?

JSON 객체 합치기, 여러 소스에서 애플리케이션 상태를 가져오기, 불변 상태를 업데이트 하기(이전 상태를 새 데이터와 병합)등 런타임에 점진적으로 데이터 구조를 조립하는 것이 필요하다면 언제든지

참고사항

  • 기존 객체를 변이시킬 때 주의해야합니다. 변경 가능한 공유 상태는 많은 버그를 일으킬 수 있습니다.
  • 클래스 계층 구조를 모방하는 것이 가능합니다. 즉, is-a 관계를 만들 수 있습니다. 같은 문제가 적용됩니다. “기본” 인스턴스에서 속성들을 상속하고 다중상속을 하는 대신 작고 독립적인 객체들을 "합성"한다는 아이디어로 접근해야합니다.
  • 구성요소들간의 암시적인 종속성에 주의하십시오.
  • 속성 이름 충돌은 연결 순서로 해결됩니다.(마지막 값이 적용됨) 이는 기본값을 지정하거나 오버라이딩 할 때 유용하지만 순서가 명확하지 않은 경우 문제가 될 수 있습니다.

코드 예제

1
2
3
4
5
6
7
8
9
10
11
const c = objs.reduce(concatenate, {});

const concatenate = (a, o) => ({...a, ...o});

console.log(
'concatenation',
c,
`enumerable keys: ${ Object.keys(c) }`
);

// concatenation { a: 'a', b: 'cb', c: 'c' } enumerable keys: a,b,c

위임 Delegation

위임은 객체를 다른 객체로 전달하거나 특정 기능을 위임 하는 경우입니다.

예제들

  • JavaScript은 기본적으로 위임을 사용하여 메소드 호출을 프로토타입 체인으로 전달합니다. e.g. [].map()Array.prototype.map()으로, obj.hasOwnProperty()Object.prototype.hasOwnProperty() 으로 호출이 위임됩니다.
  • jQuery 플러그인은 위임을 사용해 모든 인스턴스에서 내장 및 플러그인 메소드를 공유합니다.
  • Sketchpad의 "마스터"는 동적 위임자였습니다. 마스터의 변경사항은 모든 객체 인스턴스에 즉시 반영됩니다.
  • Photoshop에서는 "스마트 객체"라는 위임자를 사용하여 별도의 파일에 정의 된 이미지와 리소스를 나타냅니다. 스마트 오브젝트가 참조하는 오브젝트에 대한 변경 사항은 스마트 오브젝트의 모든 인스턴스에 반영됩니다.

언제 사용 하는가?

  1. 메모리 절약 : 객체의 인스턴스가 매우 많을 경우, 각 인스턴스마다 메모리를 더 많이 할당해하는 대신 인스턴스간에 동일한 속성이나 메소드를 공유하는 것이 유용할 수 있습니다.
  2. 많은 인스턴스를 동적으로 업데이트 : 많은 객체 인스턴스가 동일한 상태를 공유하고 동적으로 업데이트해야 하는 경우 변경사항을 즉석에서 모든 인스턴스에 반영시킬 수 있습니다.(예 : Sketchpad의 "마스터"또는 Photoshop의 “스마트 객체”).

참고사항

  • 위임은 일반적으로 JavaScript에서 클래스 상속을 모방하는 데 사용되며 (실제로 extends 키워드에 묶여 있음) 실제로는 거의 필요하지 않습니다.
  • 위임을 사용하여 클래스 상속의 동작 및 제한 사항을 정확하게 모방 할 수 있습니다. 사실 JavaScript의 클래스 상속은 프로토타입 위임 체인으로 구축되어 있습니다.
  • 위임된 속성은 Object.keys(instanceObj) 와 같은 일반적인 메커니즘을 사용하여 열거 할 수 없습니다.
  • 위임은 속성 조회 속도를 희생해 메모리를 절약합니다. JS 엔진은 일부 동적 위임자(생성 된 후에 변경되는 위임자)에 대한 최적화를 수행하지 못합니다. 그러나 가장 느린 경우에도 속성 조회 성능은 수백만 ops /초로 측정됩니다. 이벤트 스트림 처리용 혹은 그래픽 프로그래밍용 범용 유틸리티 라이브러리(e.g. RxJS 또는 three.js)가 아닌 이상 병목 현상이 발생하긴 쉽지 않습니다.
  • 인스턴스의 상태와 위임된 상태를 구별해야합니다.
  • 동적으로 위임된 공유 상태는 안전하지 않습니다. 변경사항은 모든 인스턴스간에 공유됩니다. 이는 일반적으로 (항상 그런 것은 아님) 버그를 발생시키는 원인입니다.
  • ES6에서 클래스의 프로토타입은 재할당될 수 없습니다. 바벨로 컴파일했을 때 작동하는 것처럼 보일지라도 실제 ES6 환경에서는 오류가 발생합니다.

코드 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
const delegate = (a, b) => Object.assign(Object.create(a), b);  

const d = objs.reduceRight(delegate, {});

console.log(
'delegation',
d,
`enumerable keys: ${ Object.keys(d) }`
);

// delegation { a: 'a', b: 'ab' } enumerable keys: a,b

console.log(d.b, d.c); // ab c

결론

지금까지 배운 것들을 요약해보겠습니다.

  • 원시형 및 다른 객체들로 만들어진 모든 객체는 복합 객체 입니다.
  • 복합 객체를 만드는 행위를 합성이라고합니다.
  • 객체 합성에는 여러 종류가 있습니다.
  • 형성되는 관계와 의존성은 객체가 어떻게 합성되는지에 따라 다릅니다.
  • Is-a 관계 (클래스 상속에 의해 형성된 종류)는 OO 디자인의 커플링 중 가장 단단한 형태이며 꼭 필요한 경우가 아니면 일반적으로 피해야합니다.
  • Gang of Four는 모놀리틱한 기본 클래스 또는 기본 객체에서 상속하지 말고 작은 기능들을 조합하여 객체를 합성하도록 권장합니다. “클래스 상속보다는 객체 합성을 우선해라”
  • 집합은 배열, DOM 트리 등 구성원들이 자신의 참조를 유지하는 열거형 컬렉션으로 객체를 합성합니다.
  • 위임은 프로토타입 위임 체인을 연결하여 객체를 합성합니다. 이 때 객체는 속성 조회 및 메소드 호출을 프로토타입에게 전달하거나 다른 객체에 위임합니다. e.g. [].map()Array.prototype.map()을 호출합니다.
  • 접합(연결)은 Object.assign(destination, a, b) , {...a, ...b} 와 같은 새 속성으로 기존 객체를 확장하여 객체를 합성합니다.
  • 객체 합성에 대한 서로 다른 정의들은 상호 배타적인 것이 아닙니다. 위임은 집합의 하위 집합이며, 연결을 사용하여 위임과 집합을 구현하는 등의 작업을 할 수 있습니다.

단지 세 가지 종류의 객체 합성만 있는 것이 아닙니다. 또한 객체가 다른 객체에게 매개 변수로 전달되는(의존성 삽입) 등의 관계를 통해 객체간에 느슨하고 동적인 관계를 형성 할 수도 있습니다.

모든 소프트웨어 개발은 합성입니다. 쉽고 유연한 방법도 있고 부서지기 쉬운 관절염도 있습니다. 객체 합성의 일부 형태는 느슨하게 연결된 관계를 형성하고, 다른 형태는 매우 단단한 결합을 형성합니다.

프로그램 요구사항이 변경되었을 때 코드 구현을 조금만 변경하기 위해선 다양한 합성 형태를 찾아보십시오. 의도를 명확하고 간결하게 표현하고 기억하십시오 : 클래스 상속이 필요하다고 생각이 들 때 사실 더 좋은 방법이 많이 있을 것입니다.

다음: 삼항연산자의 멋짐을 모르는 당신이 불쌍해 >


  1. monolithic 단일체의, 한 덩어리로 뭉친, 단일 결정으로된

  2. 병합, 연결, 결합과 같은 용어로 번역됩니다. 배열, 문자열의 기본 연산에서 등장하는 용어이지만 객체를 합친다는 문맥에서는 접합이 적합하다고 판단했습니다