티스토리 툴바


'전체'에 해당되는 글 5건

  1. 2008/10/02 xml 직렬화 하기
  2. 2008/10/02 자바 직렬화란 무엇인가?
  3. 2008/09/29 웹로직 시작시 InvalidClassException 오류가 발생한다면
  4. 2008/09/26 mvc 강좌
  5. 2008/09/23 멀티 채팅 기본 소스및 원리 자바

xml 직렬화 하기


XML 직렬화는 객체 퍼시스턴스와 데이터 전송을 포함한 무수히 많은 쓰임새가 있습니다. 그러나 일부 XML 직렬화 기술은 구현하기 어렵습니다. XStream은 자바 객체를 XML로 직렬화하거나 그 반대로 역직렬화할 수 있는 가볍고 사용하기 쉬운 오픈 소스 자바(Java™) 라이브러리입니다. XStream을 설정하고, 이를 이용해 객체를 직렬화하고 역직렬화하는 방법을 배웁니다. 또한 설정 속성들을 XML 설정 파일에서 읽어오는 방법도 알아봅니다.

XStream 을 사용하면, 대부분의 자바 객체를 아무런 매핑없이 직렬화할 수 있다. 객체 이름은 요소 이름이 되고, 클래스 내의 문자열들이 XML 요소의 내용을 구성한다. XStream으로 직렬화할 클래스는 Serializable 인퍼페이스를 구현할 필요도 없다. XStream은 직렬화 도구일 뿐, 데이터 바인딩 도구가 아니므로 XML이나 XML 스키마 정의(XML Schema Definition: XSD)로부터 클래스를 생성하지 않는다.

XStream은 다른 직렬화 도구와 구별되는 세 가지 특징이 있다.

  1. XStream은 클래스에서 직렬화/역직렬화될 필드들의 가시성을 고려하지 않는다.
  2. 클래스에서 직렬화/역직렬화될 필드들을 위한 getter나 setter 메서드가 필요없다.
  3. 직렬화/역직렬화될 클래스는 기본 생성자가 필요없다.
어떤 서드파티 클래스라도 아무런 변경없이 XStream을 사용해 직렬화하고 역직렬화할 수 있다.

개발 환경 설정하기

다음에 나오는 절차에 따라 XStream를 다운로드하고 설치하자(참고자료).

  1. 이클립스 웹 사이트에서 이클립스를 다운로드해 원하는 폴더에 설치하자. 해당 디렉터리를 이 기사에서는 eclipse_home으로 표기하겠다. 이 기사는 이클립스 3.3 버전을 기준으로 설명한다.
  2. XStream 웹 사이트에서 XStream의 최신 안정 버전을 다운로드해 원하는 폴더에 설치하자. 해당 디렉터리를 이 기사에서는 xstream_home으로 표기하겠다. 이 기사는 XStream 1.2.2 버전을 기준으로 설명한다.
  3. 3 썬 웹 사이트에서 자바 플랫폼 스탠다드 에디션(Java Platform, Standard Edition: J2SE) 소프트웨어 개발 킷(software development kit: SDK)을 다운로드해 원하는 폴더에 설치하자. 해당 디렉터리를 이 기사에서는 java_home으로 표기하겠다. 이 기사는 1.5.0_05 버전을 기준으로 설명한다.

자바 프로젝트 만들기

File > New > Project를 선택하고, 다음 절차에 따라 자바 프로젝트를 생성하자.

  1. Java > Java Project > Next를 선택한다(그림 1).

    그림 1. 새 자바 프로젝트 시작하기


  2. 프로젝트 이름을 입력하고 Next를 클릭한다(그림 2).

    그림 2. 프로젝트 이름 채우기


  3. Finish를 클릭하여 작업을 마무리한다(그림 3).

    그림 3. 설정 마무리하기


그림 4는 이렇게 해서 만들어진 자바 프로젝트를 보여준다.


그림 4. 새로 만들어진 자바 프로젝트

XStream 지원 추가하기

아래 절차에 따라 방금 만든 프로젝트에 XStream 라이브러리를 추가하자.

  1. 이클립스의 Project Explorer에서 해당 프로젝트를 선택한 다음, Project 메뉴에서 Properties를 선택한다(그림 5).

    그림 5. XStream 라이브러리 추가하기


  2. Add External JARs를 클릭하고, xstream_home/lib 폴더에서 xstream-1.2.2.jar를 선택한다.
  3. OK를 클릭해 작업을 마무리한다(그림 6).

    그림 6. XStream 지원 마무리하기


그림 7은 XStream 지원이 추가된 프로젝트를 보여준다.


그림 7. 추가된 라이브러리들




위로


객체 직렬화하기

다음의 간단한 예제는 WriterReader 클래스를 통해 XStream을 사용해 객체를 직렬화하고/역직렬화하는 방법을 보여준다. Writer 클래스는 Employee 타입의 객체를 XML로 직렬화해 파일로 저장하기 위해 XStream API를 사용한다(Listing 1).


Listing 1. Writer.java
                
package com.samples;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import com.thoughtworks.xstream.*;

public class Writer {

    public static void main(String[] args) {
        Employee e = new Employee();

		//setter 메서드를 사용해 속성을 설정한다.
        //주의: 이 작업은 생성자에서도 할 수 있지만
        //XStream이 생성자 없이도 직렬화를 수행할 수 있음을 보여주기 위해
        //일부러 이렇게 했다.
		
        e.setName("Jack");
        e.setDesignation("Manager");
        e.setDepartment("Finance");

        //객체를 직렬화한다.
        XStream xs = new XStream();

        //파일을 파일시스템에 기록한다.
        try {
            FileOutputStream fs = new FileOutputStream("c:/temp/employeedata.txt");
            xs.toXML(e, fs);
        } catch (FileNotFoundException e1) {
            e1.printStackTrace();
        }
    }
}

Reader 클래스는 이 파일을 읽어 XML을 역직렬화하고, 데이터로부터 자바 객체를 만들어 낸다(Listing 2).


Listing 2. Reader.java
                
package com.samples;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import com.thoughtworks.xstream.*;
import com.thoughtworks.xstream.io.xml.DomDriver;

public class Reader {

    public static void main(String[] args) {
        XStream xs = new XStream(new DomDriver());
        Employee e = new Employee();

        try {
            FileInputStream fis = new FileInputStream("c:/temp/employeedata.txt");
            xs.fromXML(fis, e);

            //방금 읽어들인 객체의 데이터를 출력한다
            System.out.println(e.toString());

        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        }

    }
}

Listing 3Employee 객체의 구조를 보여준다.


Listing 3. Employee.java
                

package com.samples;

public class Employee {
    private String name;
    private String designation;
    private String department;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDesignation() {
        return designation;
    }
    public void setDesignation(String designation) {
        this.designation = designation;
    }
    public String getDepartment() {
        return department;
    }
    public void setDepartment(String department) {
        this.department = department;
    }
    @Override
    public String toString() {
        return "Name : "+this.name+
        "\nDesignation : "+this.designation+
        "\nDepartment : "+this.department;
    }
}




위로


설정 파일 읽기

복잡한 XML 파일 읽기
이 설정 예제는 단순한 XML 파일을 읽는 예를 보여준다. 복잡한 XML 파일을 읽으려면 커스텀 변환기(converter)가 필요할 것이다. 변환기 작성 튜토리얼을 XStream "변환기 튜토리얼" 사이트에서 볼 수 있다(참고자료).

대 부분의 응용 프로그램은 다수의 설정 값(properties)에 의존한다. 이 설정 값은 연결할 데이터 소스의 이름일 수도 있고, 로그 파일의 위치일 수도 있다. 설정 파일은 응용 프로그램을 다시 컴파일하지 않고도 설정 값을 변경할 수 있도록 해주고, 유지보수도 쉬워지므로 이러한 데이터를 저장하기에 가장 적합한 곳이다. 이 예제 시나리오는 XStream을 사용하여 응용 프로그램에서 XML 설정 파일로부터 설정 값을 읽는 방법을 보여준다.

전형적인 XML 데이터 바인딩은 XML 파일로부터 자바 객체를 생성(generate)해야 하지만, XStream을 사용하면 객체 생성 단계가 필요없다. 자바 클래스를 만들고 클래스의 필드를 읽고 싶은 XML의 요소에 매핑하면 된다. 이 예제는 Listing 4와 같은 설정 파일이 있다고 가정한다.


Listing 4. Config.xml
                
<?xml version="1.0" encoding="UTF-8"?>
<config>
    <datasource-name>IRIS</datasource-name>
    <ipaddress>9.124.74.85</ipaddress>
    <logfilename>DailyLogApplication.log</logfilename>
    <appender>console</appender>
</config>

Listing 5는 설정 파일을 읽어 설정 값을 자바 객체로 불러오는 ConfigReader 클래스의 소스를 보여준다.


Listing 5. ConfigReader.java
                
package com.samples;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import com.thoughtworks.xstream.*;
import com.thoughtworks.xstream.io.xml.DomDriver;

public class ConfigReader {

    String datasourcename = null;
    String ipaddress = null;
    String logfilename = null;
    String appender = null;

    @Override
    public String toString() {
        //이 메서드는 멤버 변수의 값을 출력한다.
        return "Datasource Name : "+datasourcename+
            " \nIP Address : "+ipaddress+
            " \nLogfilename : "+logfilename+
            " \nAppender : "+appender;
    }

