TOC
- 참고
- 퍼사드 패턴(Facade Pattern)
- 서브클래싱(Sub-Classing)
- 믹스인 패턴(Mixin Pattern)
- 데코레이터 패턴(Decorator Pattern)
- 의사 클래스 데코레이터 패턴
- 플라이웨이트 패턴(Flyweight Pattern)
참고
본 내용은 자바스크립트+리액트 디자인 패턴(링크) 를 읽고 정리한 내용입니다. 책의 내용과 함께 개인적인 의견과 생각을 담아 작성하였습니다.
퍼사드 패턴(Facade Pattern)
실제 모습을 숨기고 꾸며낸 겉모습만을 세상에 드러내는 패턴
특징
- 심층적인 복잡성을 숨기고, 사용하기 편리한 높은 수준의 인터페이스 제공
- 제한된 추상화 메서드만이 공개되어 사용할 수 있도록 한다.
- 클래스의 인터페이스를 단순화하고 코드의 구현 부분과 사용 부분을 분리
- 하위 시스템에 직접 접근하기보단 간접적으로 상호작용
- 사용하고 쉽고, 패턴 구현에 필요한 코드의 양이 적다는 것이 장점
예시
jquery의
$(document).ready()
메서드
function bindReady() {
// 간단한 이벤트 콜백 사용
document.addEventListener('DOMContentLoaded', DOMContentLoaded, false);
// window.onload의 대체제이며 언제나 작동
window.addEventListener('load', jQuery.ready, false);
}
- 내부적으로는 위와 같은 작업을 수행하는 bindReady() 메서드에 의존
- 내부 구현은 복잡하지만, 사용자는 간단한 인터페이스만 사용
의의
- 모듈 내부에서 비밀스러운 동작을 실행
- 하지만, 사용자는 내부에서 무슨 일이 벌어지는지 알 필요가 없다.
- 구현 수준의 세부사항을 알지 않고도 훨씬 쉽게 사용할 수 있다.
나의 생각
지금까지 이 책에서 다양한 디자인 패턴들을 접했지만, 가장 마음에 드는 패턴인 것 같아요. 목적과 사용예시, 그리고 디자인 패턴의 존재 의의인 '해결하고자 하는 문제'가 명확하다는 느낌을 받았어요. 평소에 모듈에서 export하는 인터페이스를 어떻게 구조화할 지 고민을 많이 하는데, 상황에 따라 퍼사드 패턴을 적용해보면 좋을 것 같아요.
서브클래싱(Sub-Classing)
용어 정리
-
서브클래스 : 부모 클래스를 확장하는 자식 클래스
-
서브클래싱 : 부모 클래스 객체에서 속성을 상속받아 새로운 객체를 만드는 것
-
메서드 체이닝 : 서브클래스의 메서드가 오버라이드된 부모 클래스의 메서드를 호출하는 것
-
생성자 체이닝 : 부모 클래스의 생성자를 호출하는 것
-
ES2015+에서 도입
-
서브클래스는 부모 클래스에서 먼저 정의된 메서드를 오버라이드 할 수 있다.
믹스인 패턴(Mixin Pattern)
특징
- 최소한의 복잡성으로 객체의 기능을 빌리거나 상속
- 다른 여러 클래스를 아울러 쉽게 공유할 수 있는 속성과 메서드를 가진 클래스
- 다중 상속을 지원하지 않는 언어에서 다중 상속을 구현하는 방법
- 자바스크립트의 클래스는 부모 클래스를 하나만 가질 수 있다.
- 여러 클래스의 기능을 섞는 mixin을 사용해 문제 해결 가능
사용 예시
const myMixin = (superclass) => class extends superclass {
moveUp() {...}
moveDown() {...}
stop() {...}
};
class CarAnimator {
moveLeft() {...}
}
class PersonAnimator {
moveRandomly() {...}
}
// 믹스인을 사용해 다중 상속 구현
class MyAnimator extends myMixin(CarAnimator) {}
// mixin으로 덮어쓰여진 메서드 사용 가능
const myAnimator = new MyAnimator();
myAnimator.moveLeft();
myAnimator.moveUp();
myAnimator.stop();
- class는 표현식(expression) 뿐만 아니라 문(statement)으로도 사용 가능
- 표현식은 평가될 때마다 새로운 클래스를 반환
- extends 절은 클래스나 생성자를 반환하는 임의의 표현식을 허용
- 부모 클래스를 받아 새로운 서브클래스를 만들어내는 믹스인 함수를 정의
장점과 단점
장점
- 함수의 중복을 줄이고 코드의 재사용성을 높일 수 있다.
- 믹스인을 통해 중복 기능을 공유하여 중복을 피할 수 있다.
- 고유 기능을 구현하는 데에 집중할 수 있다.
단점
- 일부에서는 클래스나 객체의 프로토타입에 기능을 주입하는 것을 나쁜 방법이라고 여긴다.
- 프로토 타입 오염과 함수 출처에 대한 불확실성을 초래하기 때문
나의 생각
예시를 봤을 때 mixin은 class에서 구성하는 메서드를 overriding하는 정도의 원본 수정으로 보였어요. 이런 것들이 JS의 프로토타입을 건드려 재구성하거나 수정하는 방법처럼 근본을 흔드는 패턴은 아닌 것 같아 보였는데, 그렇지 않나요?
데코레이터 패턴(Decorator Pattern)
코드 재사용을 목표로 하는 패턴
- 객체 서브클래싱의 다른 방법
- 기존 클래스에 동적으로 기능을 추가하기 위해 사용
- 클래스의 기본 기능에 필수적이지 않은 요소들을 덮어씌우기 위한 기능
특징
- 기존 시스템의 내부 코드를 힘겹게 바꾸지 않고도 기능을 추가
- 애플리케이션의 기능이 다양한 타입의 객체를 필요로 할 때 유용
- 객체의 생성을 신경 쓰지 않는다.
- 다만 기능의 확장에 좀 더 초점을 맞춘다.
- 베이스 클래스에 추가 기능을 제공하는 데코레이터 객체를 점진적으로 추가
- 서브 클래싱 대신 베이스 객체에 속성이나 메서드를 추가하여 간소화
사용 예시
데코레이터는 클래스 자체를 변경하는 게 아니라, 이미 존재하는 클래스/인스턴스에 기능을 동적으로 추가하는 것이라는 점에서 아래 예시들을 이해해보자.
먼저 이렇게 Vehicle이라는 클래스를 만들어 보았다. 이 클래스는 기본적인 차량 정보를 가지고 있다.
// Vehicle 클래스
class Vehicle {
constructor(vehicleType) {
this.vehicleType = vehicleType || 'car';
this.model = 'default';
this.license = '00000-000';
}
}
그리고 이 Vehicle 클래스를 통해 만든 기본 인스턴스를 만들어보았다. 이 기본 인스턴스는 이후 데코레이터 패턴의 적용 대상이 아니기 때문에 그대로 유지될 것을 기대하고 있다.
// 기본 Vehicle 인스턴스
const testInstance = new Vehicle('car');
console.log(testInstance);
// Vehicle { vehicleType: 'car', model: 'default', license: '00000-000' }
그렇다면 이제부터는 다른 인스턴스를 하나 더 만들어서 테코레이터 패턴을 적용해보자.
// 데코레이트될 새로운 Vehicle 인스턴스 truck 생성
const truck = new Vehicle('truck');
// 데코레이터 패턴을 사용해 truck 인스턴스에 기능 추가
truck.setModel = function(modelName) {
this.model = modelName;
};
truck.setColor = function(color) {
this.color = color;
};
그리고 decorate한 뒤 truck 인스턴스를 출력해보면 아래와 같이 기존의 Vehicle 인스턴스와는 다른 속성을 가지고 있음을 확인할 수 있다.
truck.setModel('CAT');
truck.setColor('blue');
console.log(truck);
// Vehicle { vehicleType: 'truck', model: 'CAT', license: '00000-000', color: 'blue' }
그렇다면 다시 Vehicle 클래스를 통해 만든 기본 인스턴스를 출력해보자. 데코레이터 패턴이 기존 클래스를 변경하지 않고 인스턴스에만 적용한다면 아래와 같이 기본 인스턴스는 변하지 않을 것이다.
const secondInstance = new Vehicle('car');
console.log(secondInstance);
// Vehicle { vehicleType: 'car', model: 'default', license: '00000-000' }
사용 예시2
데코레이터 패턴의 또다른 예시
맥북이라는 클래스에서 여러 옵션에 따라 금액이 달라지는 예시를 살펴보자.
우선 아래처럼 기본 맥북에 대한 크래스를 만들어보았다.
class MacBook {
constructor() {
this.cost = 997;
this.screenSize = 11.6;
}
getCost() {
return this.cost;
}
getScreenSize() {
return this.screenSize;
}
}
그리고 아래처럼 각 옵션들에 대해 데코레이터 클래스를 만들어보았다.
// decorator 1
class Memory extends MacBook {
constructor(macbook) {
super();
this.macbook = macbook;
}
getCost() {
return this.macbook.getCost() + 75;
}
}
// decorator 2
class Engraving extends MacBook {
constructor(macbook) {
super();
this.macbook = macbook;
}
getCost() {
return this.macbook.getCost() + 200;
}
}
// decorator 3
class Insurance extends MacBook {
constructor(macbook) {
super();
this.macbook = macbook;
}
getCost() {
return this.macbook.getCost() + 250;
}
}
그리고 아래처럼 각 데코레이터 클래스를 감싸 새로운 인스턴스를 만들듯이 사용했다.
// 맥북 인스턴스 생성
let mb = new MacBook();
// 데코레이터 패턴을 사용해 각 옵션을 추가
mb = new Memory(mb);
mb = new Engraving(mb);
mb = new Insurance(mb);
// 최종 가격 출력
console.log(mb.getCost()); // 1522
// 최종 화면 크기 출력
console.log(mb.getScreenSize()); // 11.6
나의 생각
사용 예시가 잘못됐나?
예시 코드1, 2를 분석해 분리하며 정리해봤지만, 인스턴스를 인자로 받아 다시 클래스를 감싸 사용하는 이런 패턴들이 그리 매력적인지 모르겠더라고요. 차라리 맥북의 모든 옵션을 class 내부에 다 넣어두고, 필요한 옵션을 선택적으로 붙인 뒤, 인스턴스를 다시 반환하는 빌더 패턴이 더 깔끔하지 않을까 싶더라고요.
먼저 이런 풀스택 클래스를 하나 만들었어요. (add하는 여러 메서드들은 decorator 패턴을 사용해서 분리한 뒤, 서브클래싱을 통해 풀스팩을 만들어도 좋겠다는 생각이 드네요.)
class MacBook {
constructor() {
this.cost = 997;
this.screenSize = 11.6;
}
addMemory() {
this.cost = this.cost + 75;
return this;
}
addEngraving() {
this.cost = this.cost + 200;
return this;
}
addInsurance() {
this.cost = this.cost + 250;
return this;
}
getCost() {
return this.cost;
}
getScreenSize() {
return this.screenSize;
}
}
그리고 아래처럼 각 메서드들을 붙여서 사용하는 거죠.
// 맥북 인스턴스 생성
const mb = new MacBook();
// 빌더 패턴을 사용해 각 옵션을 추가
mb.addMemory()
.addEngraving()
.addInsurance();
console.log(mb.getCost()); // 1522
각 상황에 따라 필요한 패턴들이 있겠지만, 이런 경우에는 빌더 패턴이 더 깔끔하게 보이는 것 같아요. 아니면 아직 제가 진가를 이해하지 못했을 수도 있겠다는 생각이 들어요😂 혹시 저를 깨워주실 분이 있으시다면 좋은 의견 부탁드릴게요.
예상 밖이네?
파이썬에서 함수 위에 @function_decorator처럼 올려 사용하는 것이 데코레이터 패턴의 예시라고 알고 있었어요.
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
# 결과물
# "Something is happening before the function is called."
# "Hello!"
# "Something is happening after the function is called."
비슷한 코드를 볼 것 같아 기대했는데, 조금 의외였어요. 역시 자바스크립트 언어의 구현은 확실히 다르게 보이는 것 같아요.
Copilot의 데코레이터 패턴
예시 코드를 분석해서 잘라서 작성하고 있는데, 코파일럿의 자동 완성이 이런 코드를 추천해줬어요. 이런 방법도 있겠구나 싶어서 기록해두려고 해요.
class Vehicle {
constructor(vehicleType) {
this.vehicleType = vehicleType || 'car';
this.model = 'default';
this.license = '00000-000';
}
}
function decorateVehicle(vehicle) {
vehicle.setModel = function(modelName) {
this.model = modelName;
};
vehicle.setColor = function(color) {
this.color = color;
};
return vehicle;
}
const truck = decorateVehicle(new Vehicle('truck'));
truck.setModel('CAT');
truck.setColor('blue');
console.log(truck);
// Vehicle { vehicleType: 'truck', model: 'CAT', license: '00000-000', color: 'blue' }
의사 클래스 데코레이터 패턴
데코레이터 패턴의 변형 버전
인터페이스(Interface)
- 객체가 가져야 할 메서드를 정의하는 방법
- 스스로 문서의 역할을 하고 재사용성을 높인다.
- 인터페이스의 변경사항이 객체의 구현에도 전달되게 하면서 코드의 안정성을 높인다.
나의 생각
타입스크립트에서 사용하고 있던 Interface였기 때문에 기대했던 패턴이었는데 생각보다는 와닿지 않았어요. 자바스크립트에서 내장된 기능도 아니었고, 직접 구현하는데 pseudo로 표현되어 있어서 사용 예시도 생략하고 넘어가려고 해요.
추상 데코레이터(Abstract Decorator)
필요한만큼의 데코레이터만을 사용하여 베이스 클래스에 독립적으로 기능을 추가해보자.
기존의 데코레이터 패턴으로 서브클래스를 만든다면 무수히 많은 서브 클래스가 필요하다. 이론상 n개의 옵션이 있다면 2^n개의 서브클래스가 필요한 것이다.
const MacBook = class {};
const MacBookWith4GBRam = class {};
const MacBookWith8GBRam = class {};
const MacBookWith4GBRamAndEngraving = class {};
const MacBookWith8GBRamAndEngraving = class {};
...
그러면 이걸 어떻게 해결하는지 살펴보자.
const MacBook = new Interface('MacBook', ['addEngraving', 'addParallels', 'add8GBRam', 'addCase']);
class MacBookPro {...}
// 내부적으로는 같은 구조를 사용하기 때문에
// ES2015+의 Object.prototype을 사용하여 새로운 메서드 추가
MacBookPro.prototype = {
addEngraving() {...}
addParallels() {...}
add8GBRam() {...}
addCase() {...}
getPrice() {
return 900.00;
}
}
MacBook 객체를 받아 베이스 컴포넌트로 사용하는 MacBook 데코레이터 클래스를 만들어보자.
class MacBookDecorator {
constructor(macbook) {
Interface.ensureImplements(macbook, MacBook);
this.macbook = macbook;
}
addEngraving() {
return this.macbook.addEngraving();
}
addParallels() {
return this.macbook.addParallels();
}
...
getPrice() {
return this.macbook.getPrice();
}
}
그렇다면 이 데코레이터는 어떻게 사용할까?
// MacBookPro 인스턴스 생성
const mb = new MacBookPro();
// 데코레이터 패턴을 사용해 각 옵션을 추가
const decoratedMacBook = new MacBookDecorator(mb);
decoratedMacBook.addEngraving();
decoratedMacBook.addParallels();
decoratedMacBook.add8GBRam();
decoratedMacBook.addCase();
console.log(decoratedMacBook.getPrice()); // 1200
또한, CaseDecorator와 같은 다른 데코레이터 클래스를 만들어 MacBookDecorator 클래스에 추가할 수 있다.
class CaseDecorator extends MacBookDecorator {
constructor(macbook) {
super(macbook);
}
addCase() {
return `${this.macbook.addCase()}Adding Case to macbook`;
}
getPrice() {
return this.macbook.getPrice() + 45.00;
}
}
이렇게 CaseDecorator 클래스를 이용해 이전의 addCase와 getPrice 메서드를 오버라이드할 수 있다. 결과는 아래와 같다.
// MacBookPro 인스턴스 생성
const mb = new MacBookPro();
console.log(mb.getPrice()); // 900
// 데코레이터 패턴을 사용해 각 옵션을 추가
const decoratedMacBook = new CaseDecorator(mb);
console.log(decoratedMacBook.getPrice()); // 945
장점과 단점
장점
- 베이스 객체가 변경될 걱정 없이 사용 가능
- 수많은 서브클래스에 의존할 필요도 없다.
단점
- 네임스페이스에 작고 비슷한 객체를 추가하므로 구조의 복잡도를 높인다.
- 패턴에 익숙하지 않은 개발자에게는 파악하기 어려운 코드가 될 수 있다.
나의 생각
위에서 언급되었던 그냥 decorator 패턴보다는 제 의문을 해결하는 방식인 것 같아요. 잘 이해해서 실무에 활용해보면 좋을 것 같아요.
플라이웨이트 패턴(Flyweight Pattern)
반복되고 느리며 비효율적으로 데이터를 공유하는 코드를 최적화하는 전통적인 구조적 해결 방법
- 연관된 객체끼리 데이터 공유
- 메모리 사용량을 최소화하고 성능을 향상시키는 패턴
- 플라이웨이트(플라이급) : 패턴의 목표가 메모리 공간의 경량화이기 때문에 이렇게 명명
사용법
- 데이터 레이어에서 메모리에 저장된 수많은 비슷한 객체 사이로 데이터 공유
- DOM 레이어 (이벤트 위임 등)
데이터 공유
-
여러 비슷한 객체나 데이터 구조에서 공통으로 사용되는 부분만을 하나의 외부 객체로 내보낸다.
-
각 객체에서 데이터를 저장하기보다는 하나의 의존 외부 데이터에 모아서 저장
-
내재적 상태
- 객체의 내부 메서드에 필요
- 없으면 절대 동작 불가능
- 같은 내재적 정보를 가진 객체를 팩토리를 사용해 만들어진 하나의 공유된 객체로 대체 가능
- 저장된 내부 데이터의 양 감소
-
외재적 상태
- 제거되어 외부에 저장 가능
- 외재적 상태를 다룰 때는 따로 관리자를 사용
- 플라이웨이트 객체와 내재적 상태를 보관하는 중앙DB를 관리자로 사용
사용 예시
수천 개의 책 객체를 다루는 예시
- 책에는 책의 구성에 필수적인 요소(내재적 요소)와 대출에 관련된 요소(외재적 요소)가 있다.
- 내재적 요소는 책 객체 내부에서, 대출은 외부의 대출 관리자 클래스에서 별도로 처리
- 이렇게 되면 책 객체를 정의하고 관리할 때 공통 요소들 중 일부가 중앙 관리되어 책 객체 각각이 가벼워진다.
- 모든 객체들에 함수들이 제각각 존재하는 것이 아니라, 관리자 내부에 한 번만 존재하게 되어 메모리 사용량이 줄어든다.
나의 생각
이 예시가 플라이웨이트 패턴이 뭘 해결하고자 했는지 명확히 설명하는 느낌이었어요.
DOM 객체
DOM의 이벤트 버블링에서 하위 요소의 이벤트를 상위 요소에 위임하는 예시
- 이벤트 버블링 : 하위 요소에서 상위 요소로 이벤트가 전파되는 현상
- 이벤트 위임 : 상위 요소에 이벤트를 위임하여 하위 요소의 이벤트를 처리하는 방법
- 여러 요소들에 하나하나 클릭 이벤트를 바인딩하는 대신, 최상위 컨테이너에 플라이웨이트 부착
- 하위 요소로부터 전달되는 이벤트를 감지하여 하나의 이벤트 핸들러로 처리
장점
- 개별적으로 관리되었던 많은 동작을 하나의 동작으로 바꿔 공유한다.
- 이를 통해 메모리를 절약할 수 있다.
나의 생각
React에서도 이벤트 위임에 대한 이해를 위해 예시들은 많은데, 실제로 메모리 성능만을 위해 저런 최적화를 많이 안 하지 않나요?