6. 디자인패턴 1/2

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

  • 전략(Strategy)

  • 상태(state)

  • 템플릿(Template)

  • 미들웨어(Middleware)

  • 커맨드(Command)

6.6 전략(Strategy)

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

  • 간단 전략패턴 적용 예제

    // 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-삭제) 모델링 상태전이, 그다음 상태에 대한 관심사분리

  • 적용 예제

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

// 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)

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

  • 예제

// 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 요청 및 응답의 처리

// 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 용 미들웨어 프레임워크 만들기

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

// 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 : 콜백 사용 대신 제너레이터 함수를 사용하는 미들웨어 패턴을 구현 인바운드(다운스트림), 아웃바운드(업스트림)

  • 예제

    // 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

    // 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

  // 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);

Last updated