    /**
     * @param args
     * @throws FileNotFoundException 
     */
    public static void main(String[] args) throws FileNotFoundException {
        XStream xs = new XStream(new DomDriver());

        FileInputStream fis = new FileInputStream("c:/temp/Config.xml");
        xs.aliasField("datasource-name", ConfigReader.class, "datasourcename");
        xs.alias("config", ConfigReader.class);
        ConfigReader r = (ConfigReader)xs.fromXML(fis);

        System.out.println(r.toString());
    }
}




위로


요약

몇 가지 간단한 절차만 거치면 XStream을 설치하고 응용 프로그램을 개발할 수 있다. 이 기사를 통해 XStream을 사용하여 자바 객체를 직렬화하고, 역직렬화하고, 설정 파일을 읽는 방법을 배웠으니 XStream 사이트에서 앨리어스(alias), 어노테이션(annotation), 변환기(converter)에 대해 더 자세히 배울 수 있다(참고자료). 앨리어스와 변환기를 사용하면 생성되는 XML을 완전히 제어할 수 있다.

저작자 표시 비영리 변경 금지
Trackback 0 Comment 0

자바 직렬화란 무엇인가?

자바에서는 자바에서 제공하는 기본적인 데이터 유형 이외에도 여러 객체들을 스트림에 쓰고, 읽을 수 있는 기능을 제공하는데 이것이 바로 객체 직렬화를 통해서 가능하다.
이러한 객체 직렬화 기법은 원격 메소드 호출, 보안 및 암호화 등 실제 프로그래밍 시에 유용하게 사용되어 질 수 있다.

 객체 직렬화의 비밀

그러면 먼저 객체 직렬화가 어떠한 과정을 거쳐서 이루어 지는지 비밀을 벗겨보도록 하자.
먼저 객체들은 ObjectOutputStream에 의해서 직렬화가 되며, ObjectInputStream에 의해서 직렬화가 해제된다는 사실을 기억하도록 하자. 이러한 일련의 과정을 그림으로 나타내면 다음과 같다.

[그림 객체 직렬화]

여기에 사용되는 ObjectOutputStream과 ObjectInputStream은 모두 java.io 패키지의 일부분이고, 각각 DataOutput과 DataInput클래스를 implement한 ObjectOutput과 ObjectInput을 extends 해서 만들어 진것이다. 그리고 이 두 클래스에는 비원시 객체와 배열등을 스트림에 읽고 쓰기 위한 메쏘드들이 추가되어져 있다.

[그림 클래스 구조도]

객체 직렬화의 과정

지금부터는 보다 자세히 객체의 직렬화 과정에 대해 알아보도록 하자.

객체는 ObjectOutputStream의 writeObject() 메쏘드에 자신을 넘김으로써 직렬화 된다.
WirteObject()메쏘드는 private 필드와 super 클래스로부터 상속받은 필드를 포함, 객체의 모든 것을 기록하게된다.
직렬화 해제는 직렬화와 반대의 과정을 거치게 되는데 ObjectInputStream의 readObject()메쏘드를 호출함으로써 스트림으로부터 읽어 들이고 이를 직렬화가 되기전의 객체로 다시 만들게 된다.

다음은 객체가 직렬화 되고 해제되어 원래의 객체로 복원되는 과정에 대한 간단한 예제이다.

눈여겨 볼 부분은 ObjectInputStream을 생성해서 writeObject()를 사용해서 객체를 직렬화 하는것과 ObjectInputStream을 생성해서 readObject()를 통해서 객체를 복원하는 부분이다. 또한 SerializableClass가 Serializable을 implements한 것을 주의해서 보길 바란다.

 

import java.io.*;

public class ObjectSerializeTest {

    public static void main(String[] args) throws Exception {
/* 파일을 열어서 그곳에 객체를 직렬화 시켜서 저장한다. */
// 파일 출력 스트림을 연다.

    FileOutputStream fos = new FileOutputStream("_imsi.txt");

// 객체 스트림을 열고, 객체스트림을 통해 객체를 파일에 저장
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    oos.writeObject(new SerializableClass("Serialize Test Program", 1004));

// 스트림을 닫는다.
    oos.close();

/* 직렬화 된 객체가 저장된 파일로 부터 객체를 해제시켜 원래의 객체를 복원*/
// 파일 입력 스트림으로부터 객체 입력 스트림을 연다.

    FileInputStream fis = new FileInputStream("_imsi.txt");
    ObjectInputStream ois = new ObjectInputStream(fis);

// 객체 입력 스트림으로부터 객체를 읽어온다.
    SerializableClass sc = (SerializableClass)ois.readObject();

// 스트림을 닫는다.
    ois.close();

/* 스트림으로부터 읽어들인 객체의 내용을 출력 원래 생성되었던 객체와 같은 값을 갖는다는 것을 알수가 있다. */
    System.out.println("String : " + sc.mString);
    System.out.println("Integer : " + sc.mInt);
   }
}

/* 하나의 문자열과 정수를 저장하고있는 클래스
Serializable을 implements 함으로써 스트림을 통해 직렬화되고 해제되어질수 있다. */

class SerializableClass implements Serializable {

    public String mString;
    public int mInt;

// 생성자
    public SerializableClass(String s, int i) {
        this.mString = s;
        this.mInt = I;
    }
}

  • 실횅방법

    javac.exe ObjectSerializeTest.java
    java.exe ObjectSerializeTest

  • 실행결과

String : Serialize Test Program
Integer : 1004

 

 

커스텀 직렬화

  • Serializable

스트림을 통해서 직렬화 또는 해제 될수 있는 객체는 Serialiazable을 implements한 객체만이 가능하다.자바 2에서는 java.awt, javax.swing, etc등의 클래스들이 Serializable을 implements하고 있으며, 이러한 상위 클래스들이 implements하므로 하위 클래스들 또한 같이 직렬화/해제의 범주에 들 수가 있다.

다음은 Serializable을 implements하고있어서 객체의 직렬화가 가능한 객체들의 리스트이다.

[직렬화 가능 객체 리스트]

 

패키지

Serializable

java.awt

BorderLayout, CardLayout, CheckboxGroup, Color, Component, ComponentOrientation, Cursor, Dimension, Event, FlowLayout, FocusManager, Font, FontMetrics, GraphicsConfigTemplate, GridBagConstraints, GridBagLayout, GridBagLayoutInfo, GridLayout, ImageMediaEntry, Insets, LightweightDispatcher, MediaTracker, MenuComponent, MenuShortcut, Point, Polygon, Rectangle, ScrollPaneAdjustable, SystemColor

java.awt.dnd

DropTarget

java.awt.font

TransformAttribute

java.awt.geom

AffineTransform

java.awt.image.renderable

ParameterBlock

java.beans

PropertyChangesSupport, VetoableChangeSupport, BeanContext, BeanContextChildSupport, BeanContextSupport

java.io

ExternalizableFile, FilePermission, FilePermissionCollection, ObjectStreamClass

java.net

InetAddress, SocketPermission, SocketPermissionCollection, URL

java.rmi

MarshalledObject

java.rmi.activation

ActivationDesc, ActivationGroupDesc, ActivationGroupID, ActivationID

java.rmi.dgc

Lease, VMID

java.rmi.server

ObjID, RemoteObject, UID

java.security

AllPermissionCollection, BasicPermission, BasicPermissionCollection, CodeSource, GuardedObject, Identity, Key, KeyPair, Permission, PermissionCollection, Permissions, PermissionsHash, SecureRandomSpi, SignedObject, UnresolvedPermission, UnresolvedPermissionCollection

java.text

BreakIterator, Collector, DateFormatSymbols, DecimalFormatSymbols, Format, SpecialMapping, TextBoundaryData, UnicodeClassMapping, WordBreakTable

java.util

ArrayList, BitSet, Calendar, Date, EventObject, HashMap, HashSet, Hashtable, LinkedList, Locale, PropertyPermissionCollection, Random, TimeZone, TreeMap, TreeSet, Vector

javas.swing.table

AbstractTableModel, DefaultTableCellRenderer, DefaultTableColumnModel, DefaultTableModel, TableColumn

javax.swing.text

AbstractDocument, EditorKit, GapContext, SimpleAttributeSet, StringContent, StyleContext, TabSet, TabStop

javax.swing.tree

DefaultMutableTreeNode, DefaultTreeModel, DefaultTreeSelectionModel, TreePath

 

Serializable은 Cloneable과 같은 마커 인터페이스로서, 어떠한 메쏘드들을 정의해놓은 것이 아닌, serizlVersionUDI라는 변수 하나만을 가지며, 이 객체가 직렬화가 가능하다는 것을 알려주는 역할만을 하는 인터페이스일 뿐이다.

  • transient

스트림을 이용해서 직렬화 하는데 있어서, 커다란 프로그램 전체가 직렬화된다면, 여러가지 면에서 많은 낭비일 것이다. 예를 들어, 한 객체가 마우스가 눌려진 위치를 알기 위해서 마우스 클릭시에 위치를 저장하는 필드를 가지고 있다고 가정하자, 이 경우 마우스의 위치값은 프로그램이 돌아가는 상태에서 마우스가 눌려졌을 당시에만 유효한 값으로, 객체가 직렬화 되었다가 해제 되었을 경우에는 쓸모없는 값이 되어버린다.

