프록시 (Proxy, Surrogate)
"다른 객체에 대한 액세스를 제어"
Subject라 하는 다른 객체에 대한 액세스를 제어하는 객체 (subject == original)
프록시와 subejct 는 동일한 인터페이스를 가지며 이를 통해 투명하게 하나를 다른것으로 바꿀 수 있음
실행 되는 작업의 전부 또는 일부를 가로채 해당 동작을 증강하거나 보완
프록시는 각 작업을 subject에 전달하여 추가적인 전/후 처리로 동작을 향상
* 클래스간의 프록시 아님, 대상의 실제 인스턴스를 감싸 내부 상태를 보존
유용한 상황
- 데이터 검증 : 입력의 유효성 검사
- 보안 : 클라이언트 권한 확인
- 캐싱 : 프록시로 내부에 캐시 유지
- 느린 초기화 : Subject 생성에 많은 비용이 들 경우 필요 시점까지 초기화 지연
- 기록 : 호출 매개변수를 가로채고 이벤트 발생 시 기록
- 원격 객체 : 원격 개체를 가져와 로컬로 표시
1. 프록시 구현 기술
모든 함수를 가로채거나 일부만 가로채고 나머지는 Subject가 직접 처리하도록 위임
간단하게 계산기 예제를 봅시다.
// StackCalculator.js
class StackCalculator {
constuctor () {
this.stack = [];
}
putValue (value) {
this.stack.push(value);
}
getValue () {
return this.stack.pop();
}
peekValue() {
return this.stack[this.stack.length - 1];
};
clear() {
this.stack = [];
}
divide () {
const divisor = this.getValue();
const dividend = this.getValue();
const result = dividend / divisor;
this.putValue(result);
return result;
}
multiply () {
const mutiplicand = this.getValue();
const multiplier = this.getValue();
const result = multiplier * multiplicand;
this.putValue(result);
return result;
}
};
devide()에서 오류가 있음
JS 에서 0으로 나누면 Infinity 값 반환, 다른 언어는 패닉으로 런타임 에러 발생
그래서 0으로 나눌 경우 오류 발생하도록 변경
몇가지 방법으로 해볼게유
1-1. 객체 컴포지션
컴포지션은 기능을 확장해서 사용하기 위해 객체를 다른 객체와 결합하는 기술
특정 프록시 패턴의 경우 Subject와 동일한 인터페이스를 가진 객체 생성
Subject에 대한 참조가 인스턴스 변수나 클로저 변수의 형태로 프록시 내부에 저장
Subject는 생성시 사용자가 주입시키거나 프로시 자체에서 생성될 수 있음
// SafeCalculator.js
class SafeCalculator {
constuctor(calculator) {
this.calculator = calculator;
}
// 프록시 함수
divide () {
// 추가적 검증 로직
const divisor = this.calculator.getValue();
if (divisor === 0) {
throw Error('Division by 0');
}
// Subject 에 대한 유효한 위임자(delegate) 일 경우
return this.calculator.divide();
}
// 위임된 함수들
putValue(value) { return ...}
getValue() { return this.calculator.getValue() }
peekValue() { return this.calculator.peekValue() }
clear() {...}
multiply() {...}
}
SafeCalculator 객체는 calculator 인스턴스의 프록시
해당 객체에서 multiply() 호출하면 calculator의 동일한 함수 호출
divide()는 프록시에서 수행
기능을 변경하려는 함수는 가로채고 그렇지 않은경우 위임
주의할점! calculator(스택내의 값들) 상태는 여전히 calculator 인스턴스에 의해 유지됨
SafeCalculator 는 필요에 따라 값을 읽거나 변경을 위해 calculator 함수를 호출
객체 리터럴과 팩토리 함수를 사용할 수 있음
function createSafeCalculator(calculator) {
return {
// 프록시 함수
divide() {
const divider = calculator.peekValue();
if (divisor === 0) {
throw Error('Division by 0');
}
return calculator.divide();
},
// 위임 함수, 객체 리터럴 방식
putValue(value) { return ...},
getValue() { return this.calculator.getValue() },
peekValue() { return this.calculator.peekValue() },
clear() {...},
multiply() {...},
}
}
const calculator = new StackCalculator();
const SafeCalculator = createSafeCalculator(calculator);
// ...
이 구현법은 클래스 기반의 구현보다 간단하고 간결
모든 함수를 Subject에 명시적으로 위임해야 함
모든 함수를 작성해야 하기 때문에 매우 반복적 => 귀찮음
대부분의 함수 위임할 프록시를 만들어주는 delegates 라이브러리 사용
현대적이고 근본적인 대안은 Proxy 객체 사용
1-2. 객체 확장 (Object argumentation)
객체 확장 (몽키 패치) : 몇몇 함수를 프록시하는 가장 간단하고 일반적인 방법
함수의 프록시의 구현으로 대체, Subject 를 직접 수정하는 작업을 포함
계산기를 살짝 변경해봅시다.
function patchToSafeCalculator (calculator) {
const divideOrig = calculator.divide;
calculator.divide = () => {
// 추가적 검증 로직
cont divisor = calculator.putValue();
if (divisor === 0) {
throw Error("Division by 0");
}
// Subject 에 유효한 위임자일 경우
return divideOrig.apply(calculator);
}
return calculator;
}
const calculator = new StackCalculator();
const SafeCalculator = patchToSafeCalculator(calculator);
// ...
모든 함수를 위임할 필요가 없어 1~2개의 함수를 프록시할 때 편함
다만, 단순하게 대상 객체를 직접 변경하기 때문에 위험함...
* Subject 는 코드기반으로 다른부분과 공유되어있으므로 무조건 피애햐 하는 방법
몽키패치를 하면 다른 컴포넌트에 영향을 미치기 때문에 예기치않은 부작용 발생
Subject가 제한된 컨텍스트나 프라이빗 범위에 있는 경우에만 사용하세요!
1-3. 내장 프록시 객체
ES20215 Proxy 객체는 생성자가 대상과 핸들러를 인자로 받음
const proxy = new Proxy(target, handler);
target : 적용될 객체 = subject
handler : 프록시의 동작을 정의하는 객체 (수행시 자동으로 호출되는 트랩함수 (apply, get ,set, has ...) 라는 미리 정의된 이름의 부가적인 함수들이 포함됨
const safeCalculatorHandler = {
get : (target, property) => {
if (property === 'divide') {
// 프록시 된 함수
return function() {
// 추가적인 검증 로직
const divisor = target.peekValue();
if (divisor == 0) {
throw Error('Division by 0');
}
// Subjkect 에 대한 유효한 위임자일 경우
return taret.divide();
}
}
// 위임된 함수들과 속성들
return target[property];
}
}
const calculator = new StackCalculator();
const SafeCalculator = new Proxy(
calculator, safeCalculatorHandler
)
// ...
원래 객체의 속성과 함수에 대한 접근을 가로채기 위해 get 트랩 함수를 사용
결과적으로 proxy 객체는 subject의 프로토타입을 상속함
=> safeCalculator instanceof StackCalculator = true
모든 함수와 속성을 위임할 필요가 없으며 변경하려는 부분만 프록시, 코드 변경을 방지
1-4. Proxy 객체의 추가적인 기능과 제약사항
Proxy 객체는 JS 언어 자체 깊이 통합된 기능, 객체에서 수행하는 작업 가로채고 재 지정 가능
=> 메타프로그래밍, 연산자 오버로딩, 객체 가상화 등의 사용법 제공
const evenNumebers = new Proxy ([], {
get : (target, index) => index *2,
has : (target, number) => number % 2 === 0,
})
console.log(2 in evenNumebers); // true
console.log(5 in evenNumebers); // false
console.log(evenNumebers[7]); // 14
이 예제는 평범한 배열으로 액세스 하거나 in 연산자를 사용해 특정 항목 존재여부 체크
배열은 데이터를 내부에 저장하지 않아서 가상의 배열임
구현을 보면 get, has 라는 트랩을 정의함
get : 배열 요소에 대한 접근을 가로채 주어진 인덱스에 대한 짝수 반환
has : in 연산자 사용을 가로챔,
Proxy 객체는 set, delete, construct 같은 트랩을 제공, 모든 트랩을 비활성, 원래동작으로 복원할 수 있는 프록시 생성 가능, 이거는 알아서 하셈
=> 프록시 패턴 디자인에 강력한 기반 제공
! 제약사항
- 프록시 객체는 완전히 트랜스파일/ 폴리필 될 수 없다
- 객체 트랩중 일부 런타임 수준에서만 구현 가능
1-5. 여러 프록시 기술의 비교
컴포지션 : 원래 동작을 변경하지 않고 대상을 그대로 둠 => 간단하고 안전한 방법
모든 함수들을 수동으로 위임하는 단점 존재. 접근 권한을 위임 가능
컴포지션이 반드시 필요한 상황 : 필요시에만 생성(lazy initialization), Subject 초기화를 제어하는 경우
객체확장 : Subject를 수정, Subject 를 수정해야하는 모든 상황에서 선호됨
Proxy 객체 : 함수 호출을 가로채야 하거나 객체 속성에 대한 다른 형태의 접근이 필요할 경우 선호
고급수준의 제어 제공
subject를 변경하지 않아서 어플에서 subject를 공유하고 있는 다른 컴포넌트들이 안전하게 사용 가능
변경이 불필요한 모든 함수와 속성 그대로 위임 가능
2. 쓰기 가능한 로깅 스트림 만들기
write() 에 대한 모든 호출을 가로채고 상황에 따라 메시지를 기록하는 Writable 스트림 프록시 객체
export function createLoggingWriable (writable) { // 팩토리 함수
return new Proxy(writable, {
get (target,propKey, receiver) { // 접근 가로채기
if (propKey === 'write') {
return function (...args) { // 프록시 함수를 반환함
const [chunk] = args; // 인자 목록에서 현재 청크 추출, 그 후 원래 함수 호출
console.log('Writing', chunk);
return writable.write(...args);
}
}
returntarget[propKey];
}
})
}
// index.js
import { createWriteStream } from 'fs';
iport {createLoggingWriable} from './logging-writable.js';
const writable = createWriteStream('test.txt');
const writableProxy = createLoggingWriable(writable);
writableProxy.write('first chunk');
writableProxy.write('second chunk');
Writable.write('This is not logged');
writableProxy.end();
3. 프록시를 사용한 변경 옵저버
변경 옵저버 패턴 : 객체가 하나 이상의 옵저버에게 상태 변경을 알리는 패턴, 변경 즉시 반응 가능
옵저버 패턴과는 다른데 이것은 발생 이벤트에 대한 정보를 전파하기 위해 emitter를 사용하는 패턴
프록시는 관찰 가능한 객체를 만드는 매우 효과적인 도구
// observable.js
export function cretaeObservale (target, observer) { // 대상객체(관찰할 객체), 관찰자 (변경 사항 감지시 호출 함수)
const observable = new Proxy (target, {
set (obj, prop, value) {
if (value !== obj[prop]) {
const prev = obj[prop];
obj[prop] = value;
observer({prop, prev, curr: value});
}
return true;
}
})
return observable;
}
Es2015 Proxy 를 통해 인스턴스 생성, set 트랩 구현, 현재 값을 새 값과 비교하여 다른 경우 관찰자에게 알림 전송
기능을 향상을 시키고 싶다면? 멀티 관찰자 알아보세요.
더 많은 트랩을 사용해 필드 삭제, 프로토타입 변경과 같은 다른유형의 변경 탐지 가능
재귀적 처리도 가능
최종적으로 변경 사항을 관찰하고 합계 자동 계산 어플 예시
import { createObservable } from './create-observable.js';
function calculateTotal (invoice) {
return invoice.subtotal - invoice.discount + invoice.tax;
}
const invoice = {
subtotal : 100,
discount : 10,
tax : 20,
};
let total = calculateTotal(invoice);
console.log(`Starting total : ${total}`);
const obsInvoice = createObservable( // 송장 객체와 관찰 가능한 버전을 만들어! 추척하기 위한 로그도 추가
invoice,
({prop, prev, curr }) => {
total = calculateTotal(invoice);
console.log(`Total : ${total} (${prop} changed : ${prev} => ${curr})`);
}
)
obsInvoice.subtotal =200;
obsInvoice.discount = 20;
obsInvoice.discount = 20;
obsInvoice.tax = 30;
모든 변경 사항이 관찰됨, 합계가 최신 상태로 유지됨
관찰 가능함 => 반응형 프로그래밍의 초석!
Summary
변경 옵저버 패턴은 프론트, 백엔드 에서 널리 사용되는 패턴
- LoopBack : 컨트롤러에서 함수의 호출을 가로채서 가공, 웹 프레임워크
- Vue.js
- Mobx 상태관리 라이브러리 , 프록시 객체를 사용해 반응형 관찰가능 기능 구현
'Javascript > Node.js' 카테고리의 다른 글
Adaptor Pattern - 구조적 설계 패턴 (0) | 2022.02.09 |
---|---|
Decorator Pattern - 구조적 설계 디자인 (0) | 2022.02.09 |
Wiring Pattern - 생성자 디자인 패턴 (0) | 2022.02.03 |
Singleton Pattern - 생성자 디자인 패턴 (0) | 2022.02.03 |
Domenic Revealing Constructor Pattern - 생성자 디자인 패턴 (0) | 2022.02.03 |
댓글