85 거침없는 자바스크립트 5회차

이누의 벨로그·2022년 3월 22일
0

거침없는 자바스크립트 5회차 코드스피츠 유튜브

저번 시간에 만들었던 AIter 클래스 타입과 그를 이용한 제네레이터 함수들을 조금 더 객체지향적으로 고쳐보자.

앞서 이러한 dataLoader를 만들었었다.

const dataLoader = async function* (...aIters) {
  let prev;
  for (const iter of aIters) {
    iter.update(prev); //이전 값으로 업데이트 한 뒤 API 호출
    prev = (await iter.load().next()).value;
    yield prev;
  }
};

이 dataLoader 함수는 prev에 대한 다양한 정책을 사용할 수 있다. 현재는 예전 인자를 넘겨주고 있지만 배열에 json을 계속해서 누적해나갈 수도 있을 것이다. 헌데 우리는 해당 정책을 바꿀 때마다 코드를 변경해야 한다. 따라서 우리는 prev 정책에 따라 다른 정책을 가진 인스턴스를 만들 수 있도록 추상화된 객체를 만들려 한다.

추상화란 무엇인가? 흔히들 객체지향에서의 추상화공통점을 찾는 것 이라고 생각하고, 구상이란 무언가 차이가 나는 부분에 대한 것이라고 생각한다. 하지만 개발에서의 추상화라는 것은 훨씬 더 단순한 개념이다. 바로 보다 덜 변하는 것 이 바로 추상화의 대상이다. 코드에 있어서 추상화란 바로 변화율이 작은 것 을 말하며 추상 클래스는 변화율이 작은 클래스이며 ’인터페이스는 변화율이 작은 함수 시그니처를 기술한 것이다. 따라서 우리는 덜 변하는 부분 을 찾을 때마다 더 추상화를 시키고, 변화율의 차이가 있는 부분을 발견할 때마다 계층을 나누어서 추상 계층을 만들게 된다. 이렇게 변화율에 따라 코드를 나누어 관리하는 이유는 결국 변화가 일어나는 부분에 대해서만 코드를 고치고 그 외의 부분에는 영향이 없도록 하고 싶기 때문이다.

설계란 결국 코드를 재배치 하는 것이다. 재배치하는 이유는 결국 항상 변화하는 프로그램에서 변화가 일어나는 부분만을 고치고 회귀 테스트를 할 필요성을 없애기 위함이다.

그렇다면 우리의 dataLoader함수를 추상화하여 변화율에 따라 분리해보자. prev 변수가 가장 변화율이 높은 부분이므로 이 부분을 분리한 사용코드를 먼저 작성해보자.

const dataLoader = async function ( ...aIters) {
  const dataPass = new DataPass();
  for (const item of aIters) {
    const v = await item.load(dataPass.data).next();
    yield dataPass.data = v.value; //setter
  }
};

우선, update 메서드를 호출하는 코드가 없어진 것을 볼 수 있다. AIter를 사용하는 코드는 dataLoader 뿐인데, dataLoader는 언제나 update 후에 load를 호출하고 있다. 이것은 곧, update와 load는 하나의 transaction 이라는 뜻이다. 트랜잭션은 DB에서는 DB가 처리하는 응용프로그램과의 상호작용의 단위를 의미하고, 응용 프로그램에서는 한 번에 수행되야 하는 코드의 단위가 되어 보통 하나의 메서드의 단위가 된다. 따라서 우리는 update와 load를 하나의 메서드로 만들어야 한다. transaction이 하나의 메서드의 단위가 되는 이뉴는, 제어문은 트랜잭션의 순서를 보장할 수가 없지만 함수로 만들게 되면 단 하나의 함수 시그니쳐를 가짐으로써 항상 순서를 보장할 수 있기 때문이다. 그렇다면 우리는 update를 없애고 load 함수에서 같이 처리하도록 할 것이다.

