본문 바로가기
Javascript/Node.js

Iterator Pattern - 행위 디자인 패턴

by v8rus 2022. 2. 14.

반복자 (Iterator)

 

매우 중요하고 일반적으로 사용됨
언어는 어떤 방식으로든 이 패턴을 구현함

배열, 트리 구조와 같은 컨테이너 요소들을 반복하기 위한 공통 인터페이스 또는 포로토콜을 정의
배열은 단순한 루프 필요
트리는 순회 알고리듬 필요
반복자 패턴 사용 시 알고리듬 또는 데이터 구조에 대한 세부정보는 숨기고 모든 유형의 컨테이너를 반복하는데 필요한 공통의 인터페이스를 제공
 => 순회 연산의 결과를 처리하는 방식과 순회 안고리듬의 구현을 분리할 수 있음

JS 에서 반복자는 이벤트 이미터와 스트림처럼 반드시 컨테이너일 필요가 없음
다른 유형의 구조에서도 잘 동작

 

 

1. 반복자 iterator 프로토콜  = 반환되는 전체가 반복을 위한것

상속 X, 형식적 구조 X, 프로토콜을 통해 구현됨 => 사전에 합의된 형태의 인터페이스 객체 이용

시작점 : 시퀀스를 생성하기 위한 인터페이스를 정의하는 '반복자 프로토콜'
next() 함수를 구현한 객체를 반복자 라고 함
함수가 호출될 때마다 함수는 반복의 다음 요소를 객체에 담아 반환, '반복자 결과'

-반복 완료 시 done 변수 = true, 그렇지 않으면 done = undefined ||| false;
-done = true 일때라도 value 가 설정 될 수 있는데, 반환한 요소값이 아닌 반복 전체 동작에 대한 정보와 관련된 값임

* 반복자가 추가적인 속성을 반환하는것을 막을 수는 없지만 API에 의해 무시될뿐임

 

간단한 반복자 프로토콜 예를 봅시다

// createAlphabetIterator.js
const A_CHAR_CODE = 65;
cosnt Z_CHAR_CODE = 90;

function createAlphabetIterator() {
  let currCode = A_CHAR_CODE;

  return {
    next() {
      const currChar = String.fromCodePoint(currCode);
      if (currCode > Z_CHAR_CODE) {
        return {done : true };
      }

      currCode++;
      return {value : currChar, done : false; };
    }
  }
}

next() 함수 호출마다 문자에 해당하는 문자코드를 나타내는 숫자 증가, 문자 변환 후 프로토콜에 정의된 객체 형식을 사용해 반환함

* 반복자에서 {done : true} 반환이 반복을 끝내는데 항상 필요한것은 아님
  난수 필요 할 경우나 pi 상수 숫자, 피보나치 수열 등과 수학적 급수 반복자에는 필요없음


현재 위치를 어떤 방식으로든 추적해야 함 => 대부분의 경우 반복자는 상태 저장 객체

  • 상태를 클로저 (currCode) 로 보관했지만 다른 방법도 있음
  • 인스턴스 변수에 상태를 보관 가능, 일반적으로 반복자 자체에서 반복상태를 언제든지 앍을 수있기 때문에 디버깅관점에서 좋지만 외부 코드가 인스턴스 변수를 수정하여 반복상태를 변경하는것을 막지는 못함

각각의 장단점을 보고 결정은 알아서!

반복자는 실제로 완전히 상태 비저장일수도 있음
무작위 요소를 반환하고 무작위로 종료하거나 무한 반복을 하는 반복자거나 첫번째 반복에서 중지하는 반복자를 예로 들 수 있음

 

const itrerator = createAlphabetIterator();

let iterationResult = iterator.next();
while (!iterationResult.done) {
  console.log(iterationResult.value);
  iterationResult = iterator.next();
};

반복자를 사용하는 코드는 패턴 자체로 간주될 수 있음, 다만 좀 불편할 뿐, JS 반복자를 사용하는 훨씬 더 편리하고 멋진 방법이 있음...

 

 

2. 반복가능자 iterable 프로토콜 = 클래스의 일부분