이런 객체 직렬화에 쓸모없는 값들은 transient로 선언해 줌으로써 객체 직렬화에서 제외되어질수 있다.

Private transient int x;

이러한 선언은 플랫폼에 따라 다른 값을 가지는 필드나, 현재의 환경에서만 유효한 필드등을 객체 직렬화에서 제외하는데 유용하게 쓰일 수가 있다.

transient선언이 적용된 예이다.

 

import java.io.*;

public class ObjectSerializeTest1 extends TestClass implements Serializable {

// 변수 선언, x는 객체직렬화에서 제외되도록 transient로 선언
    private int i;
    private transient int x;

// 생성자 Serializable을 implements한 i와 x는 받아온 인자를 그대로 대입하고, TestClass를 extends한 j는
// i값에 5배를 한 값을 대입했다.

    public ObjectSerializeTest1(int i, int x) {
        this.i = i;
        this.x = x;
        j = i*5;
    }

/* 객체직렬화를 하기전의 객체의 값들을 알아본뒤에, 해당 객체를 스트림에 직렬화 시킨다.*/
    private void writeObject(ObjectOutputStream out) throws IOException {
        System.out.println("writeObject");
        System.out.println("write ==> i = "+this.i+", j = "+j+", x = "+this.x);
        out.defaultWriteObject();
// 객체를 직렬화 한다.
        out.writeInt(j);
// 임시파일에 기록
    }

/* 스트림으로 부터 객체를 해제시킨다. */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        System.out.println("readObject");
        in.defaultReadObject();
// 객체 직렬화 해제 작업
        j=in.readInt();
// 임시파일에서 읽어옴
    }

