> For the complete documentation index, see [llms.txt](https://ssosso.gitbook.io/study/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ssosso.gitbook.io/study/javascript/node.js-1/6.-1-2.md).

# 6. 디자인패턴 1/2

## 6. 디자인패턴 (1/2)  6 \~ 10

* 전략(Strategy)
* 상태(state)
* 템플릿(Template)
* 미들웨어(Middleware)
* 커맨드(Command)

### 6.6 전략(Strategy)

전체적인 틀 (뼈대)는 유지하고, 알고리즘 변경 및 형태 변경으로 프로세스 처리가 가능한 패턴. `유연성을 확보`한다.

* 간단 전략패턴 적용 예제

  ```javascript
  // config.js
  const fs = require('fs');
  const objectPath = require('object-path');

  class Config {
    constructor(strategy) { // 전략 객체 : strategy
      this.data = {};
      this.strategy = strategy;
    }

    get(path) {
      return objectPath.get(this.data, path);
    }

    set(path, value) {
      return objectPath.set(this.data, path, value);
    }

    read(file) {
      console.log(`Deserializing from ${file}`);
      this.data = this.strategy.deserialize(fs.readFileSync(file, 'utf-8'));
    }

    save(file) {
      console.log(`Serializing to ${file}`);
      fs.writeFileSync(file, this.strategy.serialize(this.data));
    }
  }

  module.exports = Config;

  // strategies.js
  // json 전략
  module.exports.json = {
    deserialize: data => JSON.parse(data),
    serialize: data => JSON.stringify(data, null, ' ')
  };

  // ini 전략 (key=value 형식)
  const ini = require('ini');
  module.exports.ini = {
    deserialize: data => ini.parse(data),
    serialize: data => ini.stringify(data)
  };

  // configTest
  const Config = require('./config');
  const strategies = require('./strategies');

  const jsonConfig = new Config(strategies.json);
  jsonConfig.read('samples/conf.json');
  jsonConfig.set('book.nodejs', 'design patterns');
  jsonConfig.save('samples/conf_mod.json');

  const iniConfig = new Config(strategies.ini);
  iniConfig.read('samples/conf.ini');
  iniConfig.set('book.nodejs', 'design patterns');
  iniConfig.save('samples/conf_mod.ini');
  ```

### **6.7 상태(state)**

상태를 하나의 객체화. 동적 변형 및 처리.

호텔예약시스템 -> 3가지의 상태 (confirm-확인, cancel-취소, delete-삭제) 모델링\
상태전이, 그다음 상태에 대한 관심사분리

* 적용 예제

  소켓 활성/비활성화를 통한 메시지 큐 처리  &#x20;

```javascript
// failsafeSocket.js
const OfflineState = require('./offlineState');
const OnlineState = require('./onlineState');

class FailsafeSocket {
  constructor(options) {
    this.options = options;
    this.queue = [];
    this.currentState = null;
    this.socket = null;
    this.states = {
      offline: new OfflineState(this),
      online: new OnlineState(this)
    };
    this.changeState('offline');
  }

  changeState(state) {
    console.log('Activating state : ' + state);
    this.currentState = this.states[state];
    this.currentState.activate();
  }

  send(data) {
    this.currentState.send(data);
  }
}

module.exports = options => {
  return new FailsafeSocket(options);
}

// online.js
module.exports = class OnlineState {
  constructor(failsafeSocket) {
    this.failsafeSocket = failsafeSocket;
  }

  send(data) {
    this.failsafeSocket.socket.write(data);
  }

  activate() {
    this.failsafeSocket.queue.forEach(data => {
      this.failsafeSocket.socket.write(data);
    });
    this.failsafeSocket.queue = [];
    this.failsafeSocket.socket.once('error', () => {
      this.failsafeSocket.changeState('offline')
    })
  }
};

// offlineState.js
const jot = require('json-over-tcp');

module.exports = class OfflineState {
  constructor(failsafeSocket) {
    this.failsafeSocket = failsafeSocket;
  }

  send(data) {
    this.failsafeSocket.queue.push(data);
  }

  activate() {
    const retry = () => {
      setTimeout(() => this.activate(), 500);
    };

    this.failsafeSocket.socket = jot.connect(
        this.failsafeSocket.options,
        () => {
          this.failsafeSocket.socket.removeListener('error', retry);
          this.failsafeSocket.changeState('online');
        }
    )

    this.failsafeSocket.socket.once('error', retry);
  }
};

// server.js
const jot = require('json-over-tcp');
const server = jot.createServer({port:5000});
server.on('connection', socket => {
  socket.on('data', data => {
    console.log('Client data : ', data)
  });
});

server.listen(5000, () => console.log('Stated'));

// client.js
const createFailsafeSocket = require('./failsafeSocket');
const failsafeSocket = createFailsafeSocket({port: 5000});

setInterval(() => {
  failsafeSocket.send(process.memoryUsage());
}, 1000);
```

### 6.8 템플릿(Template)

템플릿은 상위타입에서 틀이 되는 추상메서드를 제공하고, 하위에서 그 추상메서드를 구현함으로써 동적 실행시 하위 타입에 맞게 실행되게 하는 구조\
틀을 제공한다는 관점에서 `전략패턴`과 유사하나, **하나의 객체타입이 대체되는 부분과 메서드를 오버라이딩하여 실행하는 관점의 차이가 있다.**\
그래서 `템플릿`은 메서드 내의 알고리즘 순서에 의해 완성되고, `전략`은 객체타입의 변경에 따라 완전 내용이 전반적으로 모든게 바뀌는 느낌

* 예제

```javascript
// configTemplate.js
const fs = require('fs');
const objectPath = require('object-path');

class ConfigTemplate {

  read(file) {
    console.log(`Deserializing from ${file}`);
    this.data = this._deserialize(fs.readFileSync(file, 'utf-8'));
  }

  save(file) {
    console.log(`Serializing to ${file}`);
    fs.writeFileSync(file, this._serialize(this.data));
  }

  get(path) {
    return objectPath.get(this.data, path);
  }

  set(path, value) {
    return objectPath.set(this.data, path, value);
  }

  _serialize() {
    throw new Error('_serialize() must be implemented');
  }

  _deserialize() {
    throw new Error('_deserialize() must be implemented');
  }
}

module.exports = ConfigTemplate;

// jsonConfig.js -> JSON을 처리해주는 하위타입
const util = require('util');
const ConfigTemplate = require('./configTemplate');

class JsonConfig extends ConfigTemplate {
  _deserialize(data) {
    return JSON.parse(data);
  }

  _serialize(data) {
    return JSON.stringify(data, null, ' ');
  }
}

module.exports = JsonConfig;

// iniConfig.js -> ini으로 처리해주는 하위타입
const ConfigTemplate = require('./configTemplate');
const ini = require('ini');

class IniConfig extends ConfigTemplate {
  _serialize(data) {
    return ini.stringify(data);
  }

  _deserialize(data) {
    return ini.parse(data);
  }
}

module.exports = IniConfig;

// test.js
const JsonConfig = require('./jsonConfig');
const jsonConfig = new JsonConfig();
const IniConfig = require('./iniConfig');
const iniConfig = new IniConfig();

jsonConfig.read('samples/conf.json');
jsonConfig.set('nodejs', 'design patterns');
jsonConfig.save('samples/conf_mod.json');


iniConfig.read('samples/conf.ini');
iniConfig.set('nodejs', 'design patterns');
iniConfig.save('samples/conf_mod.ini');

```

### 6.9 미들웨어(Middleware)

저수준 추상화 -> OS, API, 네트워크통신, 메모리 관리 등\
미들웨어 예 -> CORBA, ENterprise Service Bus, Spring, JBoss, Apache Web Server 등\
미들웨어 : 일반적으로 `하위서비스`와 `어플리케이션`사이의 **인터페이스, 연계, 접착제 같은 모든 SW 계층으로 정의 가능**

#### 6.9.1 미들웨어로서의 Express

Express에서 미들웨어 -> 파이프라인에서 구성되고 들어오는 HTTP 요청 및 응답의 처리

```javascript
// req : 요청, res : 응답, next : 미들웨어 (트리거 할때 콜백)
function (req,res,next) { 
}
```

#### 6.9.2 패턴으로서의 미들웨어

Intercepting Filter 패턴과 Chain of Responsibility 패턴의 Node.js화\
스트림, 파이프라인 이라고 할 수도 있음\
`모든 종류의 데이터`에 대한 `전처리 및 후처리`를 수행\
함수형태로 처리 단위(processing unit), 필터(filter) 및 핸들러(handler) 집합이 비동기 시퀀스의 형태로 연결된 특정 패턴

express -> use함수 : 미들웨어 등록 -> 데이터처리는 `비동기 순차실행의 흐름`

#### 6.9.3 MQ 용 미들웨어 프레임워크 만들기

메시지큐, 메시징라이브러리를 중심으로 미들웨어 프레임워크를 구축함으로써 패턴을 설명.\
분산시스템 데이터 처리에 활용됨\
`데이터의 전처리 및 후처리 추상화`하는 미들웨어 인프라 구축

```javascript
// server.js
// const zmq = require('zeromq');
const zmq = require('zmq');
const ZmqMiddlewareManager = require('./zmqMiddlewareManager');
const jsonMiddleware = require('./jsonMiddleware');
const socket = zmq.socket('req');
// const socket = zmq.Push;
socket.bind('tcp://127.0.0.1:5000');

const zmqm = new ZmqMiddlewareManager(socket);

zmqm.use(jsonMiddleware.json());
zmqm.use({
  inbound: function (message, next) {
    console.log('Received: ', message.data);
    if (message.data.action === 'ping') {
      this.send({action: 'pong', echo: message.data.echo});
    }
    next();
  }
});

// zmqMiddlewareManager.js
module.exports = class ZmqMiddlewareManager {
  constructor(socket) {
    this.socket = socket;
    this.inboundMiddleware = [];  // 들어오는 메시지 처리  (수신메시지)
    this.outboundMiddleware = []; // 나가는 메시지 처리   (발신메시지)

    socket.on('message', message => {
      this.executeMiddleware(this.inboundMiddleware, {
        data: message
      });
    });
    // console.log(this.socket.send);;
  }

  // 새로운 메시지가 소켓을 통해 전송될 때, 미들웨어 역할
  send(data) {
    const message = {
      data
    };

    // 데이터 보내기, 아웃바운드 처리
    this.executeMiddleware(this.outboundMiddleware, message, () => {
      this.socket.send(message.data);
    });
  }

  // 미들웨어 형식에 따른 추가
  use(middleware) {
    if (middleware.inbound) {
      this.inboundMiddleware.push(middleware.inbound);        // 배열 맨 뒤에 추가
    }
    if (middleware.outbound) {
      this.outboundMiddleware.unshift(middleware.outbound);   // 배열 맨 앞에 추가
    }
  }

  // 미들웨어가 처리
  executeMiddleware(middleware, arg, finish) {
    function iterator(index) {
      if (index === middleware.length) {
        return finish && finish();
      }

      middleware[index].call(this, arg, err => {
        if (err) {
          return console.log('There was an error : ', err.message);
        }
        iterator.call(this, ++index);
      });
    }

    iterator.call(this, 0);
  }
};

// jsonMiddleware.js
module.exports.json = () => {
  return {
    inbound: function (message, next) {
      message.data = JSON.parse(message.data.toString()); // 수신 데이터 Buffer -> JSON
      next();
    },
    outbound: function (message, next) {
      message.data = new Buffer(JSON.stringify(message.data));  // 데이터 송신 : JSON -> Buffer
    }
  }
};


// client.js 
const zmq = require('zmq');
const ZmqMiddlewareManager = require('./zmqMiddlewareManager');
const jsonMiddleware = require('./jsonMiddleware');

const request = zmq.socket('req');
request.connect('tcp://127.0.0.1:5000');

const zmqm = new ZmqMiddlewareManager(request);
zmqm.use(jsonMiddleware());
zmqm.use({
  inboud: function (message, next) {
    console.log('Echoed back : ', message.data);
    next();
  }
});

setInterval(() => {
  zmqm.send({action: 'ping', echo: Date.now()})
});
```

#### 6.9.4 Koa에서 제너레이터를 사용한 미들웨어

미들웨어 패턴을 많이 사용하는 웹프레임워크 `Koa`\
`Koa` : 콜백 사용 대신 제너레이터 함수를 사용하는 미들웨어 패턴을 구현 인바운드(다운스트림), 아웃바운드(업스트림)

* 예제

  ```javascript
  // app.js
  const Koa = require('koa');
  const app = new Koa();
  app.use(function* () {
    this.body = {"now": new Date()} // 응답 -> 알아서 JSON 문자열로 변환, 알맞은 content-type 헤더 추가
  });

  app.use(require('./rateLimit'));

  app.listen(3000);
  ```

### 6.10 커맨드(Command)

나중에 수행할 동작에 필요한 모든 정보를 캡슐화 하는 객체.\
메서드난 함수를 직접 호출하는 대신 위임을 통해 그런 호출을 수행할 목적을 나타내는 객체를 생성.

* 커맨드(Command) : 메서드 or 함수를 호출하는데 `필요한 정보를 캡슐화 하는 객체`
* 클라이언트(Client) : `명령을 생성`하고 그것을 `호출자에게 제공`
* 호출자(Invoker) : 대상(Subjet)에서 `명령을 실행하는 역할`
* 타겟(Target or Receiver) : `호출의 대상`으로 단일 함수 or 한 객체의 메서드

#### 장점

* 커맨드를 `나중에 실행하도록 예약(지연실행)`
* 커맨드는 쉽게 직렬화 되어 `네트워크를 통해 전송`, `원격 컴터간에 작업을 배포`하고 , `브라우저 -> 서버로 명령을 전송`하고, `RPC 시스템을 만드는` 등의 작업 수행이 가능
* 커맨드를 사용하면 `시스템에서 실행되는 모든 작업의 내역을 쉽게 유지 가능`
* 커맨드는 `데이터 동기화 및 충돌 해결`을 위한 일부 알고리즘에서 중요한 부분을 차지
* `실행이 예정된 커맨드가 아직 실행되지 않은 경우 취소 가능` -> 어플리케이션의 상태를 커맨드를 실행하기 전의 상태로 되돌릴 수 있음. (`실행쥐소`)
* 몇가지 `명령을 그룹화 가능` -> 원자성을 가진 `트랜잭션`을 만들거나 그룹의 모든 `작업을 한번에 실행하는 메커니즘 구현`에 사용
* 중복 제거, 결합 및 분할 or 실시간 협업 SW(공동 텍스트 편집 같은) 기반인 `운영 변환(Operational Transformation : OT)` 같은 복잡한 알고리즘을 적용하는 `일련의 커맨드들로 다양한 종류의 변환 수행` 가능

**특히, Node의 네트워킹 및 비동기 실행 필수인 플랫폼에서 중요한 패턴**

* 예제1

  ```javascript
  // createTast.js 
  function createTask(target, args) { // invoker 역할
    return () => {
      target.apply(null, args); // target 실행
    }
  }

  module.exports = createTask;

  // test.js
  const task = require('./createTask'); // invoker
  const args = [{say: "hello"}, {say: "world"}];  // 상태정보

  let task1 = task((s, s2) => {     // callback -> target
    console.log(s.say + s2.say);
  }, args);

  task1();
  ```

* 예제2

```javascript
  // statusUpdate.js -> command  
  const statusUpdateService = { // target  
    statusUpdates: {},

    // 작업 생성
    sendUpdate: function (status) {
      console.log('Status sent : ', status);
      let id = Math.floor(Math.random() * 1000000);
      statusUpdateService.statusUpdates[id] = status;
      return id;
    },

    // 작업 삭제
    destroyUpdate: id => {
      console.log('Status removed : ', id);
      delete statusUpdateService.statusUpdates[id];
    }
  };


  // 커맨드
  function createSendStatusCmd(service, status) {
    let postId = null;

    // 호출될 때 시작하는 함수
    const command = () => {
      postId = service.sendUpdate(status);  // service(target) 상태 업데이트
    };

    // 동작결과 취소
    command.undo = () => {
      if (postId) {
        service.destroyUpdate(postId);      // 취소 (상태정보 없앰)
        postId = null;
      }
    };

    // 커맨드 객체 재구성
    command.serialize = () => {
      return {type: 'status', action: 'post', status}
    };
    return command;
  }

  module.exports = {statusUpdateService, createSendStatusCmd};


  // invoker.js
  class Invoker {

    constructor() {
      this.history = [];
    }

    // 실행
    run(cmd) {
      this.history.push(cmd);
      cmd();
      console.log('Command executed ', cmd.serialize());
    }

    // 지연
    delay(cmd, delay) {
      setTimeout(() => {
        this.run(cmd);
      }, delay);
    }

    // 실행 취소
    undo() {
      const cmd = this.history.pop();
      cmd.undo();
      console.log('Command undone ', cmd.serialize());
    }

    // 웹서비스 사용 -> 네트워크 직렬화
    runRemotely(cmd) {
      request.post('http://localhost:3000/cmd',
                    {json: cmd.serialize()},
                    err => {
                      console.log('Command executed remotely ', cmd.serialize());
                    }
                  )

    }
  }

  module.exports = Invoker;


  // client.js -> 실행 시작점 (엔트리포인트)
  const Invoker = require('./invoker'); // invoker
  const {statusUpdateService, createSendStatusCmd} = require('./statusUpdate'); // target, command

  const invoker = new Invoker();
  const command = createSendStatusCmd(statusUpdateService, "HI~!");   // command
  const command2 = createSendStatusCmd(statusUpdateService, "HI2~!"); // command
  const command3 = createSendStatusCmd(statusUpdateService, "HI3~!"); // command

  invoker.run(command);
  invoker.run(command2);
  invoker.run(command3);
  invoker.undo();
  invoker.delay(command, 1000 * 2); // 1시간
  invoker.delay(command, 1000 * 2); // 1시간
  invoker.delay(command, 1000 * 2); // 1시간
  invoker.delay(command, 1000 * 2); // 1시간
  // invoker.runRemotely(command);
```