객체가 반복자를 반환하는 표준화된 방법 정의 = 반복가능자 iterable
일반적으로 반복가능자는 요소들의 컨테이너로 데이터 구조 같은것,
디렉터리의 파일 반복할 수있는 Directory 객체와 같은 요소들의 집합을 가상적으로 나타내는 객체일수도 있음

JS @@iterator 함수 = Symbol.iterator 함수를 통해 접근 가능한 함수를 구현하여 반복가능자를 정의할 수 있음
@@iterator 함수는 반복자 객체를 반환해야 함. 다음과 같은 형식임

class MyIterable {
  // 다른 함수들...
  [Symbol.iterator] () {
    // 반복자 (iterator)를 반환
  }
}

 

어떻게 작동하는지 보기 위해 2차원 행렬 예제

 

// matrix.js
export class Matrix {
  constructor (inMatrix) {
    this.data = inMatrix;
  }

  get (row, column) {
    if (row >= this.data.length || column >= this.data[row].length) {
      throw new RangeError('Out of bounds');
    }

    return this.data[row][column];
  }

  set (row, column, value) {
    if (row >= this.data.lenght || column >= this.data[row].length) {
      throw new RangeError('Out of bounds');
    }
    this.data[row][column] = value;
  }

  [Symbol.iterator] () {      // @@iterable 함수 = 지정된 대로 반복자를 반환, 반복자 프로토콜 준수
    let nextRow = 0;    // 클로저의 일부
    let nextCol = 0;

    return {
      next : () => {
        if (nextRow === this.data.length) {
          return {done: true};
        }

        const currVal = this.data[nextRow][nextCol];

        if (nextCol === this.data[nextRow].length -1) {
          nextRow++;
          nextCol = 0;
        } else {
          nextCol++;
        }

        return { value : currVal };
      }
    }
  }
}

// index.js
import {Matrix} from './matrix.js';

const matrix2x2 = new Matrix([
  ['11', '12'],
  ['21','22'],
]);

const iterator = matrix2x2[Symbol.iterator]();
let iterationResult = iterator.next();

while (!iterationResult.done) {
  console.log(iterationResult.value);
  iterationResult = iterator.next();
}

Matrix 인스턴스 생성 @@iterator 함수를 통해 반복자를 얻는것이 전부임

 

 

3. 네이티브 jS 인터페이스로서의 iterator 과 iterable

두가지의 프로토콜을 가지는데 그렇게 한 이유는?
제 3자의 코드를 모델링 할 수 있음
반복가능자를 입력받는 문장 뿐 아니라 API 도 가질 수 있게 됨

반복자를 허용하는 분명한 구문은 for... of 루프
실제 항상 next() 를 호출하여 다음 요소를 검색하고 반복 결과의 done 속성이 반복의 끝을 나타내는 true 로 설정되어있는지 확인
for ... of 명령에 반복가능자를 전달하면 반복자가 반환하는 요소를 원활하게 반복하는 것을 볼 수 있음???...
 => 직관적이고 간결한 구문으로 반복 처리 가능

 

다시 위의 Matrix 예제를 가져와 봅시다

for (const element of matirx2x2) {
  console.log(element);
}

이렇게 쓸 수 있다는거고

 

반복가능자와 호환되는 또 다른 구조는 전개 구문 (spread operator)

const flattendMatrix = [...matrix2x2];
console.log(falttendMatrix);    // 그냥 통째로 뜨는데???

비슷하게 구조 분해 할당 (desturcturing assignment) 와 함께 반복 가능자 사용 가능

const [oneOne ,oneTwo, twoOne, twoTow] = matrix2x2;
console.log(oneOne, oneTow, twoOne, twoTwo);

 

  * 반복가능자를 허용하는 몇가지 JS 내장 API
  Map
  WeakMap
  set
  WeakSet
  Promise.all
  Promise.race
  Array.from

 

* 지금까지 살펴본 모든 API 와 구문 구조는 반복자가 아닌 반복가능자를 받아들임, 다만 createAlphabeIterator()처럼 반복자를 반환하는 함수가 존재한다면?
해결책은 반복자 객체 자체에 @@Iterator 함수를 구현하는 것! 단순히 반복객체 자체를 반환
코드는 다음과 같이 쓰면 됨

for (const letter of createAlphabetIterator()) {
  // ...
}