/* 객체의 값을 알기 위해서 오버라이드 */
    public String toString() {
        return "i = "+i+", j = "+j+", x = "+x;
    }

    public static void main(String[] args) throws Exception {
// 객체직렬화에 사용될 화일을 열고 스트림을 얻는다.

        FileOutputStream fos = new FileOutputStream("temp.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);

// 객체를 생성시킨뒤, 스트림에 객체를 쓴다.(10은 transient로 선언된 필드의 값임을 유의)
        oos.writeObject(new ObjectSerializeTest1(1, 10));
        oos.close();

// 정보를 저장한 화일을 열고 스트림을 통해 읽은뒤 객체직렬화를 해제시킨다.
        FileInputStream fis = new FileInputStream("temp.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        ObjectSerializeTest1 ser = (ObjectSerializeTest1)ois.readObject();
        ois.close();

// print문 수행시 형 변환이 이루어져서 이미 오버라이드된 toString()메쏘드가 수행된다.
        System.out.println("read ==> "+ser);
    }
}

/* Serialize되지 않은 클래스 객체 직렬화 작업으로부터는 제외, 만약 사용자가 이 클래스를 객체 직렬화에 사용하고 싶다면 Serializable을 implements하면 된다. 만약 그렇게 할수 없을 경우에는 위와 같이 하도록 한다. */
class TestClass {
    int j;
}

  • 실횅방법

    javac.exe ObjectSerializeTest1.java
    java.exe ObjectSerializeTest1

  • 실행결과

writeObject
write ==> i = 1, j = 5, x = 10
readObject
read ==> i = 1, j = 5, x = 0

 

  • externalizable

객체직렬화의 또 다른 방법으로는 Externalizable 인터페이스를 들수 있는데, 이 방법은 Serializable보다 더 강력하게 객체 직렬화를 제어하고 싶을 경우에 사용되어진다. 어떤 클래스가 이 인터페이스를 구현하게 되면 readExternal()과 writeExternal()메쏘드를 사용하여 객체를 스트림으로부터 읽고 쓸수있다. Externalizable객체들은 자신의 상태를 직렬화 및 해제하는 과정에서 강력한 제어를 할수 있으며, Serializable 객체에서의 기본 데이터 포멧을 사용하고싶지 않을때에 사용한다.

다음은 externalizable을 사용한 객체 직렬화의 예이다.

 

import java.io.*;

public class ObjectExternalizeTest
   
 implements Externalizable {

    int i;
    String s;

/* 인자없는 생성자(직렬화된 데이터를 읽어들여서 객체를 만들기 위해 필요) */
    public ObjectExternalizeTest() { }

/* 생성자 */
    public ObjectExternalizeTest(String s, int i) {
        this.s = s;
        this.i = i;
    }

/* readObject()메쏘드로부터 호출이 되며, 직렬화된 객체를 해제시킨다. */
    public void
readExternal(ObjectInput oi) throws IOException, ClassNotFoundException {
        System.out.println("Execute readExternal()");
        s = (String)oi.readObject();
        i = oi.readInt();
    }

/* writeObject()메쏘드로부터 호출이 되며, 변수들을 직렬화한다.*/
    public void
writeExternal(ObjectOutput oo) throws IOException {
        System.out.println("Execute writeExternal()");
        oo.writeObject(s);
        oo.writeInt(i);
    }

/* 변수들의 값을 보여준다.*/
    public String toString() {
        return Integer.toString(i)+", "+s;
    }

    public static void main(String[] args) throws Exception {
        ObjectExternalizeTest oet1 = new ObjectExternalizeTest("Externalizable Test", 1000);

// 객체를 직렬화시켜 저장할 화일로부터 쓰기 스트림을 얻어온다.

        FileOutputStream out = new FileOutputStream("temp.txt");
        ObjectOutputStream oos = new ObjectOutputStream(out);

/* 객체를 직렬화 시킨다. 이경우 writeObject는 oet1의 writeExternal을호출하게되고writeExternal() 메쏘드의 코드들이 확장되어 실행이 된다. */
       oos.writeObject(oet1);

// 객체가 직렬화되어 저장된 파일로부터 스트림을 얻어온다.
        FileInputStream in = new FileInputStream("temp.txt");
        ObjectInputStream ois = new ObjectInputStream(in);
        ObjectExternalizeTest oet2 = (ObjectExternalizeTest)ois.readObject();

// 객체 직렬화 이전의 객체와 직렬화를 거친 뒤 다시 해제된 객체의 값을 출력한다.
        System.out.println(oet1);
        System.out.println(oet2);
// 만약 모든 과정이 제대로 이루어졌다면 oet1과 oet2객체는 같은 값을 가지고있을 것이다.
    }
}

  • 실횅방법

    javac.exe ObjectExternalizeTest.java
    java.exe ObjectExternalizeTest

  • 실행결과

Execute writeExternal()
Execute readExternal()

1000, Externalizable Test
1000, Externalizable Test

 

 

애플릿 직렬화

객체직렬화는 애플릿을 직렬화하는데에도 사용이 되어진다. 이러한 경우 <Applet>태그는 클래스 파일 대신에 직렬화 된 객체 파일을 지정하게 되고, 브라우저는 이러한 태그를 만나면 직렬화된 애플릿을 해제하여 화면에 보여주게 된다. 즉, 그래픽 유저 인터페이스를 생성하기 위한 코드를 전혀 가지고 있지 않으면서도 완벽한 그래픽 유저 인터페이스를 가지게 되는것이다.

일반적으로 직렬화된 애플릿은 *.ser의 확장자를 가진다.

직렬화된 애플릿을 호출하는 html 파일은 다음과 같은 내용을 가진다.

<Applet Object="SerializableApplet.ser" Width=300 Height=300></Applet>

소켓을 통한 객체 직렬화

소켓을 생성한 뒤 스트림을 얻어서 ObjectInputStream과 ObjectOutputStream을 얻어서 사용한다면, 파일이 아닌 네트웍 너머의 다른 컴퓨터와도 객체를 직접 주고 받을수 있다.

소켓을 통해 직렬화 되어 쓰고 읽혀질 객체는 역시 Serializable을 구현하고 있어야 하며, 사용법은 파일에 쓰고 읽는것과 동일하다.

다음은 서버에서 이미지를 포함하고 있는 Serializable을 구현한 객체를 생성하여 소켓을 이용하여 클라이언트 애플릿에게 직렬화된 객체를 보내고, 애플릿에서는 이를 해제하여 화면에 나타내는 예제이다.

여기서 주의깊게 살펴보아야 할 점은 이미지를 직렬화하기위한 방법이다.

이미지는 Serializable을 구현하고 있지 않기 때문에 직접적으로 객체 직렬화에 사용되어질 수는 없다. 하지만, 자바 2에서는 배열을 직렬화에 사용할 수가 있고, 이미지는 각각의 픽셀들이 배열에 저장되어질수 있으므로 직접적으로 이미지를 직렬화 할 수는 없지만 배열을 사용하여 간접적으로나마 이미지를 객체 직렬화에 사용할 수가 있다.

  • 서버측 프로그램

 

import java.awt.*;
import java.awt.image.*;
import java.net.*;
import java.io.*;
import java.util.*;

public class ImageSerializeServer {

// 서버는 3434 포트로 임의로 설정
    public static final int DEFAULT_PORT = 3434;
    protected int port;
    protected ServerSocket server;

// ImageSerializeServer를 실행한다.
    public static void main (String args[]) {
        new ImageSerializeServer ();
    }

/* 생성자. 서버의 소켓을 생성하고 클라이언트의 연결을 대기 */
    public ImageSerializeServer() {

// 포트를 지정(3434)
        port = DEFAULT_PORT;
        this.port = port;

// 지정된 포트로 서버의 소켓을 생성한다.
        try {
            server = new ServerSocket (port);
        } catch (IOException e) {
            System.err.println ("Error creating server");
            System.exit (1);
        }

// 클라이언트의 연결을 대기
        connectClient();
    }

/* 클라이언트로 부터의 연결을 기다리면서 무한루프를 돈다. */
    public void connectClient() {
        System.out.println ("Server Running");
        try {

// 클라이언트로 부터의 연결이 요청되면 클라이언트의 소켓을 생성 하고 이미지 픽셀을 클라이언트로 보내는 쓰레드를 생성한다.
            while (true) {
                Socket client = server.accept();
                System.out.println ("Connection from: " + client.getInetAddress().getHostName());
                appletConnection c = new appletConnection (client);
// 쓰레드 생성
            }
        } catch (IOException e) {
            System.err.println ("Exception listening");
            System.exit (1);
        }
        System.exit (0);
    }
}

/* 간단한 이미지를 이루는 픽셀을 만들어 이를 네트웍너머의 클라이언트로 보내는 클래스 */
class appletConnection extends Thread {

// 변수 선언
    private final static int b = Color.blue.getRGB();
    private final static int g = Color.green.getRGB();
    private final static int r = Color.red.getRGB();
    protected ObjectOutputStream oos;

// 간단한 이미지 픽셀
    protected int ipTemp[] = { b, b, b, b, b, b, b, b, b, b,
                                         b, b, b, b, b, b, b, b, b, b,
                                         b, b, g, g, g, g, g, g, b, b,
                                         b, b, g, g, g, g, g, g, b, b,
                                         b, b, g, g, r, r, g, g, b, b,
                                         b, b, g, g, r, r, g, g, b, b,
                                         b, b, g, g, g, g, g, g, b, b,
                                         b, b, g, g, g, g, g, g, b, b,
                                         b, b, b, b, b, b, b, b, b, b,
                                         b, b, b, b, b, b, b, b, b, b };

    public appletConnection (Socket client) {

// 객체 직렬화를 위한 출력 스트림을 생성
        try {
            oos = new ObjectOutputStream(client.getOutputStream());
        } catch (IOException e) {
            try {
                client.close();
            } catch (IOException e2) { }
            System.err.println ("Unable to connect.");
            return;
        }
        start();
    }

    public void run () {

/* Serializable을 구현한 imagePixel객체를 생성해서 Vector에 집어넣는다. 따라서 이미지가 하나가 아닌 여러개 일지라도 객체 직렬화를 통해 전달하는것이 가능하다.*/
        Vector vt = new Vector();
        ImagePixel ip = new imagePixel(ipTemp);
        vt.addElement(ip);

// 이미지 픽셀들을 포함하고 있는 벡터를 객체 직렬화 시켜 스트림에 쓴다.
        try {
         
   oos.writeObject(vt);
        } catch (Exception e) {
            System.out.println("Object write exception : " + e);
        }
    }
}

/* 객체의 직렬화를 위해 Serializable을 구현한 imagePixel 클래스 */
class imagePixel implements Serializable {
    protected int ip[];

// 생성자
    public imagePixel(int[] ip) {
        this.ip = ip;
    }
}

  •  실행방법

    javac.exe ImageSerializeServer.java
    java.exe ImageSerializeServer

    Server Running.....(서버가 대기하고 있는 상태)

 

  • 클라이언트측 프로그램

 

import java.applet.*;
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.net.*;
import java.util.*;

public class ImageSerializeClient extends Applet {

// 기본 포트는 서버와 같이 3434로 하고 host는 localhost로 설정한다.
    private int defaultPort = 3434;
    private String host = "localhost";
    private Socket s;
    private ObjectInputStream oin;

// 변서 선언
    Vector vt;
    Image myImage;

    public void init() {

/* 서버에 접속하여 소켓을 구한뒤에 객체 직렬화를 해제하기 위한 스트림을 얻는다. 이것은 마치 파일로부터 스트림을 얻는 과정과 비슷하다. */
        try{
            s = new Socket(host, defaultPort);
            oin = new ObjectInputStream(s.getInputStream());
        } catch(IOException e) { }
    }

    public void start() {

// 객체를 스트림으로 부터 읽어들여 직렬화를 해제한다.
        try {
         
   vt = (Vector)oin.readObject();
        } catch(Exception e) {}

// 직렬화가 해제된 객체는 Vector에 들어가게 되고 Vector의 요소인 imagePixel을 구한다.
        imagePixel myPixel = (imagePixel)vt.elementAt(0);

// 이렇게 구해진 imagePixel의 이미지 픽셀을 가지고 실제 이미지를 생성
        myImage = createImage(new MemoryImageSource(10, 10, myPixel.ip, 0, 10));
    }

// 생성된 이미지로 그림을 그린다.
    public void paint(Graphics g) {
        g.drawImage(myImage, 0, 0, 100, 100, this);
    }
}

  •   실행방법

    javac.exe ImageSerializeClient.java
    java.exe ImageSerializeClient

     

  • 애플릿 포함 Html 내용

 

<HTML>
<TITLE>객체 직렬화</TITLE>
<BODY>

<APPLET CODE = "ImageSerializeClient.class" WIDTH = 100 HEIGHT = 100 ></APPLET>

</BODY>
</HTML>

 

  • 최종 결과 화면

 프로그램을 실행하기 위해서는 다음의 과정으로 실행하면 된다.

  1. 서버측 프로그램인 ImageSerializeServer.java를 컴파일하고, 서버 쪽에서 실행시면, "Server Running..."이라는 메시지가 나타나면서 서버측 프로그램이 제대로 작동을 하는 것이다.
  2. 다음으로는 클라이언트 애플릿 프로그램인 ImagSerializeClient.java를 컴파일한다.
  3. 브라우져를 실행시킨뒤에 클라이언트 애플릿 클래스를 실행시키기 위한 html파일이 있는 URL주소를 주소 입력란에 넣는다.
  4. 이때 서버프로그램이 돌아가는 곳과 애플릿을 위한 코드 및 html파일이 위치한 곳은 같은 Machine상에 있어야 한다.

참고로 이번 예제에서는 이미지 픽셀을 직접 그 값을 대입해서 이미지를 대신했는데, 실제 이미지를 이미지 픽셀로 바꾸는 기능이 자바에서 지원되고 있으므로 이를 간단히 소개토록 하겠다.

이미지는 여러개의 색깔을 표현하고 있는 픽셀들의 집합으로 볼 수 있고, 보다 정교하게 이미지를 다루는 작업을 하고 싶다면, 이미지의 픽셀들을 얻어서 조작을 해야한다. 그렇다면 어떻게 이미지의 픽셀들을 얻을 수 있을까? 그것은 PixelGrabber를 통해서 가능하다.

PixelGrabber는 이미지와 해당 이미지의 정보들을 기반으로 이미지 픽셀을 구해준다.
다음은 실제로 이러한 것을 보여주는 코드이다.

 

.
.

int imagePixel[];                         // 이미지의 픽셀들을 저장할 배열
int imageWidth, imageHeight;       
// 이미지의 폭과 높이를 가지게 될 변수들
Image imageObject;                   
// 이미지 객체
.
.

/* imageObject로부터 픽셀들을 얻어서 imagePixel배열에 저장한다. */
private void getImagePixel() {

    imagePixel = new int[imageWidth * imageHeight];    // imagePixel을 초기화 한다.
    PixelGrabber grabber = new PixelGrabber(imageObject, 0, 0, imageWidth, imageHeight, imagePixel, 0,                                                           imageWidth);

// grabber를 생성해서 픽셀들을 얻는다.
    try {
        grabber.grabPixels();
    } catch(Exception e) {
        return; }

    if((grabber.status() & ImageObserber.ABORT) != 0)
        return;
}

.
.

 참고 문헌

저작자 표시 비영리 변경 금지
Trackback 0 Comment 0

웹로직 시작시 InvalidClassException 오류가 발생한다면

1. setDomainEnv.cmd 파일에
- Dcom.sun.xml.namespace.QName.useCompatibleSerialVersionUID=1.0 추가

2. ant task 수행 중 다음과 같이 ANT_OPT를 설정한다.
Set ANT_OPT=-Dcom.sun.xml.namespace.QName.useCompatibleSerialVersionUID=1.0

3. 모든 ANT 파일의 java 혹은 javac 작업에서 jvmarg를 아래 샘플처럼 적용하라.
<target name="run">
<java classname="examples.webservices.jws_basic.simple.Client" fork="true"
failonerror="true" >
<!--Note the jvmarg tag -->
<jvmarg line="-Dcom.sun.xml.namespace.QName.useCompatibleSerialVersionUID=1.0" />
<classpath refid="client.class.path"/>
<arg line="http://${wls.hostname}:7001/jws_basic_simple/SimpleService" />
</java>
</target>

저작자 표시 비영리 변경 금지
Trackback 0 Comment 0

mvc 강좌

MVC 첫번째강좌 - 모델1 기법으로 게시판 리스트 만들기


작성자 : 김시웅
테스트환경 : 이클립스2.1.2, mysql, tomcat4.1.30

본강좌는 jsp에 어느정도 익숙하신걸로 가정하고 진행하겠습니다. 일단 환경부터 구축해야겠죠^^.

먼저 DB에 테이블을 하나 만들겠습니다. Board(번호, 이름, 제목, 내용, 등록일) 테이블을 하나 만들고 디폴트로 몇개의 데이타를 넣겠습니다.(Board.sql, insert.sql) 자, 기본적인 준비는 됐습니다.

이제 이클립스에서 자바프로젝트를 하나 만듭니다. 프로젝트명은 MvcBoard 라고 하겠습니다.

프로젝트에서 www 폴더를 만듭니다. 이제 톰캣플러그인이 필요합니다. 설치하지 않으신분은 자료실에서 다운 받으시면 됩니다.

톰캣플러그인이 설치되셨다면 MvcBoard->특성을 누릅니다. 왼편에 Tomcat을 선택하고 Is a Tomcat Project 를 체크합니다. 다음에 Subdirectory to set as web application root(optional) 부분을 /www 로 변경합니다. 변경한 화면은 다음과 같습니다.
 

확인을 누르면 톰캣설정이 완료되었습니다. 이제 www폴더에 test.jsp를 만들고 내용은 test 로 합니다.

톰캣을 실행하고 브라우저에서 http://localhost:8080/test.jsp 를 입력합니다. 아래화면처럼 나오면 성공입니다.
 

지금까지는 db를 구성하고 이클립스에서 프로젝트를 톰캣과 연동하는 과정이었습니다. 앞으로 진행할 웹관련 강좌는 이런형태로
프로젝트를 설정하게됩니다.

자 그럼 모델1방식으로 게시판 리스트를 만들어보도록 하겠습니다. www폴더에 List.jsp 를 만들고 다음의 순서대로 내용을 채우겠습니다.

먼저 import 와 contentType을 설정합니다.
<%@ page language="java" import="java.sql.*,java.io.*" contentType="text/html; charset=euc-kr" %>

그 다음에 db에 접속을 해서 커넥션을 맺습니다. db서버주소, db명, 유저명,암호는 여러분의 db에 맞게 직접 넣으시기 바랍니다.
본강좌에서는 mysql을 사용합니다.
<%
   String url = "jdbc:mysql://db서버주소:3306/db명";
   String username = "유저명";
   String password = "암호";
   Connection con = null;
   try {
      Class.forName("org.gjt.mm.mysql.Driver");
      con = DriverManager.getConnection(url, username, password);
   } catch (ClassNotFoundException e) {
      out.println("드라이버를 찾을수 없습니다.");
   } catch (SQLException ex) {
      out.println("접속실패");
   }
%>

커넥션을 얻었다면 ResultSet을 얻습니다. 테이블은 위에서 만든 Board 입니다. Board.sql을 사용하여 테이블을 만들고 insert.sql 을 사용하여 데이타를 먼저 입력해놓겠습니다.(위의 첨부파일을 참조하세요)
<%   
   PreparedStatement ps = null;
   ResultSet rs = null;      
   String query = "select * from Board";
   try {
      ps = con.prepareStatement(query);
      rs = ps.executeQuery();
%>

이제 ResultSet을 사용하여 화면에 리스트를 구성하도록 하겠습니다.
<html>
   <body>
      <table border="1">
         <tr>
            <td>번호</td>
            <td>제목</td>
            <td>이름</td>
            <td>등록일</td>
         </tr>
<%
      while (rs.next()) {            
         String num = rs.getString("num");
         String subject = rs.getString("subject");
         String name = rs.getString("name");
         String regiDate = rs.getString("regiDate");
%>         
         <tr>
            <td><%=num%></td>
            <td><%=subject%></td>
            <td><%=name%></td>
            <td><%=regiDate%></td>
         </tr>
<%
      } 
%>
      </table>
   </body>
</html>

자, 이제 남은건 커넥션을 닫기만 하면 됩니다.
<%
   } catch (SQLException e) {
      out.println(e);
   } finally {
      try {
         if(rs != null) rs.close();         
         if(ps != null) ps.close();         
         if(con != null) con.close();               
      } catch(SQLException e) {
         out.println(e);      
      }
   }
%>

이제 브라우저에서 http://localhost:8080/List.jsp 를 호출하면 다음화면처럼 나올겁니다.
 

다음 강좌에서는 MVC기법으로 소스를 변경해보도록 하겠습니다.(물론 화면에 나타나는 결과물은 동일할겁니다)

 

저작자 표시 비영리 변경 금지
Trackback 0 Comment 0

멀티 채팅 기본 소스및 원리 자바


네, 안녕하십니까 김상욱입니다. 이 글의 목적은 제가 회사와서 처음 제작한 자바프로그램인 멀티채팅애플릿에 쓰여진 각종 기술들을 공개함으로써 제가 불과 몇개월전까지 걷고 있던 길을 걷고 계시는 초보분들에게 조금이라도 보탬이 되고자 하는 데에 있습니다. 아무쪼록 도움이 되시길 바랍니다. 물론 제가 쓴 방법 말고도 수만가지 방법이 있으니(^^) 생각에 여지를 두고 이 글을 읽어주시길 바랍니다. 제가 정확하게 아는 부분이 아니면 언급하지 않는 것을 전제로 하겠습니다.

이 글은 이렇게 구성되어 있습니다.

1. 서론 (잡담)

2. Socket의 개념과 채팅에서의 사용법

3. Socket부분 소스

4. Thread 이용법

5. Thread부분 소스

6. 프로토콜 정의와 StringTokenizer클래스의 이용법

7. Canvas를 이용한 Image다루기

...

 

1. 서론

웹프로그래밍에서 게시판 짜는 것이 기본이듯이(여기서 기본이라는 것은 이모저모 다 알아야만 할 수 있는 프로그램이다라는 뜻입니다.) 네트웍프로그래밍에서 채팅을 짜는 것은 기본입니다. 자바가 처음에 인기를 끈 이유가 인터넷의 보급과 브라우저에서 볼 수 있는 프로그램(애플릿-작은 애플리케이션)이었기 때문에 네트웍 프로그래밍으로 자바를 시작하신다면 현명한 선택을 하신 것이 됩니다. 눈에 보여지는 것이 많기 때문에 배우는 입장에서도 무척 흥미로운 내용입니다. 제법 잘 짜여진 채팅애플릿을 만들기 위해서 어떤 것을 알아야 할까요?

이를 알기 위해서는 채팅이 어떤 구조로 되어있는 지 대략적으로 알아야 합니다. 가장 간단한 채팅은 대부분의 네트웍프로그램이 가지는 서버/클라이언트의 양단을 애플리케이션과 애플릿이 차지하는 것입니다. 클라이언트(손님)의 요청을 서버가 받아서 처리한 후 클라이언트(들)에게 전송해주는 것이 주요 골자죠. 따라서 클라이언트가 전송할 애플릿 프로그램과 서버에 해당하는 애플리케이션 두 가지를 제작해야 하겠습니다. 네트웍 상에서 서버/클라이언트 요청을 처리하려면 아무래도 서버를 돌리기 위해 웹서버 역할을 하는 컴퓨터가 따로 있어야 합니다.(자신의 컴퓨터-로컬컴퓨터 를 쓰셔도 되구요.) 웹서버 역할을 할 수 있다는 것은 NT거나, 2000 server에 IIS(ms에서 제공하는 서버), 리눅스/유닉스에 아파치 등을 깔았다거나, 아니면 우리가 보통 쓰는 윈도(95, 98, Me 등)에 퍼스널웹서버를 설치해야한다는 것이죠. 에구 뭐가 이리 어려워? /^_^/

어쨌든 서버역할을 하는 컴이 있어서 거기에다 서버용 프로그램을 설치하고 돌려야겠구나 하고 감 잡으시면 되겠습니다.

채팅애플릿을 만드면서 공부하게 될 것은 우선 가장 중요한 통신도구 소켓(socket)통신에 대한 것입니다. 소켓에는 서버가 쓰는 서버소켓과 일반소켓이 있는데 그리 어렵지 않습니다. 그 소켓을 통해 주고받는 메시지들을 처리하기 위해서 몇가지 클래스들을 보너스로 배우겠습니다.(StringTokenizer 등)

어, 언제 손님이 접속하고, 메세지가 올 지 모르니 무한 루프를 도는 쓰레드(Thread)에 대해서도 알아야겠네요.

대충 초기설정이 끝나면 이제 액션을 받을 수 있어야겠죠. 액션이벤트 마우스이벤트 등에 대해 배워봅니다.

이것만 알면 가장 간단한 채팅은 구현할 수 있겠습니다만, 디자인 이쁘고, 기능 멋있는 건 무리겠죠... 다양한 방을 관리할 수 있도록 해주는 몇 가지(Vector, HashTable 등)유틸 클래스와 하는 김에 내부클래스의 개념에 대해서 알아보고요, 내친 김에 마우스 이벤트를 받을 수 있는 이미지영역 Canvas 클래스에 대해 알아보겠습니다.

여기에 친구와 음악을 같이 듣는다거나 친구에게 이미지를 보내준다거나 하면 재미있겠죠? 구현해봅니다 (^^)

서론 끝! 이 글에서는 여러분이 궁금해하시는 부분부분에 대해 각개격파식으로 통달해나가도록 하겠습니다. 부디 도움이 되시길....

참, 이 글을 읽을 때에는 항상 API의 index문서를 준비해두세요... 같이 찾아봅니당../^_^/

 

2. Socket의 개념과 채팅에서의 사용법

소켓(Socket)에 앞서 스트림(Stream)에 대해 알아야 합니다. 스트림의 개념은 간단합니다. 한 프로그램에서 다른 프로그램으로 어떤 값들을 넘겨주어야 할 때 스트림을 사용합니다. 스트림(흐름)은 그야말로 데이타들의 흐름이죠. 자바에서만 지원하는 것이 아니기 때문에 양단이 반드시 자바프로그램이어야할 필요는 없습니다.

Socket은 TCP을 사용해 통신을 하는데 쓰입니다. Socket을 이용해 통신하기 위해서는 상대 프로그램이 깔린 컴퓨터의 ip와 사용할 port를 정해야 합니다. port는 1024이상 6만5천번까지 사용할 수 있습니다. 통신이란 서버/클라이언트 구조로 되어있으므로 통신을 하기 위해서는 먼저 서버에서 소켓을 준비해야겠지요. 이를 서버소켓이라고 합니다. 생성은 간단합니다. 서.버.소.켓 이라고 쓰면 되지요.

ServerSocket 서버소켓객체이름 = new ServerSocket(사용할 포트);

됐습니다. 이렇게 서버에서 사용할 포트로 서버소켓을 열어놓으면 이제 통신할 준비가 된 것입니다. 이 서버소켓은 클라이언트의 소켓요청이 들어오면 새로이 소켓을 하나 생성하고 이를 클라이언트와 접속시켜주는 역할을 합니다. 요청을 기다려 소켓을 생성하는 코드입니다.

Socket 소켓객체이름 = 서버소켓객체이름.accept();

accept()함수는 block함수로 요청이 들어올 때까지 진행을 멈추고 기다립니다. 요청이 들어오면? Socket을 소켓객체이름으로 생성하죠. 여기에 스크림을 연결하려면 소켓에서 스트림을 얻기만 하면 됩니다.

InputStream 스트림객체이름 = 소켓객체이름.getInputStream();

스트림의 종류는 InputStream말고도 셀 수 없이 다양합니다. ^^; 기본적인 입출력스트림과 파일 입출력 스트림 외에 연결, 메모리, 필터 입출력 스트림 등이 그것입니다. 각자 유용한 메소드들로 무장되어 있습니다.

클라이언트에서 이 컴퓨터와 통신하고 싶으면 포트 외에 한 가지 더 ip를 알아야겠지요? 소켓을 생성합니다.

Socket 객체이름 = new Socket(상대 ip, 포트);

마찬가지로 여기에도 스트림을 연결하면 됩니다. 끝! 참 쉽게 말하죠? ^^; 살은 하나도 붙이지 않고 두리뭉실하게 뼈만 만들었습니다. /^_^/

일방통신(클라이언트의 요청을 서버가 받아 처리해서 다시 클라이언트에게 응답)소켓프로그램으로 에.. 통신계산기를 가정해볼까요? 두리뭉실~ 계산기 서버는 서버소켓을 생성하고 클라이언트를 기다립니다. 클라이언트는 소켓을 생성하고 여기에 스트림을 붙인 다음 서버에 ip와 port로 접속합니다. 서버에서 서버소켓이 클라이언트의 소켓과 통신할 소켓을 생성해주겠지요? 클라이언트가 소켓으로부터 얻은 스트림에 계산식을 쓰면 계산식이 스트림을 통해 소켓을 타고 서버로 전달되어 서버가 계산하도록 하고 서버는 이를 계산해 클라이언트에게 응답합니다.

끝! 너무 심심하죠? 계산 한 번 하고 끝내니까 그렇죠. 음 그러면 서버에서 계산식을 받는(read) 부분을 스레드로 구성해 무한루프 돌리면 되겠군요. 그러면 계산식이 오면 스레드가 받아서 열심히 계산하고 있고, 그 중간에라도 클라이언트로부터 또 요청이 오면 새로운 스레드가 받아서 열심히 받아서 보내(write)해주면 되겠네요.

끝! 에 또 섭섭한가요? 에써 서버를 구축했는데 클라이언트를 하나만 받자니 좀 그렇네요. 클라이언트의 요청을 받아 소켓을 생성하는 부분 즉,

Socket 소켓객체이름 = 서버소켓객체이름.accept();

이 부분도 무한루프에 스레드 처리해서 클라이언트가 접속하는 대로 새로 소켓을 생성하면 되겠군요. 스레드 처리했으니까 이전의 클라이언트의 요청과 상관없이 새로운 클라이언트도 계산을 할 수 잇겠습니다. 진짜 끝! 지금까지 다 채팅을 위해서 필요한 내용이었습니다. 스레드(멀티스레드)는 자바의 주요한 특징중의 하나입니다. c로 하면 무쟈게 늘어질 코딩을 단숨에 줄여주죠. 그만큼 부하도 많이 걸립니다만...

자, 지금까지 모르는 내용 있으셨나요? 걱정마세요. 100% 200% 뒤에서 다 설명할 겁니다. 일단 읽어둔다 생각하고 머리속에 넣어두세요. /^_^/

 

채팅에서는 이 소켓을 어떤 구조로 이용해야 할까요?

눈 크게 뜨세요. 뼈만 세우는 것이어도 조금 복잡합니다.

채팅은 클라이언트와 서버가 하는 것이 아닙니다. 클라이언트끼리 하는 것이죠. 2명이 채팅을 한다고 합시다. 둘 다 클라이언트입니다. 먼저 사람이 서버에 통신계산기처럼 소켓접속을 해서 기다리고 있고, 두 번째 사람도 소켓접속을 합니다. 한 사람이 말을 합니다. 이 글씨가 두 사람 모두에게 보여야겠지요? 그럴려면 그 사람의 말이 자신의 화면에 그냥 보여져서는 안됩니다. 우선 서버에 자신이 말을 한 것을 알려야 되고요, 서버는 이를 받아서 처리(할 게 있나?) 자신에게 접속한 클라이언트 모두에게 뿌려줍니다. 말을 친 사람도 그제서야 자신의 말이 보입니다. 물론 상대도 보입니다. 끝!

어렵지 않지요? 그럼 여기에 스레드는 어디어디에 들어가야 할까요? 우선 계산기에서 말했듯이 클라이언트가 언제든 접속할 수 있도록 서버소켓에 소켓을 열어주는 부분에 걸어서 각 클라이언트(소켓)이 서로 상관없이 동작할 수 있도록 해주어야겠지요. 그리고? 네 열려진 소켓에 언제 메세지가 갈 지 모르니 메세지를 받아 읽는 부분에다 스레드를 걸어주어야겠군요.

끝! 이렇게 떼어서 이야기를 하니 쉬워보이지만, 아아 스레드가 두부분이나 들어가다니 /^_^/ 깊게 생각할 수록 머리가 복잡해집니다. 위의 이야기를 그림으로 보이겠습니다.

짠~ /^_^/ 네, 요새 s/w단속떠서 포토샵 라이센스를 디자이너만 줘가지구 그림을 못그립니다. 어흑~ 만능엔터테이너가 되려는 나에게 포토샵도 안주다니..

어쩔 수 없이 눈 아프시게 다시 한 번 말로 떠듭니다. 채팅서버에서 서버소켓을 생성합니다. 클라이언트의 연결을 무한루프로 기다립니다. 소켓연결되는 족족 클라이언트 스레드를 생성해주고 이 쓰레드를 서버의 리스트에 추가합니다. 서버가 이들을 관리하기 위해서죠. 주로 Vector클래스를 사용합니다. (앗! 벡터가 나왔군요! ) 클라이언트에서는 우선, 서버와 소켓 연결을 합니다. 스트림도 붙여야 겠지요. 그리고 언제라도 서버로부터 메시지를 받을 수 있도록 스레드를 생성해 구동합니다. 이 스레드가 메시지를 받으면, 화면에 표시해줍니다. 이와 별도로 사용자가 어떤 입력을 하면 메시지를 서버로 전송합니다. 메시지가 서버에 전송되면 서버가 처리해서 자신의 스레드 리스트 모두(혹은 일부)에게 메시지를 보내줍니다. 다른 클라이언트 스레드들은 기다렸다는 듯이 이를 받아 화면에 표시하겠죠. 끝!

아아 눈아프게 해드려서 죄송합니다. 이로써 채팅에서의 소켓구조에 대해 알아보았습니다. 그리 어렵지 않으셨기를~ 앗 다 아시는 내용이라구요~ /^_^/ 후다닥~

3. Socket부분 소스

서버부분:

//자 메인함수에서 포트를 정할 수 있도록 했습니다. 실행을 할 때 java chat 20001 이런식으로 실행합니다.

//만약 리눅스에서 백그라운드로 항상 실행되어 돌아가도록 하려면,

//nohup java chat 20001 & <-이렇게 실행합니다.

public static void main(String arg[])throws IOException
{
//chat이 서버부분의 클래스입니다.

chat server = new chat();

//서버용 소켓이 따로 있다고 말씀드렸죠?
ServerSocket serverSocket = null;

//20001을 받아옵니다.
int port = Integer.parseInt(arg[0]);

boolean listening = true;
try
{

//서버용 소켓을 port로 엽니다. 서버용 소켓이므로 ip는 필요없겠죠?
serverSocket = new ServerSocket(port);
}
catch(IOException e)
{
System.out.println("연결 실패입니다. \n");
System.exit(-1);
}
System.out.println("서버" + serverSocket +","+ port+ "에서 연결을 기다립니다.\n");
//말씀하신 대롭니다^^;

}

클라이언트 부분:

public void connect()
{
try
{

//chaturl:ip와 port로 소켓을 엽니다.
chatSocket = new Socket(applet.chaturl,applet.port);

//각각 스트림을 연결합니다.

input = new DataInputStream(new BufferedInputStream(chatSocket.getInputStream()));
output = new DataOutputStream(new BufferedOutputStream(chatSocket.getOutputStream()));

//입력스트림에 읽을 준비를 합니다. readUTF는 block method로 읽을 것이 올 때까지 기다립니다.

String line=input.readUTF();

//서버에서 소켓이 연결되면 Terminal이란 메세지를 보내기로 했습니다.
if(line.equals("Terminal"))
{
//사용자정보를 보냅니다. |로 연결된 스트링을 보내는데, 나중에 StringTokenizer로 잘라써야겠지요.

output.writeUTF("chogi|"+subtitle+"|"+sex+"|"+age+"|"+nick.trim()+"|"+loca+"|"+email+"|"+cpic+"|"+ccolor);

//쓸 때는 꼭 flush를 해주어야 버퍼에 쌓여있지 않고 바로 전송됩니다.

output.flush();
}
}
catch( UnknownHostException e)
{
System.exit(1);
}
catch(IOException e)
{
System.exit(1);
}
}

 

 

4. Thread 이용법

음 프로세스와 Thread의 차이점은 아시겠지요?

일반적으로 프로그램을 실행시키면, 하나의 프로세스로서 동작하게 됩니다. 다시 말해서, 우리가 실행시키는 하나의 프로그램은 하나의 프로세스로서 나타나게 됩니다. 자바에서의 프로세스는 자바 런타임 환경과 밀접한 관계를 갖고 있습니다. 왜냐하면, 자바 런타임 환경은 프로세스가 실행될 수 있는 기반 환경을 제공해 주기 때문입니다. 프로세스는 다른 프로세스를 생성할 수 있는데, 이 때 생성된 프로세스를 자식 프로세스라하고 기존에 있던 프로세스를 부모 프로세스라 합니다. 이러한 부모/자식 프로세스 개념은 하나의 자바 프로그램에서 다른 프로그램을 실행시키고자 할 때, 주로 사용됩니다. 다시 말해서, 플랫폼 독립적인 자바 프로그램이 플랫폼과 밀접한 관련이 있는 작업을 해야 할 경우, 해당 작업을 수행할 프로그램을 다른 언어로 해당 플랫폼에 맞도록 작성하고, 이 프로그램을 자바 프로그램에서 실행시켜 주는 것입니다. - 박용우님 강좌에서 발췌 -

자바에서 프로세스를 관리해주는 클래스로 Runtime 클래스와 Process클래스가 있습니다. 메모장같은 외부프로그램을 실행하고 관리할 수 있도록 해주지요.

하나의 프로그램을 프로세스라고 볼 때, 스레드는 하나의 프로그램 내에서의 실행 단위라고 할 수 있습니다. 자바에서는 각 작업(타스크)을 스레드로 표현하도록 하고, 이러한 스레드를 여러 개 둘 수 있도록 함으로써 멀티타스킹을 가능하게 해 줍니다. 다시 말해서, 자바에서는 멀티태스킹을 여러 개의 스레드를 동시에 수행하는 멀티스레딩을 이용하여 해결하고 있습니다. 따라서, 자바 가상머신은 하나의 애플리케이션이 동시에 수행되는 여러 개의 스레드를 가질 수 있도록 하고 있습니다. 물론, 일의 우선순위가 존재하듯이 모든 스레드는 그 우선순위를 가지게 됩니다. - 역시 박용우님 강좌에서 발췌 -

멀티쓰레드는 자바에 장점 중에 하나입니다. c에서 쓰레드를 구현하려면 무척 힘들지만(가령, 0.05초마다 번갈아가며 함수를 수행한다든지), 자바에서는 다만 Thread클래스를 상속 혹은 Runnable 인터페이스를 구현하기만 하면 됩니다.

자 그러면 제가 만든 채팅에서는 쓰레드가 어디서 어떻게 쓰이고 있는 지 한 번 알아볼까요? (위에서도 언급한 내용이긴 합니다.)

우선 서버가 사용해야 합니다. 일단 서버용 소켓을 열어놓고, 클라이언트들이 언제 접속할 지 모르니 클라이언트들의 소켓접속 시도가 오면 이를 소켓 하나를 열어 연결시켜주는 작업을 스레드로 처리합니다. 스레드가 아니라면 접속요청이 하나 왔을 때, 소켓열고 하는 작업 하는 동안 다른 클라이언트는 접속을 할 수 없겠지요?

또한, 메시지를 받을 때도 필요합니다. 메시지가 언제 올 지 모르니 메시지를 받는 함수를 무한루프를 통해 돌고 있어야 겠지요. 이를 스레드로 처리하지 않으면 메시지 받기 위해서 다른 아무 일도 할 수 없겠지요? ^^; 메시지 받아서 처리해서 돌려보내는 동안 다른 메시지가 와도 처리 못하는 것은 물론이고요.

클라이언트 입장에서는 메시지 받을 때 필요하겠군요. 어떤 넘이 말할 지 모르니 음....또 언제 필요 있나? 갸우뚱 /?^_^?/ 말이 필요없다. 소스를 보시겠습니다. ^^

5.Thread부분 소스

서버 클라이언트 접속 받아 스레드로 생성해 넘기는 부분:

try
{
while(listening)
{
//ChatThread 는 각 클라이언트를 다루는 Thread를 상속한 클래스입니다.

ChatThread thread;
thread = new ChatThread(server,serverSocket.accept());
// server.addClient(thread);
thread.start();
}
serverSocket.close();
}
catch(IOException e) {}

이 부분이 서버측에서 위에서 보신 소스(main 메소드) 뒤 부분에 추가되어야하는 부분입니다.

listening = true니까 무한루프겠지요.

thread 생성시, chat 클래스(server)를 인자로 넘겨줍니다. 그쪽에서 쓸 필요가 있거든요.

그러면 ChatThread를 보시겠습니다. 필요없거나 지나치게 긴 부분은 생략된 부분입니다.

//클래스 자체가 스레드입니다.

class ChatThread extends Thread //그치요?
{
chat xServer;
Socket xSocket;

//클라이언트 속성
String bangje;
String szUserName;
String sex = null;
String age = null;
String loca = "없음";
String email = "없음";
String host;


//스트림 선언

public DataInputStream streamIn;
public DataOutputStream streamOut;
int nState=0;
boolean bangjang = false;//방장인가?
String time=null;
Vector temp;
String cpic="0",ccolor="0";//캐릭터채팅과 글자색상 저장을 위한 변수

public ChatThread(chat server,Socket socket)
{


//생성자 처리

xServer = server;
xSocket = socket;
streamOut =null;
streamIn = null;
}

public void run()
{
try
{

//스트림 열고
streamIn = new DataInputStream(new BufferedInputStream(xSocket.getInputStream()));
streamOut = new DataOutputStream(new BufferedOutputStream(xSocket.getOutputStream()));

String inputLine, outputLine;

//아까 접속되면 Terminal이라고 써서 보내주기로 했지요.

streamOut.writeUTF("Terminal");
streamOut.flush();


//이건 제가 함 해본건데, 클라이언트의 ip를 추적한 것이지요.

String localhost = xSocket.getInetAddress().toString();
host=localhost.substring(0,localhost.indexOf("/"));

//메시지받는 무한루프입니다.

while ((inputLine = streamIn.readUTF()) != null)
{

//StringTokenizer 사용법은 나중에.


StringTokenizer str = new StringTokenizer(inputLine,"|");
inputLine =(String) str.nextToken();

//이런저런 메시지 처리하도록 if문 분기

if(inputLine.equals("admin"))
{
xServer.adminchogi(str,this);
}
else if(inputLine.equals("adminout"))
{
xServer.adminout(this);
}
else if(inputLine.equals("newbang"))
{
xServer.newbang(str,this);
}

}
//종료처리

streamOut.close();
streamIn.close();
xSocket.close();
xServer.removes(this);
}

//Exception발생시 종료처리
catch(Exception e)
{

//갑자기 접속끊기거나 해서 나갔다면, 그 넘을 방에서 빼지 않으면 계속 남아 있을 지도..(리스트에서도 삭제)
temp = (Vector)xServer.groups.get(bangje);
temp.removeElement(this);
xServer.groups.put(bangje,temp);
}
}
}


헉헉.. 됐군요. 네 다 알려드리고 있습니다. 속 후련하시죠?

클라이언트 부분:

이 부분은 간단합니다. 메시지만 무한루프 돌면서 받아주면 되지요.

public void run()//Thread시건.. Runnable을 구현하셨던.. 어쨌든 run()메소드
{
try
{
while(true)
{
//메시지 종류에 따라 처리해주는 메소드
Messagekind(new StringTokenizer(input.readUTF(),"|"));
try
{

//스레드 잠깐 쉬어줍니다. 스레드가 너무 바쁘게 돌아가면 자칫 해야할 일을 못합니다.
wait(100);
}
catch(Exception e)
{
}
}
}
catch(IOException e)
{
disconnect();//스트림이 끊기면 접속이 끊겼단 뜻이므로 disconnet()를 호출(물론 사용자 정의 메소드)
}
}
 

 

6. 프로토콜 정의와 StringTokenizer 클래스의 이용법

먼저, 통신규약(프로토콜)을 먼저 정의해봅시다. 프로토콜은 보내는 측과 받는 측에서 미리 약속한 일정한 형식 같은 것을 의미합니다. 아무튼 서버와 클라이언트가 어떠한 형식으로 서로를 구분할 것인지를 약속해야 합니다. 프로토콜을 잘 정의하는 것은 소켓 통신의 기본입니다. 프로토콜을 얼마나 잘 정의하느냐에 따라서 프로그래밍이 대단히 어려울 수도 있고, 쉬워질 수도 있습니다. 프로토콜을 잘못 정의하면 프로그램 중에 프로토콜을 바꾸어야 하는 경우도 생길 수 있습니다. 이런 경우에는 데이터를 전송하거나 수신하는 부분을 다시 다 바꾸어주어야 하므로 보통 힘든 일이 아니겠죠.

정의는 크게 두 가지가 있습니다. 쓸 때 일정 바이트 수로 척척척 써서, 받을 때 그 바이트 수대로 잘라서 사용하는 법과, 각 토큰(우리가 사용할 메시지들)과 토큰 사이에 분리자를 넣어서 받을 때 분리자를 기준으로 메시지를 잘라서 사용하는 법이 그것입니다.토큰의 크기가 일정한 크기를 갖거나, 특정 최대값을 갖고 있다면 모르지만, 채팅에서는 일반적으로 그렇지 않기 때문에, (이 사람이 몇 글자를 칠 지 어떻게 알겠습니까..) 두 번째 방법을 많이 씁니다.

가령, 대화는 "dewha:할말" 귓속말은 "ear:id:할말" 이런 식으로 만들어서 서버로 전송하는 것이지요. 이해가 되시나요? 저것을 API에 나와있는 String의 무지 많은 메소드들을 잘 활용해서 잘라서 쓰면 되는 것입니다. 어려우시다고요? 음....그러면.. 이를 편하게 해주는 StringTokenizer클래스에 대해 알아볼까요? 간단합니다. 당장 치겠습니다. ^^;

StringTokenizer stn = new StringTokenizer("메시지", "구분자들");

String str1 = stn.nextToken();

String str2 = stn.nextToken();

...

이런 식으로 사용합니다. 구분자들은? 예를 들어 "@!"이렇게 넣으면 @혹은 !을 기준으로 토큰들을 구분해냅니다.

StringTokenizer str = new StringTokenizer(inputLine,"|");
s = str.nextToken();

s2 = str.nextToken();

s3 = str.nextToken();

이렇게 되어 있습니다. inputLine이라는 String을 "|"을 기준으로 잘라내는 것이지요.

inputLine은 뭐.. "ear | id | 할 말 " 이렇게 되어 있다면 s에 ear가 s2에 id가 s3에 할말이 들어가겠네요.

ear이므로 귓속말에 해당하는 메소드를 호출하시면 되겠고.. id이므로 자신의 방에 들어있는 각 클라이언트 스레드들을 검색해서 그 대화명이랑 여기 id랑 같은 스레드에게만 "할 말"을 보내주면 됩니다.

음하하. 오늘은 여기까지..... 일 안하고 강좌만 두 시간을 써댔네요. 문의는 문의게시판으로 해주세요. 다 도움되셨죠?

7. Canvas를 이용한 Image다루기

자, 이번엔 Canvas클래스에 대해 알아보도록 하겠습니다. Canvas는 마우스 이벤트를 받을 수 있는 그림 영역으로, Applet처럼 public void paint(Graphics g)를 오버라이드해서 사용합니다. Applet을 사용해보셨던 (이 채팅도 애플릿으로 되어있긴 하지만..) 분이라면 어렵지 않게 익히실 수 있으실 겁니다. Graphics클래스도 아셔야겠지요? Graphics는 Image를 그리는 도화지라고 표현을 하더군요. 하지만, 제가 보기엔 도화지보다는 펜이라는 생각이 듭니다. 어쨌든 이 안에는 drawString이라든지.. drawImage라든지.. 무궁무진한 메소드들이 있지요. API 한 번 찾아서 꼼꼼히 읽어보세요.

제가 자주 사용하는 Image 크기에 맞춰 그 크기 그대로 그리는 Canvas를 하나 보시겠습니다.

class Imagespr extends Canvas
{
Image xImg = null;

public void paint(Graphics g)
{
if(xImg == null) return ;
g.drawImage(xImg,0,0,this);
}

public void loadImage(Image img)
{
int status;
xImg = img;
setSize(xImg.getWidth(this),xImg.getHeight(this));
repaint();
}
}
 

네, 내부클래스로 많이 사용을 하지요. 사용은 Imagespr의 객체를 생성한 다음에 이 안의 loadImage(Image)메소드를 호출하여 표시합니다. Image는 applet에서 getDocumentBase(), getCodeBase() 등으로 받아와야겠지요? 그 부분을 보시겠습니다.

iloading = getImage(getCodeBase(),"chart-img/loading.gif");


로딩이미지를 받아오는 부분입니다. iloading은 Image의 객체겠지요. 음 화면에 바로 보여야하는 이미지라면 이렇게만 하면 간혹 큰 이미지의 경우 전체가 다 로딩되지 않은 채 애플릿이 뜰 때도 있습니다. 이미지를 로딩하는 것은 자바에서 내부적으로 별도의 스레드를 돌려서 로딩하기 때문이지요.

이럴 때는

MediaTracker tracker;
tracker = new MediaTracker(this);
iloading = getImage(getCodeBase(),"chart-img/loading.gif");
tracker.addImage(iloading,0);
iarrow = getImage(getCodeBase(),"chart-img/arrow.gif");
tracker.addImage(iarrow,0);


try
{
tracker.waitForAll(0);
}
catch(InterruptedException e)
{
return;
}
 

 

이런 식으로 MediaTracker를 이용합니다. MediaTracker생성하고, tracker에 Image와 인덱스(0)를 붙이고, 그 인덱스(0) 모두를 기다립니다. 써먹어보세요. ^^; 또한 이에 대한 API도 찾아보시기 바랍니다. 다른 메소드들도 대동소이합니다.

이제, 저 위에서 보인 Imagespr의 객체를 생성해 봅시다.

Imagespr isbt = new Imagespr[12];//기왕이면 어렵게 배열로 선언해봅시다. 배열 개수 선언하고,
for(int i = 0 ; i < 12 ; i++)
{
isbt[i] = new Imagespr();//각자 따로 생성해야 하는 거 아시죠?
if(i == 0)
isbt[i].loadImage(applet.ibt_p[i]);//loadImage를 사용합니다.

//이때, 상위클래스 객체로 받아온 Image를 인자로 받습니다.

else
isbt[i].loadImage(applet.ibt[i]);
isbt[i].addMouseListener(new MyMouseListener());//마우스리스너도 붙이네요? 필요하다면..
if (i < 10)
if (i == 4 || i == 9)
addComp(isbt[i], i, 0, 1, 1, 2);//이는 제가 만든 메소드로 GridBackLayout으로 어딘가에 붙이는 과정.
else
addComp(isbt[i], i, 0, 1, 1, 0);

else
addComp(isbt[i], i+4, 0, 1, 1, 1);
}

 

 

코딩을 보니까 단박에 이해되신다고요? 흠흠~ 좋습니다.

자 Canvas클래스의 자세한 이용(더블버퍼링, 백업이미지다루기, Clip이용, 스크롤바와의 연계 등)은 웹차트 강의때 하기로 하고...오늘은 일단 유용한 내부클래스 하나를 배워보는 것으로 ...여기까지!! ..........하고 끝내려고 했으나, 내부클래스에 대해 궁금해하시는 분이 계실까봐.. ㅜ.ㅜ 마저 더 써봅니다.

자 클래스 안에 또 클래스가 들어오는 형식이 4가지가 있습니다.(자세한 건 제 강좌 아래에 박용우님의 강좌 참고) 그 중 하나가 내부클래스인데, 그냥 멤버변수 선언하는 부분에 떡하니 클래스가 들어오는 것입니다. 여기서는 static변수를 사용할 수 없으며, 내부클래스안에서는 외부클래스의 필드와 메소드를 아무런 제한 없이 그대로 사용하실 수 있습니다. 허걱 좋지요.. 그러나 기우에서 말씀드리는데 혹시 this라는 명령어를 쓰고 싶으시면, 이 내부클래스 안에서 this를 사용하시면, 내부클래스 자체를 가리키겠지요...(당연한가?) 그럼 외부클래스의 메소드를 사용하는데 그 외부클래스 자체를 나타내고 싶다.. 하면. 그 외부클래스명.this 이렇게 쓰셔야 합니다. 쉽지요? 자 오늘은 여기까지. /^.^/

 

보다 나은 버전의 채팅 등 3가지의 프로젝트를 보다 충실하고 친절하게 설명한

"자바 실무 테크닉 비법전수"가 발간되었습니다.

구경하기 -> [http://www.50001.com/books/ ]
이 포스트를..
 덧글 36개  엮인글 쓰기  공감 

                    상우기님입니다. java awt 멀티채팅강좌  
                    상우기님입니다. java awt 멀티채팅강좌    
 [출처] java awt 멀티채팅강좌 |작성자 하이
 

저작자 표시 비영리 변경 금지
Trackback 0 Comment 0
prev 1 next