개인참고자료/자바(네트워크)

TCP 프로그래밍 - 객체 직렬화를 이용한 네트워크 프로그래밍(계산기)

경진 2008. 7. 19. 15:28
객체 직렬화를 이용한 네트워크 프로그래밍

객체 직렬화란 말 그대로 객체를 일렬로 늘어선 바이트의 흐름으로 만드는 기술을 말한다. 이 때 사용하는 IO객체가 ObjectOutputStream과 ObjectInputStream이다.

네트워크 프로그래밍에서도 ObjectOuputStream과 ObjectInputStream을 이용해서 소켓을 통해서 객체를 주고받을 수 있다.

객체 직렬화를 이용해서 각ㄴ단한 계산을 행하는 클라이언트 / 서버 프로그램인데, 클라이언트는 접속에 성공한 후 SendData에 숫자 두개와 연산자를 저장해서 서버에 전송한다. 서버는 전달받은 SendData 객체로부터 숫자 두 개와 연산자를 읽어와서 계산한 후 클라이언트 에게 결과 값을 문자열로 전송하는 프로그램이다.

객체 직렬화를 이용한 원격 계산기의 작동 순서

객체 직렬화를 이용한 원격 계산기의 작동 순서

직렬화 가능 객체 : SendData

SendData 객체는 직렬화를 가능하게 하기 위해서 java.io.Serializable 인터페이스를 구현한다. 그리고 SendData 객체에 있는 세 가지 필드인 op1, op2, opcode도 직렬화가 가능한 기본현 변수와 String 객체다.

op1과 op2에는 계산에 사용할 정수 값을, opcode에는 연산자를 저장한다. 필드 값은 생성자에서 초기화 시키며, 필드에 접근하면 getter 메소드(getOp1(), getOp2(), getOpcode())를 이용한다.

import java.io.Serializable;        
      
public class SendData implements Serializable {       
    private int op1;   
    private int op2;   
    private String opcode;   
    public SendData(int op1, int op2, String opcode){   
        this.op1 = op1;
        this.op2 = op2;
        this.opcode = opcode;
    }   
    public int getOp1(){   
        return op1;
    }   
    public int getOp2(){   
        return op2;
    }   
    public String getOpcode(){   
        return opcode;
    }   
       
} // class       

계산 서버 : ObjectCalculatorServer

계산 서버는 클라이언트로부터 SendData를 전달 받은 후, SendData에 저장되어 있는 두가지 정수 값을 SendData에 저장되어 있는 연산자에 따라서 다르게 계산한다. 그리고 결과 값을 문자열로 만들어 클라이언트에게 전송하게 된다.

import java.net.*;           
import java.io.*;            
           
public class ObjectCalculatorServer {            
           
    public static void main(String[] args) {       
        Socket sock = null;   
        ObjectOutputStream oos = null;   
        ObjectInputStream ois = null;   
        try{   
            ServerSocket ss = new ServerSocket(10005);
            System.out.println("클라이언트의 접속을 대기합니다.");
            sock = ss.accept();
           
            oos = new ObjectOutputStream(sock.getOutputStream());
            ois = new ObjectInputStream(sock.getInputStream());
            Object obj = null;       
            while((obj = ois.readObject()) != null){       
                SendData sd = (SendData)obj;   
                int op1 = sd.getOp1();   
                int op2 = sd.getOp2();   
                String opcode = sd.getOpcode();   
                if(opcode.equals("+")){   
                    oos.writeObject(op1 + " + " + op2 + " = " + (op1 + op2));
                    oos.flush();
                }else if(opcode.equals("-")){   
                    oos.writeObject(op1 + " - " + op2 + " = " + (op1 - op2));
                    oos.flush();
                }else if(opcode.equals("*")){   
                    oos.writeObject(op1 + " * " + op2 + " = " + (op1 * op2));
                    oos.flush();
                }else if(opcode.equals("/")){   
                    if(op2 == 0){   
                        oos.writeObject("0으로 나눌수 없습니다.");
                        oos.flush();
                    }else{   
                        oos.writeObject(op1 + " / " + op2 + " = " + (op1 / op2));
                        oos.flush();
                    }   
                } // end if       
                System.out.println("결과를 전송하였습니다.");       
            } // while           
        }catch(Exception ex){               
            System.out.println(ex);           
        }finally{               
            try{           
                if(oos != null) oos.close();       
            }catch(Exception ex){}           
            try{   
                if(ois != null) ois.close();
            }catch(Exception ex){}   
            try{   
                if(sock != null) sock.close();
            }catch(Exception ex){}   
        } // finally       
    } // main           
}               