가장 볼만한 반복가능자는 Array, Map, Set 과 같은 데이터 구조, string 도 @@iterable 함수 구현함, Buffer 또한 마찬가지

 

* 배열에 중복 요소 포함 확인법 : uniqArray = Array.from(new Set(arrayWithDuplicates));
반복 가능자가 어떻게 공용 인터페이스를 사용하여 서로 다른 구성요소 같이 통신할 수있는 방법을 제공하는지 보여줌

 

 

4. 제너레이터 = 세미코루틴 (semicoroutines)

다른 진입점이 있을 수 있는 표준 함수를 일반화 함
표준함수에서함수 자체의 호출에 해당하는 진입점 하나만 가질 수 있음
제너레이터는 yield 문을 사용하여 일시 중단된 다음 나중에 해당 지점에서 다시 시작 가능
반복자 구현에 매우 적합함
반환하는 제너레이터 객체는 실제로 반복자이면서 반복가능자

 

4-1. 이론상의 제너레이터

function * myGenerator() {
  // 제너레이터의 바디 부분
}

제너레이터 함수를 호출해도 바로 본문이 실행되지 않음.

다만,제너레이터 객체를 반환할거임
제너레이터 객체에서 next() 호출하면 yield 명령어가 호출되거나 제너레이터에서 반환이 발생할 때 실행을 시작하거나 재개 함
yield 키워드 다음 x 를 반환하는 것은 반복자에서 { done : false, value : x } 를 반환하는것고 같음
제너레이터가 종료되면 x 를 반환하는것은 반복자에서 { done: true, value : x } 를 반환하는것과 같음

 

4-2. 간단한 제너레이터 함수

fruitGeneroator() 간단한 예제

function * fruitGenerator() {
  yield 'peach';
  yield 'watermelon';
  
  return 'summer';
}

const fruitGeneratrObj = fruitGenerator();
console.log(fruitGeneratorObj.next());
console.log(fruitGeneratorObj.next());
console.log(fruitGeneratorObj.next());


// 결과
// 첫번째 yield 수행/반환 후 중지
{ value : peach, done :false }
{ value : watermelon, done : false }
// 마지막 명령인 return 문에서 재게됨, 제너레이터 종료하고 done :true 설정된 객체를 반환
{ value : summer, done : true }


제너레이터도 반복 가능하기에 for ... of 루프를 사용 가능

for (const fruit of fruitGenerator()) {
  console.log(fruit);
};

// 결과
// peach
// watermelon

// summer 출력 불가인이유는 제너레이터에 의해 생성되는 값이 아니라 반복이 종료되어 반환하는 값이라 그럼

 

4-3. 제너레이터 반복 제어

제너레이터 개게는 일반 반복자보다 유용함
next() 함수는 선택적으로 인자를 허용함, 반드시 x
인수는 yield 명령의 반환값으로 전달
예제를 봅시다

function* twoWayGenerator () {
  const what = yield null;    // 처음 호출시 여기서 일시 중지
  yield 'Hello' + what;   // 두번재 호출시 전달값이 what 변수에 설정되어 출력됨
}

const twoWay = twoWayGenerator();
twoWay.next();
console.log(twoWay.next('world'));

 

제너레이터 객체에서 제공하는 두가지 추가적인 함수
throw(), return()

  • throw()
    next() 함수처럼 동작하지만 제너레이터 내의 마지막 yeild 지점에서 throw 된 것처럼 예외를 발생시킴
    done 및 value 속성이 잇는 표준 반복자 결과 객체를반환함
  • return()
    제너레이터를 강제적으로 종료, 다음과 같은 객체 반환
    {done: true, value : return Argument}
    return Argument : return() 함수에 전달된 인자

예제를 봅시다.

// demo.js
function * twoWayGenerator() {
  try {
    cosnt what = yield null;
    yield 'Hello ' + what;
  } catch (Err) {
    yield 'Hello Error!'  + Err.message;
  }
}

console.log('Using throw () :');
const twoWayException = twoWayGenerator();
twoWayException.next();   // 첫번쨰 yield 명령어가 반환되는 즉시 예외가 발생함
console.log(twoWayExcepiton.throw(new Error('Boom!')));

console.log('USing return():');
cosnt twoWayReturn = twoWayGenerator();
console.log(twoWayReturn.return('myReturnValue'));


