Java RMI (Remote Method Invocation)와 그를 활용한 통신 프로그램을 만들어 보겠다.
먼저, Head First Design Patterns를 참고해 원격 메서드에 대해서 기본적인 것을 알아보겠다.
Local 객체에 대해서 메서드를 호출할 때, 그 요청을 원격 객체에 전달할 수 있게 해주는 시스템을 만들것이다. 통신을 처리해주는 보조 객체가 필요할 것이고, 그 보조 객체를 이용하면 Client 입장에서는 Local 객체에 대해서만 메서드를 호출하면 된다. 그러면 Client 보조 객체가 그 요청을 원격 객체한테 전달한다.
즉, Client 객체는 원격 서비스에 있는 메서드를 호출한다고 생각하고 작업을 처리한다 (Client 보조 객체가 Service 객체인것처럼 행세함). 하지만 Client 보조 객체는 진짜 원격 서비스가 아니다 (진짜 서비스와 같은 메서드가 들어있기 때문에 진짜 원격 서비스인 것처럼 보이지만, Client에서 필요로 하는 실제 메서드 로직이 그 안에 있는것이 아니기 때문에). Client 보조 객체에서는 Server에 연락을 취하고, 메서드 호출에 대한 정보 (Method name, parameter 등)를 전달 후, 서버에서 리턴되는 정보를 기다린다.
Server 쪽에는 service 보조 객체가 있어서, Client 보조 객체로부터 요청을 Socket 연결을 통해 받아오고, 호출에 대한 정보를 해석해서 진짜 Service 객체에 있는 메서드를 호출한다. Service 객체 입장에서는 해당 메서드 호출이 원격 Client가 아닌 Local 객체로부터 들어오는 셈이다.
Service 보조 객체는 리턴값을 받아와서 Socket 출력 스트림을 통해 Client 보조 객체한테 전송한다. 그러면 Client 보조 객체가 Client에게 리턴해주는 식이다.
- RMI의 개요
RMI에서는 우리 대신 Client와 Service 보조 객체를 만들어 준다. RMI를 이용하면 우리가 직접 네트워킹이나 입출력 관련 코드를 직접 작성하지 않아도 된다. Client에서는 그 Client와 같은 local JVM에 있는 메서드를 호출하듯이 원격 메서드를 호출할 수 있다. 또한 RMI는 Client에서 원격 객체를 찾아 그 객체에 접근하기 위해 쓸 수 있는 룩업(lookup) 서비스 같은 것도 제공해 준다.
RMI 호출과 일반적인 local 메서드 호출의 차이점도 있는데, Client 입장에서는 로컬 메서드 호출과 똑같은 식으로 메서드를 호출하면 되지만 Client 보조 객체는 네트워크를 통해서 호출을 전송한다는 점이다. 따라서 네트워킹이나 입출력 기능을 활용할 수 밖에 없는데 이 기능들은 언제든 예외가 발생할 수 있는 위험이 따른다. 결과적으론 Client에서도 예외를 대비해야 한다는 것이다.
- 원격 서비스 만들기
원격 서비스를 만들기 위한 단계를 다섯가지로 나눠서 보겠다. 일반 객체를 원격 Client로부터 들어온 메서드 호출을 받아서 처리할 수 있는 원격 서비스로 개조할 수도 있다.
1. 원격 인터페이스를 만든다.
- 원격 Interface에서는 Client에서 원격으로 호출할 수 있는 메서드를 정의한다. Client에서 이 인터페이스를 서비스의 클래스 형식으로 사용한다. stub와 실제 서비스에서 모두 이 인터페이스를 구현해야 한다.
RMI 용어 → RMI에서 Client 보조 객체는 stub(스터브), Service 보조 객체는 skeleton(스켈레톤)이라고 한다.
2. Service 구현 클래스를 만든다.
- 실제 작업을 처리하는 클래스이다. 원격 인터페이스에서 정의한 원격 메서드를 실제로 구현한 코드들이 있는 클래스이다. 나중에 Client에서 이 객체에 있는 메서드를 호출하게 된다.
3. rmic를 이용하여 stub과 skeleton을 만든다.
- Client와 Service의 보조 객체를 생성한다. 직접 클래스를 만들거나 소스 코드를 건드릴 필요는 없다. JDK에 포함된 rmic 툴을 실행시키면 자동으로 만들어진다.
4. RMI 레지스트리를 실행시킨다.
- rmiregistry는 전화번호부와 비슷하다고 보면 된다. Client에서는 이 레지스트리로부터 프록시(Client stub)를 받아갈 수 있다.
5. 원격 서비스 시작
- Service 객체를 가동시켜야 한다. 서비스를 구현한 클래스에서 서비스의 instance를 만들고 그 인스턴스를 RMI 레지스트리에 등록한다. 일단 RMI 레지스트리에 등록되고나면 Client에서 그 서비스를 사용할 수 있다.
RMI의 특성을 이용한 채팅 프로그램을 만들것이다.
우선 클라이언트에 해당하는 윈도우를 만들겠다.
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.rmi.*;
class ButtonPanel extends JPanel
{
MainPanel clientWnd;
JTextField ip;
JButton connectButton;
JButton closeButton;
ButtonPanel(MainPanel clientWnd) {
this.clientWnd = clientWnd;
ip = new JTextField(15);
connectButton = new JButton("Connect");
closeButton = new JButton("Close");
ip.setText("localhost");
add(ip);
add(connectButton);
add(closeButton);
connectButton.addActionListener(clientWnd);
closeButton.addActionListener(clientWnd);
}
String getIpAddress() {
return ip.getText();
}
}
class InputListener extends KeyAdapter
{
MainPanel clientWnd;
JTextField textInput;
InputListener(JTextField textInput,MainPanel clientWnd) {
this.clientWnd = clientWnd;
this.textInput = textInput;
}
public void keyPressed(KeyEvent ev) {
int keyCode = ev.getKeyCode();
if (keyCode == KeyEvent.VK_ENTER)
{
String msg = textInput.getText().trim();
clientWnd.writeText("[I say] " + msg);
clientWnd.sendLine(msg);
textInput.setText("");
}
}
}
class ReportPanel extends JPanel
{
MainPanel clientWnd;
JTextField reportText;
JButton reportButton;
JButton queryButton;
ReportPanel(MainPanel clientWnd) {
this.clientWnd = clientWnd;
reportText = new JTextField(15);
reportButton = new JButton("Report");
queryButton = new JButton("Query");
add(reportText);
add(reportButton);
add(queryButton);
reportButton.addActionListener(clientWnd);
queryButton.addActionListener(clientWnd);
}
String getReportText() {
return reportText.getText();
}
}
class InputPanel extends JPanel
{
MainPanel clientWnd;
JTextField textInput;
ReportPanel reportPanel;
InputPanel(MainPanel clientWnd) {
this.clientWnd = clientWnd;
setLayout(new GridLayout(2,1));
textInput = new JTextField();
textInput.addKeyListener(new InputListener(textInput,clientWnd));
reportPanel = new ReportPanel(clientWnd);
add(textInput);
add(reportPanel);
}
String getReportText() {
return reportPanel.getReportText();
}
}
class MainPanel extends JPanel implements ActionListener
{
String ipAddress = "localhost";
PrintWriter toChatServer;
Thread chatThread;
String myIP;
ButtonPanel buttonPanel;
InputPanel inputPanel;
JTextArea textBox;
JScrollPane textPane;
MainPanel() {
buttonPanel = new ButtonPanel(this);
setLayout(new BorderLayout());
textBox = new JTextArea();
textPane = new JScrollPane(textBox);
textBox.setBorder(BorderFactory.createLoweredBevelBorder());
inputPanel = new InputPanel(this);
add(buttonPanel,BorderLayout.NORTH);
add(textPane,BorderLayout.CENTER);
add(inputPanel,BorderLayout.SOUTH);
try
{
InetAddress address = InetAddress.getLocalHost();
myIP = address.getHostAddress();
System.out.println(myIP);
}
catch (Exception ex)
{
}
}
public void writeText(String msg) {
textBox.append(msg + "\r\n");
}
public void sendLine(String msg) {
toChatServer.println(msg);
toChatServer.flush();
}
public void actionPerformed(ActionEvent ev) {
String cmd = ev.getActionCommand();
String ipAddress = buttonPanel.getIpAddress();
if (cmd.equals("Report"))
{
try
{
String remoteHost = "rmi://" + ipAddress + "/RemoteServer";
Server s = (Server)Naming.lookup(remoteHost);
int data = Integer.parseInt(inputPanel.getReportText());
String answer = s.report(myIP,data);
writeText("[Server says] " + answer);
}
catch (Exception ex)
{
}
} else if (cmd.equals("Query"))
{
} else if (cmd.equals("Connect"))
{
chatThread = new Thread() {
public void run() {
}
};
chatThread.start();
} else if (cmd.equals("Close"))
{
}
}
}
class ClientFrame extends JFrame
{
ClientFrame() {
super("Client");
setSize(400,600);
setDefaultCloseOperation(EXIT_ON_CLOSE);
Container contentPane = getContentPane();
contentPane.add(new MainPanel());
}
}
class TestClient
{
public static void main(String[] args)
{
ClientFrame clientFrame = new ClientFrame();
clientFrame.setVisible(true);
}
}
Client의 윈도우는 ip값을 불러오기 위한 textField와 Connect와 Close를 수행할 버튼을 갖는 ButtonPanel, text를 입력하여 report를 해줄 기능을 갖는 textField와 Report 버튼, Query 버튼의 ReportPanel, 채팅입력을 위한 InputPanel 그리고 이것들을 담아줄 MainPanel과 이벤트를 위한 Listener 등으로 구성된다.
MainPanel은 위에서 말한 바와 같이 panel의 North 부분에는 ButtonPanel을 달아주고, Center에는 채팅 화면을 구성해줄 textArea를 scrollPane에 감싸서 배치해주겠다. 이 textArea부분은 setBorder의 createLoweredBevelBorder를 통해 테두리가 파여있는 효과를 준다. South에는 채팅 입력을 위한 InputPanel을 달아준다. InputPanel에는 ReportPanel이 함께 달려있다.
MainPanel Constructor 부분의 try~catch문은 InetAddress.getLocalHost() 메서드와 getHostAddress() 메서드를 통해 ip를 제대로 받아왔는지 출력해 보는 테스트이다.
actionPerformed 함수를 보겠다. 이벤트 parameter로 들어온 값이 Report, Query, Connect, Close 인가로 나누어서 반응하도록 할 것이다. 문자열 cmd에는 어떤 명령이 들어왔는지 event의 getActionCommand()를 받아준 후 네개의 명령중 어떤 명령인지 판별한다. ipAddress에는 buttonPanel의 textField에 적힌 ip값을 얻어온다.
Report → remoteHost라는 변수에 ip주소가 적힌 rmi 경로값을 저장한다. 아래에 만들 Remote를 상속받은 Server 인터페이스 s에 java.rmi.Naming 클래스의 lookup(remoteHost)를 저장하겠다. lookup은 리턴 타입이 Remote 객체이며 api에서는 Returns a reference, a stub, for the remote object associated with the specified name 이라고 소개된다. 이 s는 inputPanel에서 가져올 data값을 myIP와 넘겨줄것이다. 그리고 Server를 통해 report하고, 넘겨받는 문자열을 textBox에 msg append해주는 writeText를 해준다.
Query → 위와 비슷한 방식으로 query 기능을 해줄것이다.
Connect → chatThread를 돌리면서 Server쪽과 상호작용할 수 있게 한다.
Close → 안전하게 disconnect 시키고 프로그램을 종료하게 한다.
채팅을 치는 부분은 KeyAdapter를 상속받는 InputListener에서 다룬다. InputPanel에서 clientWnd와 textInput을 listener로 등록하고 해당 textField에서 채팅 입력후 enter를 치면 clientWnd에 textArea에 메시지를 추가해주는 식이다.
import java.rmi.*;
public interface Server extends Remote
{
public String report(String ip, int salesData) throws RemoteException;
public int query(String ip) throws RemoteException;
}
Remote 인터페이스를 상속받는 Server 인터페이스이다. Remote 인터페이스는 API에서 다음과 같이 소개한다.
The Remote interface serves to identity interfaces whose methods may be invoked from a non-local virtual machine. Any object that is a remote object must directly or indirectly implements this interface. Only those methods specified in a "remote interface", an interface that extends java.rmi.Remote are available remotely.
Implementation classes can implement any number of remote interfaces and can extend other remote implementation classes. RMI provides some convenience classes that remote object implementations can extend which facilitate remote object creation. These classes are java.rmi.server.UnicastRemoteObject and java.rmi.activation.Activatable.
이 인터페이스에 report와 query 기능을 해줄 메서드를 정의해준다.
이제 서버 윈도우를 보겠다.
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.rmi.*;
import java.rmi.server.*;
class ServerImpl implements Server
{
MainPanel wnd;
public ServerImpl(MainPanel wnd) throws RemoteException {
this.wnd = wnd;
}
public String report(String ip, int salesData) throws RemoteException {
wnd.writeText("[" + ip + "]" + "reported his selling today.");
wnd.writeText("[" + ip + "]" + salesData + "Won.");
return "Thank you";
}
public int query(String ip) throws RemoteException {
wnd.writeText("[" + ip + "]" + "asked his total selling.");
return 10;
}
}
class ButtonPanel extends JPanel
{
MainPanel serverWnd;
JButton startButton;
JButton stopButton;
JButton salesRecordButton;
ButtonPanel(MainPanel serverWnd) {
this.serverWnd = serverWnd;
startButton = new JButton("Start");
stopButton = new JButton("Stop");
salesRecordButton = new JButton("Sales Record");
add(startButton);
add(stopButton);
add(salesRecordButton);
startButton.addActionListener(serverWnd);
stopButton.addActionListener(serverWnd);
salesRecordButton.addActionListener(serverWnd);
}
}
class InputListener extends KeyAdapter
{
MainPanel serverWnd;
JTextField textInput;
InputListener(JTextField textInput,MainPanel serverWnd) {
this.serverWnd = serverWnd;
this.textInput = textInput;
}
public void keyPressed(KeyEvent ev) {
int keyCode = ev.getKeyCode();
if (keyCode == KeyEvent.VK_ENTER)
{
String msg = textInput.getText().trim();
serverWnd.writeText("[I say ] " + msg);
String clientIP = serverWnd.getSelectedIP();
//serverWnd.sendLine(clientIP,msg);
textInput.setText("");
}
}
}
class InputPanel extends JPanel
{
MainPanel serverWnd;
JComboBox<String> ips;
JTextField textInput;
InputPanel(MainPanel serverWnd) {
this.serverWnd = serverWnd;
setLayout(new GridLayout(2,1));
ips = new JComboBox<String>();
textInput = new JTextField();
textInput.addKeyListener(new InputListener(textInput,serverWnd));
add(ips);
add(textInput);
}
public void addComboBoxItem(String ip) {
int n;
ips.removeItem(ip);
ips.insertItemAt(ip,0);
ips.setSelectedIndex(0);
}
String getSelectedIP() {
return ips.getItemAt(ips.getSelectedIndex());
}
}
class MainPanel extends JPanel implements ActionListener
{
ButtonPanel buttonPanel;
JTextArea textBox;
JScrollPane textPane;
InputPanel inputPanel;
String myIP;
ServerImpl server;
MainPanel() {
buttonPanel = new ButtonPanel(this);
setLayout(new BorderLayout());
textBox = new JTextArea();
textPane = new JScrollPane(textBox);
textBox.setBorder(BorderFactory.createLoweredBevelBorder());
inputPanel = new InputPanel(this);
add(buttonPanel,BorderLayout.NORTH);
add(textPane,BorderLayout.CENTER);
add(inputPanel,BorderLayout.SOUTH);
writeText("Please press start button");
try
{
InetAddress address = InetAddress.getLocalHost();
myIP = address.getHostAddress();
}
catch (Exception ex)
{
}
}
String getSelectedIP() {
return inputPanel.getSelectedIP();
}
public void addComboBoxItem(String ip) {
inputPanel.addComboBoxItem(ip);
}
public void writeText(String msg) {
textBox.append(msg + "\r\n");
}
public void actionPerformed(ActionEvent ev) {
String command = ev.getActionCommand();
if (command.equals("Start"))
{
try
{
server = new ServerImpl(this);
writeText("Server started...");
writeText(myIP + " as name: RemoteServer");
Naming.rebind("RemoteServer", server);
}
catch (Exception ex)
{
}
} else if (command.equals("Stop"))
{
}
}
}
class ServerFrame extends JFrame
{
ServerFrame() {
super("Server");
setSize(400,600);
setDefaultCloseOperation(EXIT_ON_CLOSE);
Container contentPane = getContentPane();
contentPane.add(new MainPanel());
}
}
class TestServer
{
public static void main(String[] args)
{
ServerFrame serverFrame = new ServerFrame();
serverFrame.setVisible(true);
}
}
ServerImpl에서는 report와 query가 할 일을 구체적으로 정의해준다. ButtonPanel에는 서버를 실행시켜줄 Start 버튼과 서버 중단을 위한 Stop 버튼 Sales Record를 띄워줄 버튼을 세개 배치한다. InputListener는 Client의 InputListener와 같은 역할을 해준다. 다만, Client가 여러개일 경우를 대비하여 InputPanel에 있는 getSelectedIP를 통해 메시지를 보낼 Client의 IP값을 받아온다.
또한 InputPanel에서는 Client들의 ip 주소들이 comboBox 형태로 저장되어질 것이다. addComboBoxItem을 통해 들어오는 ip주소들이 저장된다.
사용하는것 자체는 Client와 거의 동일하고 Start를 누르면 Naming 클래스의 rebind 메서드를 통해 다시 바인딩 한다. Api에서는 Rebinds the specified name to a new remote object.라고 소개한다.
댓글