10005번에서 동작하는 ServerSocket을 생성한 후, accept() 메소드로 클라이언트 접속에 대기한다. 클라이언트가 접속하면 Socket으로부터 InputStream과 OutputStream을 구현 후, 각각 ObjectInputStream과 ObjectOutputStream 형태로 변환시킨다.

객체 직렬화를 이용해서 네트워크에 전송하려면 ObjectOutputStream과 ObjectInputStream을 사용해야 하기 때문이다.

            ServerSocket ss = new ServerSocket(10005);
            System.out.println("클라이언트의 접속을 대기합니다.");
            sock = ss.accept();
           
            oos = new ObjectOutputStream(sock.getOutputStream());
            ois = new ObjectInputStream(sock.getInputStream());

자바 IO는 생성자가 상당히 중요하다. 생성자에 어떤 객체를 지정하느냐에 따라서 읽고 쓰는 대상이 달라지기 때문이다. ObjectOutputStream의 경우 생성자의 인자로 FileOutputStream을 지정했다면 ObjectOutputStream은 파일에 객체를 저장하게 된다. 위 예제에서는 소켓을 통해서 OutputStream을 얻어와 지정했기 때문에 ObjectOutputStream을 통해 객체를 네트워크 내에 출력할 수 있게 된다.

ObjectInputStream에 있는 readObject() 메소드를 이용해서 클라이언트가 전송한 객체 하나를 읽어 들인다. 클라이언트가 전송한 객체는 SendData므로 SendData 형태로 형 변환한 후 SendData에 있는 getter 메소드를 이용해서 정수 값 두개와 문자열 형태의 연산자를 구한다. 그 후에 if문을 이용해서 연산자가 어떤 형태인지 파악한 후, ObjectOutputStream에 있는 writeObject() 메소드를 이용해서 클라이언트에게 문자열을 전송한다.

            while((obj = ois.readObject()) != null){       
                SendData sd = (SendData)obj;   
                int op1 = sd.getOp1();   
                int op2 = sd.getOp2();   
                String opcode = sd.getOpcode();   
                if(opcode.equals("+")){   
                    oos.writeObject(op1 + " + " + op2 + " = " + (op1 + op2));
                    oos.flush();
                }

계산 클라이언트 : ObjectCalculatorClient

계산 클라이언트는 키보드로 부터 정수 두 개와 연산자를 입력받은 후, 입력 받은 내용을 SendData 객체에 저장한다. 그리고 ObjectOutputStream을 이용해서 서버에 전송한다. 그 후 서버로부터 계산된 결과인 문자열을 읽어 들이기 위해서 ObjectInputStream을 사용한다.

import java.net.*;           
import java.io.*;            
           
public class ObjectCalculatorClient {            
           
    public static void main(String[] args) {       
        if(args.length != 1){   
            System.out.println("사용법 : java ObjectCalculatorClient ip");
            System.exit(0);
        }   
        Socket sock = null;   
        ObjectOutputStream oos = null;   
        ObjectInputStream ois = null;   
        try{   
            sock = new Socket(args[0], 10005);
           
            oos = new ObjectOutputStream(sock.getOutputStream());       
            ois = new ObjectInputStream(sock.getInputStream());       
            BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));       
            String line = null;       
            while(true){       
                System.out.println("첫번째 숫자를 입력하여 주세요.(잘못 입력된 숫자는 0으로 처리합니다.)");   
                line = keyboard.readLine();   
                int op1 = 0;   
                try{   
                    op1 = Integer.parseInt(line);
                }catch(NumberFormatException nfe){   
                    op1 = 0;
                }   
                System.out.println("두번째 숫자를 입력하여 주세요. (잘못 입력된 숫자는 0으로 처리합니다.)");   
                line = keyboard.readLine();   
                int op2 = 0;   
                try{   
                    op2 = Integer.parseInt(line);
                }catch(NumberFormatException nfe){   
                    op2 = 0;
                }   
                System.out.println("+, -, *, / 중에 하나를 입력하여 주세요. ( 잘못입력하면 + 로 처리합니다.)");   
                line = keyboard.readLine();   
                String opcode = "+";   
                if(line.equals("+") || line.equals("-") || line.equals("*") || line.equals("/"))   
                    opcode = line;
                else   
                    opcode = "+";
                SendData s = new SendData(op1, op2, opcode);   
                oos.writeObject(s);   
                oos.flush();   
                String msg = (String)ois.readObject();   
                System.out.println(msg);
                System.out.println("계속 계산하시겠습니까?(Y/n)");
                line = keyboard.readLine();
                if(line.equals("n")) break;
                System.out.println("다시 계산을 시작합니다.");
            } // while   
            System.out.println("프로그램을 종료합니다.");    
               
               
        }catch(Exception ex){       
            System.out.println(ex);   
        }finally{       
            try{   
                if(oos != null) oos.close();
            }catch(Exception ex){}   
            try{   
                if(ois != null) ois.close();
            }catch(Exception ex){}   
            try{   
                if(sock != null) sock.close();
            }catch(Exception ex){}   
        } // finally       
    } // main           
}               