// 결과
// Using throw() :
{value : 'Hello Errror : Boom!', done: false}
// Using return() : 
{value : 'myReturnVAlue', done : true}

 

 

5. 반복자 대신 제너레이터를 사용하는 방법

제너레이터 객체도 반복자임
@@iterator 함수를 구현 할 수 있음
Matrix 예제 다시 불러봅시다

// matrix.js
export class Matrix {
  // ... 다른 나머지 함수들 (변경 없음)
  * [Symbol.iterator] () {    // 함수 이름 앞에 * 존재 => 제너레이터
    let nextRow = 0;    // 제너레이터의 로컬 변수, 재진입 시 로컬 상태 유지
    let nextCol = 0;

    // 매트릭스 요소 반복을 위한 표준 루프(while) 사용, next() 호출보다 더 직관적
    while (nextRow != this.data.length) {   
      yield this.data[nextRow][nextCol];

      if (nextCol === this.data[nextRow].length -1) {
        nextRow++;
        nextCol = 0;
      } else {
        nextCol++;
      }
    }
  }
}

 

! 제너레이터 위임 지시자 yield * iterable 은 iterable  을 인수로 받아들이는 JS 내장 굼누의 또 다른예
명령어는 iterable 요소를 반복하고 각 요소를 하나씩 생성함

 

 

6. 비동기 반복자

여태껏 next() 함수에서 동기적으로 값을 반환함
JS 에서 비동기 연산이 필요한 항목에 반복작업이 매우 일반적

ex)HTTP 서버에서 sql req 쿼리 결과 또는 페이지가 특정 restAPI 요소에 대한 반복 작업을 한다 하면
모든 상황에서 next() 함수는 Promise 를 반환하는것이 편리하거나 async/await 구문을 사용해 동기화 하는것이 더 좋음

비동기 반복자들은 Promise 를 반환함
비동기 함수를 사용하여 반복자의 next() 함수를 정의 함
비동기 반복가능자 async iterables, @@asyncIterator 함수, Symbolic.asyncIterator 키를 통해 접근 할 수 있는 함수를 구현한 객체로 비동기 반복자를 동기적으로 반환

비동기 반복가능자는 for await ... of 구문을 사용해 반복 가능
비동기 함수에서만 사용 가능
본질적으로 반복자 패턴 위에 순차적인 비동기 실행을 구현함
아래의 구문과 같은 문법적으로 편리한 표현일 뿐임

const asyncIterator = iterable[Symbol.asyncIterator]();
let iterationResult = await asyncIterator.next();
while (!iterationResuilt.done) {
  console.log(iterationResult.value);
  iterationResult = await asyncIterator.next();
}

 
for await ... of 구문은 Promise 들의 배열과 같이 간단한 반복가능자를 반복하는데 사용
반복자의 모든 요소가 Promise 가 아니라도 동작함

ex) URL 목록 입력을 입력받아 웹의 사용가능 상태(up/down)를 반복하여 체크하는 클래스를 만들어봄

 

// CheckUrls.js
import superagent from 'superagent';

export class CheckUrls {
  constructor (urls) {    // url 목록을 받음
    this.urls = urls;
  }

  [Symbol.asyncIterator] )() {
     // 반복가능자이어야 함, @@iterable 함수를 호출하기만 함녀
    cosnt urlsIterator = this.urls[Symboliterator]();   

    return {
      // async 가 있음, 비동기 반복가능자 프로토콜에서 요청된 대로 항상 프라미스를 반환함
      async next() {    
        const ieratorResult = urslIterator.next();
        if (iteratorResult.done) {
          return {done : true}
        }

        const url = iteratorResult.value;
        try {
          const checkREstul = await superagent
            .head(url)
            .redirects(2);
          
          return {
            done: false,
            value : `${url} is up, status : ${checkREsult.status}`
          };
        } catch (err) {
          return {
            done : false,
            value: `${url} is down, error : ${err.message}`
          }
        }
      }
    }
  }
}

// main.js
import {CheckUrls} from './checkUrls.js';

async function main () {
  const checkUrls = new CheckUrls([
    'https://naver.com',
    'https://sample.com',
    'https://google.com',

  ])

  for await (const status of checkURls) {
    console.log(status);
  }
}