앞서 코드와 어떤 것이 바뀌었는가? 바로 prev에 대한 정책을 DataPass 클래스에 위임해버렸다. prev 대한 다양한 정책을 코드를 고치지 않고 새로운 객체를 합성하고 객체를 교체함으로써 확장 가능성을 열었다. 이제 dataLoader는 보다 일반화 되었으며 OCP를 준수하는 좀 더 객체지향적인 코드가 되었다.

그렇다면 dataLoader의 사용코드를 기반으로 DataPass 클래스를 작성해보자

const DataPass = class{ 
  get data() {  throw "override";}
  set data(v) { throw "override";}
}
const PrevPass = clas extends DataPass{
	#data;
	get data(){return this.#data;}
	set data(v){this.#data = v;}
}
const IncPass = class extends DataPass {
  #data = [];
  get data() {return this.#data;}
  set data(v) {this.#data.push(v);}
}

왜 getter와 setter 밖에 없냐면 dataLoader에서 data getter와 setter만 사용하고 있기 때문이다. 클래스 getter와 setter는 함수라는 사실을 이해해야 한다. 즉 다른 메서드와 마찬가지로 오버라이드가 가능하다. 구상클래스인 PrevPass는 계속해서 #data 프로퍼티를 새롭게 갱신하고 이를 getter로 제공한다. 그럼 #data를 배열로 누적시키는 IncPass 클래스도 간단하다.

이렇게 구상 클래스가 추상 클래스의 오퍼레이션에 의존하는 의존성 역전을 이뤘으니 이제 우리는 dataLoader가 추상클래스를 인식하도록 하고 런타임에 외부에서 구상 클래스를 공급받으면 제어 역전을 이룰 수 있다.

const render = function(,...aIters){
  for await(const json of dataLoader(PrevPass, ...aIters){
		console.log(json)
	}
}
const dataLoader2 = async function (pass, ...aIters) {
  const dataPass = new pass();
  for (const item of aIters) {
    const v = await item.load(dataPass.data).next();
    yield((dataPass.data = v.value)); //setter
  }
};

추상 클래스의 일반화된 제어로 제어를 역전시키고 필요한 구상 클래스는 런타임에 외부에서 공급하는 데에 성공했다.이렇게 런타임 바인딩은 실제 실행되는 시점에 내가 어떤 객체를 사용할지를 결정하는 것을 말한다. 이는 코드 상에서 결정되어 있지 않고 실행 도중에 바꿔주는 것으로 OCP 원칙을 준수하는 방법이다. 이렇게 추상 수준에서 제어의 일반화를 달성하고 외부에서 객체를 공급받는 것을 디자인 패턴의 전략 패턴이라고 한다.

우리가 if 분기문을 제거할 수 있는 방법이 바로 이러한 과정을 전부 수행하는 것 뿐이다. 런타임에 전략 객체를 받아들이고, 전략 객체는 추상 클래스/ 인터페이스로부터 정확히 if 분기문의 개수 만큼의 수가 필요하다. 왜 if문을 제거하려고 하는가? 바로 코드를 테스트 해볼 때 필요한 케이스 분기가 없기 때문이다. 코드가 작동하거나/ 작동하지 않거나의 2가지 경우로 전부 귀결되는 것이다. 유지보수가 훨씬 용이함은 말할 필요가 없다.

그런데 우리는 dataLoader의 역할을 한번 더 점검할 필요가 있다. dataLoader가 하는 역할은 무엇인가? pass 클래스 타입과 AIter 클래스 타입을 매개변수로 받아 루프를 돌며 AIter 타입의 load메서들을 실행하고, pass 클래스의 data를 set 해주고 있다. 그런데, 이러한 행위가 dataLoader의 책임인지를 확인해야 한다. AIter 타입은 스스로의 load 메서드에 대한 지식을 이미 가지고 있으니, load 메서드를 실행하며 루프를 도는 이 행위가 AIter 스스로의 책임이 아닐까? 즉, 우리는 지식을 가지는 주체에게 해당 지식을 활용하는 행위에 대한 책임을 부여할 필요가 있는 것이므로, dataLoader라는 함수는 사실 필요없다. 오히려 이를 통해 쓸데없는 의존성 전파만 일어나고 있다. 따라서 우리는 다음과 같은 스태틱 메서드를 만들것이다. 참고로 AIter라는 이름 대신, 좀 더 역할에 어울리는 AsyncItem으로 이름을 변경했다.

const render2 = async function (...aIters) {
  for await (const json of AsyncItem.iterable(PrevPass, ...aIters)) {
    console.log(json);
  }
};

static iterable 메서드를 AsyncItem 클래스에 만듦으로써 스스로에 대한 지식을 사용하게 하고, AsyncItem이 직접 pass 클래스 타입을 알게 할 것이다. 이로써 dataLoader함수가 만들어내는 불필요한 의존성 2개를 1개로 줄일 수 있다. for await of 문법을 사용하기 위해서는 iterable 메서드는 async Iterable 이여야 한다. 즉 [Symbol.AsnycIterator] 메서드를 가지고 있어야 한다.

class AsyncItem{
	static iterable(dataPass, ...items) {
	    AsyncItem.#dataPass = dataPass;
	    AsyncItem.#items = items;
	    return AsyncItem; //스태틱 객체를 리턴하므로 이터레이터 메소드도 스태틱 수준에서 가지고 있어야함.
	}
}

스태틱 메서드로 스태틱 클래스를 반환하고 AsyncItem 클래스 안에 static [Symbol.AsyncIterator] 메서드를 만들면 된다. 이 때 iterable 메서드 인자로 다른 클래스 타입 객체들을 전달할 것이다. 자바스크립트는 싱글 쓰레드 언어이기 때문에 스태틱 변수를 객체간의 값을 전달하는 일종의 스태틱 브릿지처럼 사용할 수도 있다. 다른 멀티 쓰레드 언어에서는 메모리 공유 문제가 일어나므로 불가능하다. 즉 자바스크립트는 스태틱 변수에 대한 동기 명령의 원자성 atomicity가 보장되므로 스태틱 변수와 값을 주고 받거나 돌려받는 스태틱 브릿지가 가능한 것이다. 그럼 Async Iterable의 인터페이스를 그대로 구현해보자.

주의할 점은 우리가 구현하는 이터러블과 이터레이터 인터페이스 모두 인스턴스 수준에서 일어나는 일이 아니라 스태틱 수준에서 일어난다. 따라서 new AsyncItem으로 인스턴스를 생성하면 이터러블이 되지 않고, 오직 iterable 메소드를 통해 반환한 스태틱 객체만이 이터러블이 된다.

class AsyncItem{
	static async *[Symbol.AsyncIterator](){
		const dataPass = AsyncItem.#dataPass;
    for (const item of AsyncItem.#items) {
      const v = await item.load(dataPass.data).next();
      yield (dataPass.data = v.value);
    }
	}
}

Symbol.AsyncIterator 함수는 제네레이터 함수로 만들거나, 아니면 이터레이터 객체를 반환하도록 구현할 수 있다. 제네레이터 함수로 구현하면 그 자체로 이터러블이면서 이터레이터인 제네레이터 객체 를 반환한다고 앞서 살펴보았다. 제네레이터 함수가 아니라면 next() 메서드를 가지고 {value,done}를 리턴하는 객체를 리턴하면 된다.

앞서 iterable의 매개변수로 받아 static 브릿지에 저장했던 변수를 사용해서 정확하게 dataLoader와 똑같은 작업을 수행하고 있다. 이렇게 만들고 보니, 이제 AsyncItem이 자기 자신에 대한 지식을 사용해서 작업을 수행하고 있는 것을 알 수 있다. 이전에 비해 보다 책임을 명확하게 분배하여 불필요한 의존성을 제거한 것을 볼 수 있다.

AsyncItem.iterable의 반환값은 이제 Async제네레이터 객체 가 되어 await AsyncItem.iterable().next() 또는 for await(const itme of AsyncItem.iterable()) 과 같은 형태로 사용할 수 있다.

여기까지 왔지만 불만사항이 하나 남아있다. 우리는 여전히 render함수 내부에서 PrevPass를 하드코딩 하고 있다. 동적 바인딩했다고는 하지만 render 함수를 고쳐야하는 노릇이니 여전히 객체지향적이지 않다. render함수 또한 매번 생성 때마다 pass 클래스 타입을 외부에서 주입받아서 OCP를 지킬 수 있도록, render함수를 Renderer 클래스로 만들어보자.

const Renderer = class {
  #dataPass;
  constructor(dataPass) {
    this.dataPass = dataPass;
  }
  set dataPass(v) {
    this.#dataPass = v;
  }
	async render(...items) {
    const iter = AsyncItem.iterable(this.#dataPass, ...items);
    for await (const v of iter) console.log("**", v);
  }
};

위 코드에서 눈여겨볼 점은 생성자에서 프라이빗 변수에 직접할당하지 않고 setter 함수를 사용하고 있다는 점이다. setter를 만들었다는 얘기는 setter함수를 통해 변수에 할당하는 작업을 위임하여 무언가 추가적인 작업을 수행하겠다는 것이다. 그것이 validation이 될 수도 있고, 다른 객체에 의존성을 주입하는 것일 수도 있고, 무엇이든지 될 수 있다. 하지만 우리카 변수에 할당하는 작업을 setter로 위임하기로 한 이상, 그것은 엄연한 프로토콜이며 클래스의 생성자에서부터 지켜져야 한다. 즉 이제 변수의 할당작업은 전부 setter를 사용해야 한다. 이를 통해 데이터 일관성을 지킬 수 있다. 그 후 앞서 render함수를 그대로 메서드로 옮겨오면 된다. 또한 주의해야할 점은 Renderer도 실제로는 추상 클래스이며 현재 우리가 렌더링 정책으로 사용하는 console.log는 실제로는 전략 객체로 분리하여 외부에서 의존성을 주입하던지(Dependency Injection), 혹은 구상 훅으로 템플릿 메소드 패턴 을 사용하여 렌더링 정책의 개수만큼 구상 Renderer 클래스를 만들어 의존성 역전(Dependency Inversion Principle)을 이뤄야 하는 형태라는 것이다.. 우리 수업에서는 따로 렌더링 정책을 만들어 사용하지는 않을 것이므로 이부분은 이대로 놔두지만, 이 또한 역시 다른 변화율에 따라 분리하여 제어 역전을 이루어야 하는 부분이라느 것을 명심해야 한다.

사용코드는 다음과 같다. Renderer 클래스는 타입만을 받고, 실제 인스턴스는 AsyncItem 클래스에서 생성하게 된다.

const renderer = new Renderer(PrevPass);
renderer.render(...items)

위 코드들은 이미 바벨 플러그인을 사용하거나 직접 CPS를 사용하지 않으면 ES6로는 번역할 수 없는, ES2018 이후의 문법을 사용한 코드들이다. 자바스크립트에서 새로운 문법을 익히는 것은 개발생산성을 위해 상당히 중요한 일이다. 따라서 우리는 새로운 문법으로 지식을 업데이트 한 다음 꾸준히 매년 채택되는 표준들에 대해 학습하는 습관을 길러야 한다.

그럼 이제 기존의 Url 클래스와 같은 AIter의 구상 클래스들을 구현해보자. 우선 이전 Url 클래스를 들여다보자.

const Url = class extends AsyncItem {
  #url;
  #opt;
  constructor(u, opt) {
    super();
    this.#url = u;
    this.#opt = opt;
  }
	update(json){
		if(json)this.#opt.body = JSON.stringify(json);
	}
  async *load(v) {
    console.log(this.#opt.body);
    yield await (await fetch(this.#url, this.#opt)).json();
  }
};

load 제네레이터 안에서 yield를 하고 있지만 사실 이부분은 return을 해도 무방한 부분이다. yield와 return의 차이점은 yield는 suspend한 뒤 마저 남은 루프를 진행하고, return은 그대로 값을 반환한 뒤 종료한다. 따라서 하나의 값만을 yield할 때는 yield와 return의 차이가 없다. 보다 명확한 의미를 가지게 return으로 바꿔주자. 또한, 앞서 update와 load를 하나의 메소드로 바꾼 부분을 실제 구현하지 않았으니, 이 부분도 구현해보자.

const Url = class extends AsyncItem {
  #url;
  #opt;
  #dataF;
  constructor(u, opt, dataF = JSON.stringify) {
    super();
    this.#url = u;
    this.#opt = opt;
    this.#dataF = dataF;
  }
  async *load(v) {
    if (v) this.#opt.body = this.#dataF(v); //
    return await (await fetch(this.#url, this.#opt)).json();
  }
};

실무에서는 body의 형식이 달라질 경우가 생기므로 이를 런타임에 포맷팅해주는 함수를 받아서 처리할 것이다.

이렇게 인스턴스에서 람다를 인자로 받게 되면 곧 런타임에 필요한 분기문의 수만큼 람다를 생성해서 외부에서 주입하므로 일종의 의존성 주입이 이루어진다. 코드로 된 람드를 객체로 합성 Composition 하기만 하면 바로 전략 객체 패턴이 된다. 앞서 본 템플릿 메서드 패턴과 전략객체 패턴이 다른 점은, 템플릿 메서드 패턴은 정책의 경우의 수만큼 구상 클래스를 만들어야 하고, 전략객체 패턴은 인스턴스에서 인자로 받아 인스턴스별로 차이를 두게 된다. 이 부분은 API에서 데이터를 하나 로딩할 때마다 달라지는 변화율이 너무나 심한 부분이기 때문에 그만큼의 클래스를 생성한다는 건 무리가 있으므로, 이렇게 전략 객체 패턴을 사용하는 것이 유리하다. 즉, 템플릿 메서드 패턴은 분기문의 경우의 수만큼의 클래스를 생성하고 다시 추상클래스로 의존성 역전을 이루고, 전략 객체 패턴은 런타임에 이를 외부에서 주입받는 의존성 주입 을 통해 해결하기 때문에, 변화율이 더 심한 경우에는 전략 객체 패턴이 유리하다고 할 수 있다. 대신, 안정성의 측면에서는 인스턴스보다는 클래스가 더 안정적이므로 템플릿 메서드 패턴이 더 안정적이라고 할 수 있다.

그러면 지난시간에 Urls 클래스를 들여다보자. 우선 지난시간에 만든 Urls 클래스는 상당히 모순적인 일을 하고 있다.

const Urls = class extends AIter {
  #urls;
  #body;
  constructor(urls,body) {
    super();
    this.#urls = urls;
  }
  update(json) {
    this.#body = json;
  }
  async *load() {
    const r = [];
    for (const url of this.#urls) {
      url.update(this.#body);
      r.push((await url.load().next()).value);
    }
    yield r;
  }
};

무엇이 모순됐느냐 하면, Urls 객체는 한 번에 하나의 json만을 업데이트 하고 이를 urls 객체에게 모두 똑같이 적용하고 있는데도 불구하고 urls 객체를 순서대로 로딩하고 있다는 점이다. 매번 body를 업데이트 하지 않을 것이라면 순서대로 로딩할 이유가 없는데도 불구하고 말이다. 따라서 Urls 객체는 병렬로 url객체를 실행시켜야 한다. 따라서 Parellel 이라는 객체를 새로 만들어보자.

const Parallel = class extends AsyncItem {
  #items;
  constructor(...items) {
    super();
    this.#items = items;
  }
  async *load(data) {
    const arr = [...this.#items].map(item=>item.load(data).next());
    return (await Promise.all(arr)).map((v) => v.value); 
  }
};

앞서 load와 update는 하나의 트랜잭션으로 만들어야 한다고 했으니 load메서드로 합쳐준다. 트랜잭션이 여러개의 메서드로 나누어져있는 것은 버그의 가능성을 내포한다. 나누어진 메서드가 언제 어디에서 호출되어 트랜잭션 과정 도중에 개입할 지 보장할 수가 없기 때문이다.트랜잭션이라면 트랜잭션에 해당하는 시그니쳐를 갖추도록 하나의 메서드로 만들면 된다. 시그니쳐 자체만으로도 이미 오류가 날 가능성이 있다는 사실을 인지해야 한다.

생성자에서 인자로 AsyncItems를 받은 뒤, load 메서드에서 이를 map으로 Promise의 배열로 바꾸어 주었다. 정확하게는 {value,done}을 감싼 Promise의 배열이다. map은 이미 사본을 만들기 때문에 this.#items 원본은 변하지 않지만 메서드 내부에서 변형될 가능성을 사전에 방지하기 위해 해체한 사본을 만들었다. 우리의 AsyncItem들은 load를 호출하면 전부 async 제네레이터들을 생성하므로 이를 next()로 호출하면 전부 Promise로 바꿔줄 수 있다. 이 Promise 배열에 대해 Promise.all로 병렬 작업을 수행한 결과를 await로 바로 가져온 뒤 객체 안에 있는 value 값을 꺼내준 배열을 리턴하면 끝이다. map 고차함수는 하나의 배열을 다른 배열로 바꿔주는 projection의 역할을 한다.

그런데 AsyncItem을 Promise의 배열로 바꿔주는 이 작업, 이 작업은 Parellel이 가져야할 지식이 아닌 것 같다. 이는 AsyncItem 추상 클래스 수준에서 가질 수 있는 좀 더 추상화된 지식이므로 이를 AsyncItem의 책임으로 바꿔주자.

class AsyncItem{
	static toPromises(items, data) {
    return [...items].map((item) => item.load(data).next());
  }
}
class Parallel{
	async *load(data) {
		const arr = AsyncItem.toPromises(this.#items, data);
    return (await Promise.all(arr)).map((v) => v.value); 
  }
}

AsyncItem을 Promise로 바꿔주는 모든 코드는 전부 AsyncItem의 지식밖에 사용하고 있지 않다. 따라서 이를 Parellel 수준에서 내장하면, AsyncItem에 변화가 일어났을 때 대응할 수가 없으므로 AsyncItem가 이를 가지고 있는 것이 더 책임소재가 명확하게 구분된 코드이다. 이제 동시처리는 해결했으니 Promise.race의 우선순위 처리도 쉽게 구현할 수 있다.

const Race = class extends AsyncItem {
  #items;
  constructor(...items) {
    super();
    this.#items = items;
  }
  async *load(data) {
    return (await Promise.race(AsyncItem.toPromises(this.#items, data))).value; 
  }
};

load의 코드가 한줄로 되어있어서 가독성이 떨어진다는 의견이 있을 수 있다. 하지만 우리는 결국 Promise 배열을 Promise.race로 결과를 얻은 뒤 value값을 리턴하는 하나의 트랜잭션을 수행하고 있고, 하나의 트랜잭션을 표현한다는 측면에서는 한줄로 된 코드가 의도를 더 명확하게 하고 있는 것일 수 있다. (강의하신 맹기완 대표님의 의견이다. 물론 스스로 팀 협업보다는 외주업체를 운영하시면서 협업이 아닌 타인의 코드로 많은 작업을 하시는 본인의 환경요인이 있다는 코멘트도 남기셨다)

번외- Timeout

그럼 setTimeout의 동작을 구현하는 구상클래스도 만들어보자.

const Timeout = class extends AsyncItem{
  static get = (time, msg='timeout')=>new Timeout(time,msg);
  #timeout;
  constructor(time,msg) {
    super();
    this.#timeout = r=>setTimeout(_=>r(msg), time);
  }
  async *load(v){
    yield await new Promise(this.#timeout);
  }
}

우리가 함수 객체를 만드는 것을 피하려고 하지만 피할 수 없게 만드는 원인 중 하나가 바로 클로져 이다. 그 때 그 때의 컨텍스트를 물고 있는 클로져를 사용하기 위해서 우리는 함수객체의 오버헤드를 감수하면서도 함수객체를 만들게 되는 것이다. 이 때 함수 객체를 캐싱함으로서 새로운 함수를 생성하는 것에 대한 오버헤드를 피하는 것이 좋다. 생성자에서 timeout 필드를 람다로 초기화 함으로써 load 제네레이터에서 Promise로 함수를 감쌀 때도 미리 만들어둔 timeout 람다를 재활용할 수 있다. 물론, 매번 resolve 객체에 바인딩되어야 하는 setTimeout 내부의 람다는 어쩔 수 없이 매번 새롭게 생성되는 것을 피할 수 없다. 또한, time과 msg를 객체 생성자에서 람다에 스코프 변수로 잡아두었으므로 이 둘을 따로 필드로 초기화하지 않아도 계속해서 재활용할 수 있다는 장점도 있다.

우리는 함수가 본질적으로 하는 일이 무엇인지 알아야 한다. 함수는 기동(invocation)절차와 실행(execution)절차가 나뉜다. 보통의 함수는 이를 분리할 수 없지만 이를 분리한 가장 대표적인 예시가 바로 Promise이다. Promise는 생성 시점에 이미 내부에 실행 함수의 행위를 모두 담은 채로 기동(invocation) 되지만 실제 실행되는 execution시점은 await, 혹은 then 시점에 결정된다. 객체지향적으로 이는 커맨드 패턴에 해당한다. 실행시점을 분리할 수 있다는 것은, 단순히 실행을 지연시킨다는 의미에서 그치는 것이 아니라 이미 invocation 한 함수를 얼마든지 원하는대로 반복, 재실행, 취소하는 등의 행위가 가능하다는 것이다. 자바에서는 Executor에게 Runnable 객체를 넘겨주는 시점에 invocation되고, 이를 실제로 쓰레드를 할당 받아 Executor가 실행하는 시점에서 Runnable의 실행이 일어나는 경우가 대표적이다.

fetch함수는 기본적으로 timeout 기능이 존재하지 않는다. Response 응답 시간 자체가 긴 경우가 아니라, TCP 라우팅 도중 Connectinon이 느려지는 타이밍이 발생하는 경우가 발생하기도 하는데, 이런 경우 Connection을 기약없이 대기하는 것보다 요청을 끊고 다시 요청할 경우 Connection이 바로 연결될 수 있다. 그런데 이렇게 Connection에 timeout을 걸어 connection 요청을 끊고 재요청할 수 있는 기능이 자바스크립트의 표준 fetch API에는 없는 것이다. 그러면 우리가 만든 Timeout 클래스를 활용하여 기존의 다른 클래스의 fetch요청을 수정해보자,

const Url = class extends AsyncItem{
  static get= (u, timeout=0, opt={method:'GET'})=> timeout>0?
      new Race(new Url(u,opt), Timeout.get(timeout)):new Url(u, opt)
  static post = (u,timeout,opt={})=>{opt.method="POST"; return Url.get(u,timeout,opt);}
	static urls = (...urls)=>Parallel.get(Url.get,...urls)
}
const Parallel = class extends AsyncItem {
  static get = (cb,...urls)=>new Parallel(urls.map(cb))
}

static Timeout 클래스의 get 람다를 이용하여, timeout 값이 0보다 큰 경우에는 url요청과 timeout 요청중 느린 쪽을 좌절시키도록 만들었다. post요청인 opt만 바꾼 후 기존 get 람다를 사용하면 된다. 병렬 처리를 위한 urls 함수도 Parallel 구상 클래스를 이용하여 만들 수 있다. 이렇게 fetch에 여러가지 기능을 추가한 여러 구상 비동기 클래스들을 만들어 보았다. 사용코드는 다음과 같다.

const renderer = new Renderer(PrevPass);
renderer.render(Url.urls('1.json','2.json', Timeout.get(100), Url.get("3.json",1000)))

우리는 이제 매우 복잡한 시나리오의 비동기 작업을 처리할 수 있다.

profile
inudevlog.com으로 이전해용

0개의 댓글