semtax의 개발 일지

디자인패턴 공부내용 정리 : 브로커 패턴 본문

개발/Java

디자인패턴 공부내용 정리 : 브로커 패턴

semtax 2020. 5. 17. 19:58
반응형

개요

이번에는, 아키텍처 패턴 중 하나인 브로커 패턴에 대해서 알아보도록 하겠습니다.

왜 나오게 됬는가?

브로커 패턴은, 서로 다른 기종의 머신에 분산되어있는 서비스(객체 혹은 컴포넌트)간에 어떻게 협력을 잘 할지 고민하다 나온 패턴입니다.

따라서, 분산 시스템이나 RPC를 구현할때 사용되는 패턴에 많이 사용됩니다.

문제 상황

먼저 한가지 상황을 가정 해봅시다.

당신이 만약에, 대기업에서 IoT 기반의 스마트 홈 네트워크 시스템을 구축한다고 가정합시다.

그런데, 같이 참여하는 업체가 10개가 넘어가는데다, 임베디드 장비들이 전부 CPU 기종이 다르고 프로토콜도 제멋대로인 상황에 처하였습니다.

더 황당한것은, 임베디드 장비들이 교체되는 일도 빈번하고, 그에 따라 연결되는 서버나 주소도 자주 바뀐다는 것입니다.

그런데, 장비들이 교체되도 별다른 설정이 없이 잘 돌아야한다는 요구사항을 받았습니다.

이때 당신은 어떤 선택을 하시겠습니까?

위와 같은 상황에 처했을때, 브로커 패턴을 사용할 수 있습니다.

브로커 패턴을 사용하면 다음과 같은 상황에서 유용합니다.

  • 실제 시스템 컴포넌트를 사용하는 사용자로부터, 서비스의 구체적인 구현을 감추어야 할때(캡슐화)
  • 런타임에, 시스템 컴포넌트들을 교체 할 수 있습니다.
  • 시스템 컴포넌트가 어디에 위치해있던지 신경쓰지 않고 호출 가능합니다.

그러면, 위의 문제를 어떻게 해결 할 수 있는지 확인 해 봅시다.

해결법

위와 같은 상황에서는, 서버와 클라이언트 사이에 브로커(Dispatcher) 라는 서비스 컴포넌트를 두어서, 실제 패킷이 해당 컴포넌트를 거쳐서 가게 하면 됩니다.

그리고, 브로커에서는 클라이언트에 패킷을 전달 받아서, 어떠한 종류의 서비스를 요청하는지를 파악하고 해당 서비스에 전달하는 역할을 수행합니다.

또한, 브로커에서 전달한 요청을 응답 받은뒤, 실제 요청을 보낸 클라이언트에 전달을 해줍니다.

먼저 대략적인 구조를 그림으로 나타내면 대략적으로 아래와 같습니다.

보내는 패킷은 아래와 같이 보내게 됩니다.

0x5001|semtax|22

이때, 0x5001은 어떠한 방법으로 요청을 처리하라는 일종의 커맨드이고, 나머지 부분이 데이터의 역할을 하게 됩니다.

이제 실제로 구현을 해보도록 합시다.

구현

일단 우리가 구현하려는 예제의 구조는 아래와 같습니다.

먼저, 브로커에 해당하는 Dispatcher 클래스를 아래와 같이 만들어 줍니다.

