JAVA 개발을 하다보면 데이터를 파일, 데이터베이스 또는 다른 컴퓨터로 전송할 때 중요한 기술이 있는데, 그 중에 하나가 바로 객체직렬화(Serializable)이다.
객체직렬화는 객체의 메모리 상태를 포함한 모든 정보를 바이트스트림(stream of bytes)형태로 인코딩하는 과정을 말한다. 이과정을 통해 객체는 파일 시스템, 데이터베이스, 네트워크 등을 쉽게 전송하거나 저장할 수 있게된다.
바이트스트림(stream of bytes)란?
스트림은 클라이언트나 서버 간에 출발지, 목적지로 입출력하기 위한 데이터가 흐르는 통로를 말한다.
자바는 스트림의 기본 단위를 바이트로 두고 있기 때문에, 네트워크, 데이터베이스로 전송하기 위해 최소 단위인 바이트 스트림을 변환하여 처리한다.
(출처 : 직렬화_CodeJ.log )
구현하기위해서는 'java.io.Serializable'인터페이스를 구현해야한다. 이는 메소드를 포함하지 않는 인터페이스로, 클래스의 객체가 직렬화를 지원한다는 것을 JVM에 알리는 역할을 한다.
1. 스트림 생성 : 객체를 직렬화하기 위해 "FileOutputStream" 등의 출력 스트림을 생성한다.
2. 스트림 연결 : 생성된 출력 스트림을 ObjectOutputStream에 연결한다.
3. 객체 쓰기 : ObjectOutputStream의 writeObject 메소드를 호출하여 객체를 스트림에 쓰고, 이를 통해 파일이나 네트워크를 통해 전송한다.
역직렬화는 직렬화된 데이터 스트림을 다시 JAVA 객체로 변환하는 과정이다. ObjectInputStream을 사용하여 직렬화된 데이터를 읽고, readObject 메소드로 객체를 복원한다.
import java.io.*;
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private transient String email; // transient 키워드로 직렬화에서 제외
public Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
public String toString() {
return "Person [name=" + name + ", age=" + age + ", email=" + email + "]";
}
public static void main(String[] args) {
Person p = new Person("John Doe", 30, "john.doe@example.com");
String filename = "person.ser";
// 객체 직렬화
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))) {
out.writeObject(p);
System.out.println("Object has been serialized");
} catch (IOException ex) {
System.out.println("IOException is caught");
}
// 객체 역직렬화
Person pDeserialized = null;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename))) {
pDeserialized = (Person) in.readObject();
System.out.println("Object has been deserialized");
System.out.println(pDeserialized); // email 필드는 null로 출력될 것임
} catch (IOException ex) {
System.out.println("IOException is caught");
} catch (ClassNotFoundException ex) {
System.out.println("ClassNotFoundException is caught");
}
}
}
출력 결과
Object has been serialized
Object has been deserialized
Person [name=John Doe, age=30, email=null]
transient : Serializable로 선언한 객체 내에 전송하거나 저장하지 않는 변수를 선언할 때 사용한다. 위 예제의 email은 직렬화 대상에서 제외한다.
serialVersionUID : 직렬화된 객체를 역직렬화할 때 클래스 버전의 일치를 확인하는 데 사용되는 고유 식별자이다. 클래스를 수정하고 난 후에도 역직렬화 과정에서 호환성을 유지하기 위해 필요하다. serialVersionUID가 일치하지 않으면 InvalidClassException이 발생할 수 있다.
JDK 1.4에서부터 NIO(New IO)라는 것이 추가되었다. 이 것이 생긴이유는 속도 때문이다. NIO는 지금까지 사용한 스트림을 사용하지 않고, 대신 채널(Channel)과 버퍼(Buffer)를 사용한다.
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NioSample {
public static void main(String[] args){
NioSample sample = new NioSample();
sample.basicWriteAndRead();
}
public void basicWriteAndRead(){
String fileName = "./nio.txt";
try {
writeFile(fileName, "My first NIO Sample");
readFile(fileName);
} catch(Exception e){
e.printStackTrace();
}
}
public void writeFile(String fileName, String data) throws Exception {
FileChannel channel = new FileOutputStream(fileName).getChannel(); // 파일을 쓰기 위한 FileChannel 객체를 만들려면, FileOutputStream 클래스에 선언된 getChannel() 메소르를 호출
byte[] byteData = data.getBytes();
ByteBuffer buffer = ByteBuffer.wrap(byteData); // static으로 선언된 wrap()이라는 메소드를 호출하면, ByteBuffer 객체가 생성된다.
channel.write(buffer); // FileChannel 클래스에 선언된 write() 메소드에 buffer 객체를 넘겨주면 파일에 쓰게된다.
channel.close();
}
public void readFile(String fileName) throws Exception {
FileChannel channel = new FileInputStream(fileName).getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024); // ByteBuffer 클래스에 선언되어 있는 allocate() 메소드를 통해서 buffer라는 객체를 만들었다.
channel.read(buffer); // 데이터가 buffer 에 담으라고 알려준다.
buffer.flip(); // buffer 에 담겨있는 데이터의 가장 앞으로 이동한다.
while(buffer.hasRemaining()){ // hasRemaining() 메소드를 사용하여 데이터가 더 남아있는지를 확인하면서 반복작업을 수행한다.
System.out.println((char)buffer.get()); // get 메소드는 한 바이트씩 데이터를 읽는 작업을 수행한다.
}
channel.close();
}
}
출력 결과
My first NIO Sample
위 예제소스처럼 파일 데이터를 다룰 때는 ByteBuffer라는 버퍼와 FileChannel 이라는 채널을 사용하면 간단하게 처리할 수 있다. Channel의 경우 그냥 간단하게 객체만 생성하여 read()나 write() 메소드만 불러주면 된다고 생각하면 된다.
자바의 Serializable 인터페이스와 NIO 대해서 기초적인 내용을 정리하였다. NIO에 대해서 좀 더 알아볼 필요성이 있을거 같다.
그리고 객체직렬화라고 한다면 통신, 통신이라면 빅엔디안 / 리틀엔디안 변환 과정이 있어야하는데 JAVA에서는 다른 언어들처럼 네트워크 전송을 위해서 리틀 엔디안/빅 엔디안를 구분하지 않고, 대부분의 경우 빅 엔디안(Big-Endian) 방식을 사용한다. JAVA는 JVM 위에서 수행하므로 대부분 빅 엔디안 방식을 사용하여 처리한다. 물론 ByteBuffer 클래스에서 order(ByteOrder.LITTLE_ENDIAN)을 사용하여 리틀 엔디안으로도 데이터 처리를 사용할 수 있다.