서버에 접속하기 위한 Socket을 생성한 후, Socket을 통해서 얻은 OutputStream과 InputStream을 각각 ObjectOutputStream과 ObjectInputStream으로 변환시킨다.

            sock = new Socket(args[0], 10005);
           
            oos = new ObjectOutputStream(sock.getOutputStream());       
            ois = new ObjectInputStream(sock.getInputStream());       

키보드로부터 정수 값 두개와 연산자를 읽어와 SendData 생성자에 지정해서 SendData 객체를 생성한다. 그 후에 ObjectOutputStream의 writeObject() 메소드를 이용해서 서버에게 SendData 객체를 전송한다. 서버는 SendData 객체를 전달 받아 계산된 결과를 문자열 형태로 다시 클라이언트에게 전송하게 되고, 서버는 ObjectInputStream에 있는 readObject() 메소드를 이용해서 읽어 들여 화면에 결과를 출력한다.

                SendData s = new SendData(op1, op2, opcode);   
                oos.writeObject(s);   
                oos.flush();   
                String msg = (String)ois.readObject();   
                System.out.println(msg);

객체 직렬화를 이용한 계산 서버와 클라이언트 실행

ObjectCalculatorServer를 실행한 후 ObjectCalculatorClient를 실행한다.
클라이언트에서 입력한 정수 값 두개와 연산자를 SendData 객체에 저장해서 서버에게 전달되면 서버는 SendData의 값을 이용해서 계산한 후, 그 결과를 클라이언트에게 전송하게 된다. 클라이언트는 계산된 결과를 화면에 출력하게 되는데, 그 결과가 제대로 출력되는지 확인한다.

클라이언트 실행 결과

클라이언트 실행 결과

서버 실행 결과

서버 실행 결과

객체 직렬화는 편리하지만 무겁다

객체직렬화 기술을 이용한 네트워크 프로그래밍은 아주 쉽게 객체를 주고받을 수 있게 해준다. 하지만 내부적으로 마샬링(marshalling)과 언마샬링(unmarshalling)과정을 거치기 때문에 추가적인 과부하가 발생할 수 있다. 이런 이유로 좀더 빠른 속도를 필요하고 자원을 적게 사용하길 원할 경우에는 개체 직렬화를 권장하지 않는다.

현대의 프로그래밍은 추가적인 과부하가 발생되더라도 좀더 쉽고 빠르게 작성하려는 경향이 있다. 이는 하드웨어의 발전 속도가 눈부시게 빠르기 때문이다. 따라서 객체 직렬화 기술이 추가적인 과부하가 발생한다고 해서 사용되지 않는 것이 아니다. 바로 객체 직렬화 기술은 다음에 배우게 될 RMI 기술과 내부적으로 사용되며, 또한 RMI 기술은 J2EE 기술의 내부적으로 사용된다.