package main.java.brokerexample;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Dispatcher {

    private final int HEADER_SIZE = 6;

    public void dispatch(ServerSocket serverSocket) {
        try{
            Socket socket = serverSocket.accept();
            demultiplex(socket);
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void demultiplex(Socket socket) {
        try {
            InputStream inputStream = socket.getInputStream();

            byte[] buffer = new byte[HEADER_SIZE];
            inputStream.read(buffer);
            String header = new String(buffer);

            BaseDeviceProtocol baseDeviceProtocol;

            switch(header) {
                case "0x5001":
                    baseDeviceProtocol = new SamsungDeviceProtocol();
                    break;
                case "0x6001":
                    baseDeviceProtocol = new LGDeviceProtocol();
                    break;
                default:
                    baseDeviceProtocol = new UnhandledDeviceProtocol();
                    break;
            }

            baseDeviceProtocol.handleEvent(inputStream);
        } catch(IOException e) {
            e.printStackTrace();
        }
    }
}

Dispatcher 클래스는 클라이언트로 부터 래핑된 프로토콜을 받아서, 프로토콜의 헤더가 어떤거냐에 따라 그에 맞는 적절한 핸들러를 호출해주는 역할을 수행합니다.

(네트워크로 치면 일종의 라우터 역할을 하는거라고 보시면 됩니다. )

그리고, 실제로 Dispatcher를 통해서 들어온 프로토콜들을 처리해주는 클래스들을 아래와 같이 만들어 줍시다.

package main.java.brokerexample;

import java.io.InputStream;

public abstract class BaseDeviceProtocol {
    public abstract void handleEvent(InputStream inputStream);
    protected abstract void processDeviceInfo(String[] params);
}

아래의 코드는 적절한 프로토콜을 찾지 못했을때 요청을 처리하는 핸들러 입니다.

package main.java.brokerexample;

import java.io.InputStream;

public class UnhandledDeviceProtocol extends BaseDeviceProtocol {

    @Override
    public void handleEvent(InputStream inputStream) {
        processDeviceInfo(new String[] {"Unknown Device!"});
    }

    @Override
    protected void processDeviceInfo(String[] params) {
        System.out.println(params[0]);
    }
}

아래의 코드는 삼성 또는 LG의 프로토콜에 대한 요청을 처리하는 핸들러 입니다.

package main.java.brokerexample;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.StringTokenizer;

public class LGDeviceProtocol extends BaseDeviceProtocol{

    private static final int DATA_SIZE = 1024;
    private static final int TOKEN_NUM = 5;

    public void handleEvent(InputStream inputStream) {
        try {
            byte[] buffer = new byte[DATA_SIZE];

            inputStream.read(buffer);
            String data = new String(buffer, StandardCharsets.UTF_8);

            String[] params = new String[TOKEN_NUM];
            StringTokenizer token = new StringTokenizer(data, "|");

            int i = 0;
            while(token.hasMoreTokens()) {
                params[i] = token.nextToken();
                ++i;
            }
            processDeviceInfo(params);
        }catch(IOException e){
            e.printStackTrace();
        }
    }

    protected void processDeviceInfo(String[] params) {
        System.out.println("LG Device : " + params[0]
            + "\n Device No : " + params[1]
            + "\n Device Protocol " + params[2]
            + "\n Device Name " + params[3]
            + "\n Device Network " + params[4]);
    }
}


...


package main.java.brokerexample;

import java.io.IOException;
import java.io.InputStream;
import java.util.StringTokenizer;

public class SamsungDeviceProtocol extends BaseDeviceProtocol{

    private static final int DATA_SIZE = 512;
    private static final int TOKEN_NUM = 2;

    public void handleEvent(InputStream inputStream) {

        try {
            byte[] buffer = new byte[DATA_SIZE];
            inputStream.read(buffer);
            String data = new String(buffer);
            String[] params = new String[TOKEN_NUM];
            StringTokenizer token = new StringTokenizer(data, "|");

            int i = 0;
            while(token.hasMoreTokens()) {
                params[i] = token.nextToken();
                ++i;
            }

            processDeviceInfo(params);
        }catch(IOException e){
            e.printStackTrace();
        }

    }

    protected void processDeviceInfo(String[] params) {
        System.out.println("Samsung Device : " + params[0] + "\nDevice No : " + params[1]);
    }
}

마지막으로 해당 Dispatcher를 호출하는 메인 코드 입니다.

package main.java;

import main.java.brokerexample.Dispatcher;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;

public class BrokerExample {

    public static void main(String[] args) {
        int port = 5000;
        System.out.println("Server ON : " + port);

        try {
            ServerSocket serverSocket = new ServerSocket(port);
            Dispatcher dispatcher = new Dispatcher();

            while(true) {
                dispatcher.dispatch(serverSocket);
            }

        }catch(UnknownHostException e) {
            e.printStackTrace();
        }
        catch(IOException e) {
            e.printStackTrace();
        }
    }
}

실행 방법

먼저 아래와 같이 커맨드 라인을 켜서 실행을 해줍시다

echo "0x5001|Samsung|22" | nc localhost 5000
^C (Ctrl + C)
echo "0x6001|LG|22|csv|G5|wifi" | nc localhost 5000
^C (Ctrl + C)
echo "0x7001|LG|22|csv|G5|wifi" | nc localhost 5000
^C (Ctrl + C)

이렇게 실행을 하면 대략적으로 아래와 같은 결과가 나오게 됩니다.

// 삼성 디바이스의 경우
Samsung Device : Samsung
Device No : 22

// LG 디바이스의 경우
LG Device : LG
 Device No : 22
 Device Protocol csv
 Device Name G5
 Device Network wifi

// 나머지 경우
Unknown Device!

브로커 패턴은 위와 같이 여러 가지 프로토콜이 들어와야 하는데 일관되게 처리하고 싶을때 유용하게 사용 할 수 있습니다.

예시

아파치 Thrift나 protobuf(gRPC) 와 같은 서비스가 이러한 브로커 패턴을 자동으로 만들어주는 서비스 입니다.

실제로, 아파치 Thrift와 같은 경우 아래와 같이 프로토콜을 명시해주면 자동으로 타겟언어에 맞춰서 브로커 패턴을 따르는 뼈대 코드를 컴파일해서 만들어줍니다.

include "shared.thrift"


namespace cl tutorial
namespace cpp tutorial
namespace d tutorial
namespace dart tutorial
namespace java tutorial
namespace php tutorial
namespace perl tutorial
namespace haxe tutorial
namespace netstd tutorial

typedef i32 MyInteger

const i32 INT32CONSTANT = 9853
const map<string,string> MAPCONSTANT = {'hello':'world', 'goodnight':'moon'}

enum Operation {
  ADD = 1,
  SUBTRACT = 2,
  MULTIPLY = 3,
  DIVIDE = 4
}

struct Work {
  1: i32 num1 = 0,
  2: i32 num2,
  3: Operation op,
  4: optional string comment,
}


exception InvalidOperation {
  1: i32 whatOp,
  2: string why
}

service Calculator extends shared.SharedService {
   void ping(),

   i32 add(1:i32 num1, 2:i32 num2),

   i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch),

   oneway void zip()

}

실제로 생성되는 코드들은 아래 URL에 존재합니다.

https://gitbox.apache.org/repos/asf?p=thrift.git;a=tree;f=tutorial/java;h=a5537b5ca0cd99a80765f55a2df218bed7e50a89;hb=HEAD

 

ASF Git Repos - thrift.git/tree - tutorial/java/

 

gitbox.apache.org

 

 

출처

1. Pattern Oriented Software Architecture Vol. 1 
2. SW마에스트로, NHN Next 아키텍처 수업 교육자료

반응형
Comments