main();

for await ... of 구문은 비동기 반복 가능자를 반복할 수 있는 매우 직관적인 문법 제공
내장 반복자와 함께 비동기 정보에 접근하는 새로운 대안을 만들 수 있지

* for await ...of 루프에서 break, return, exception 으로 조기 중단 가능 하며 선택적으로 반복자의 return() 메서드를 호출하여 중단
  정리작업??? 을 즉각적으로 실행하는데 사용 가능함

 

 

7.  비동기 제너레이터

비동기 반복자 iterator 뿐 아니라 비동기 제너레이터 사용 가능
비동기 제너레이터 함수를 정의하려면 함수 정의 앞 async 키워드 추가하면 됨

async function * generatorFunction() {
  // ... 제너레이터 body
}

비동기 제너레이터는 본문 내에서 await 명령을 사용할 수 있음
next() 함수 반환값은 규약에 정의된 done 및 value 속성을 가진 객체의 이행값으로 반환하는 프라미스
유효한 비동기 반복자이기도 함, for await ...of 루프 사용 가능

비동기 반복자 구현 단순화 하는 예

// CheckUrls.js
export class CheckUrls {
  constructor (urls) {
    this.urls = urls
  }

  async * [Symbol.asyncIterator] () {
    for (const url of urls) {
      try {
        const checkResult = await superagent
          .head(url)
          .redirects(2)
        yield `${url} is up, status : ${checkResult.status}`
      } catch (err) {
        yield `${url} is down, error : ${err.message}`
      }
    }
  }
}

순수 비동기 반복자 대신 비동기 제너레이터 사용 시 코드가 단순해짐, 로직 가독성 높이 명확해짐

 

 

8. 비동기반복자 및 Node.js 스트림

비동기 반복자와 Node.js 읽기 스트림 사이의 관계, 목적과 동작이 비슷함!
stream.Readable 이 @@asyncIterator 함수를 구현하여 비동기 반복자로 만든것은 이유가 있음, 직관적 메커니즘 제공

stdin 스트림에서 개행문자 발견시 새로운 청크를 발출하는 split() 이라는 변환 스트림에 파이프로 연결, 각 줄을 반복하는 예를 보자

import split from 'split2';

async function main() {
  cosnt stream = process.stdin.pipe(split());
  for await (const line of stream) {
    console.log(`you wrote : ${line}`);
  }
}

main();

 

매우 직관적이고 간결함

반복자와 스트림 패러다임이 유사해 쉽게 상호 운용 가능

stream.Readable.from(iterable, [options]) 함수가 iterable 을 인자로 취하는것을 기억!

인자는 동기 혹은 비동기

전달된 반복 가능자를 감싸 읽기 스트림을 반환하는데 반복자의 인터페이스를 읽기 스트림이 인터페이스로 변환함

 

스트림과 비동기 반복자 밀접하게 연관 시 어떤 인터페이스 사용 시 고려사항

  • 스트림은 push 됨, 데이터가 내부 버퍼로 주입된 다음 버퍼에서 소비됨
    비동기 반복자는 기본적으로 뎅터를 제공함 => 데이터는 사용자 요청시에만 조회/생성 됨
  • 스트림은 내부 버퍼링 및 백프레셔를제공, 이진 데이터 처리에 적합
  • 스트림은 간단한 API 인 pipe() 를 사용해 연결 할 수있지만 비동기 반복자는 표준화된 연결 방법 제공 안함

 

 

9. Summary

비동기 반복자는 Node.js 생태예에서 빠른 인기를 얻고 있음
스트림 대신 선호되는 대안
@database/pg, /mysql, /sqlite 패키지는 DB 연결 라이브러리임
모두 쿼리 결과를 쉽게 반복하는데 비동기 반복자를 반환하는 queryStream() 이라는 함수 제공

for await (const record of db.queryStream(sql`SEELCT * FROM my_table`)) {
  // 레코드를 가지고 필요한 작업을 수행
}

내부적으로 반복자는 쿼리 결과에 대한 커서를 자동으로 처리함, for await .... of 구문을 사용하여 수행하면 됨
- zeromq 패키지도 가능 (API 에서 반복자에 크게 의존)

댓글