[Java][I/O] 직렬화(Serialization)

2022. 6. 28. 12:44JAVA/Language

1. 직렬화란 무엇인가?

직렬화(Serialization)란 객체를 데이터 스트림으로 만드는 것을 뜻합니다. 즉, 객체에 저장된 데이터를 스트림에 쓰기(write) 위해 연속적인(serial) 데이터로 변환하는 것을 의미합니다.

 

반대로 스트림으로부터 데이터를 읽어서 객체를 만드는 것을 역직렬화(deserialization)이라고 합니다.

 

객체의 직렬화와 역직렬화

객체의 정의 및 특징

  • 클래스에 정의된 인스턴스 변수의 집합
  • 객체에는 클래스 변수나 메서드가 포함되지 않음
  • 객체는 오직 인스턴스변수들로만 구성되어 있음

 

2. ObjectInputStream, ObjectOutputStream

클래스의 정의된 인스턴스 변수가 단순한 기본형일때는 직렬화하여 저장하는 일이 간단하지만 인스턴스 변수의 타입이 참조형일때는 간단하지 않습니다. ObjectInputStream과 ObjectOutputStream 클래스는 이러한 복잡한 것을 대신하여 직렬화 /역직렬화 해주는 클래스입니다.

 

  • ObjectInputStream : 역직렬화하는데 사용되고 InputStream을 직접 상속받고 기반스트림을 필요로 하는 보조스트림 클래스
  • ObjectOutputStream : 직렬화하는데 사용되고 OutputStream을 직접 상속받고 기반스트림을 필요로 하는 보조스트림 클래스

ObjectInputStream / ObjectOutputStream 생성자

ObjectInputStream(InputStream in)
ObjectOutputStream(OutputStream out)

 

파일에 객체를 저장(직렬화)

FileOutputStream fos = new FileOutputStream("objectfile.ser");
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(new UserInfo());

 

파일로부터 객체를 불러오기(역직렬화)

FileInputStream fis = new FileInputStream("objectfile.ser");
ObjectInputStream in = new ObjectInputStream(fis);
UserInfo info = (UserInfo) in.readObject();

 

3. 직렬화가 가능한 클래스 만들기 - Serializable, transient

직렬화가 가능한 클래스를 만들기 : 직렬화하고자 하는 클래스가 java.io.Serializable 인터페이스를 구현하도록 함

직렬화가 불가능한 클래스
public class UserInfo{
	String name;
    String password;
    int age;
}

직렬화 가능한 클래스
public class UserInfo implements java.io.Serializable{
	String name;
    String password;
    int age;
}

 

부모 클래스와 자식 클래스간의 직렬화에 대한 관계

  • 부모 클래스에서 Serializable 인터페이스를 구현하였다면 부모 클래스를 상속받는 자식 클래스도 직렬화가 가능함
  • 하지만 자식 클래스가 Serializable 인터페이스를 구현하고 부모 클래스가 Serializable 인터페이스가 구현되어 있지만 않다면 기본적으로 부모 클래스의 멤버는 직렬화 대상에서 제외된다.
  • Object 클래스는 Serializable을 구현하지 않았기 때문에 직렬화할 수 없음. 하지만 인스턴스의 Object obj에 실제로 저장된 객체가 직렬화 가능한 클래스의 인스턴스라면 직렬화가 가능함
public class SuperUserInfo{
	String name;     // 직렬화 대상 아님
    String password; // 직렬화 대상 아님
}

public class UserInfo extends SuperUserInfo implements Serializable{
	int age; // 직렬화 대상
}

 

직렬화 대상이 아닌 부모 클래스의 필드멤버를 직렬화 하는 방법

  1. 부모 클래스가 Serializable 인터페이스를 구현
  2. 자식 클래스에서 부모의 인스턴스 변수들이 직렬화되도록 처리하는 코드를 직접 추가
    • readObject(), writeObject() 메서드 직접 구현
		// 부모 클래스의 멤버의 직렬화를 수행함. 나머지는 일반적인 직렬화를 수행
	private void writeObject(ObjectOutputStream out) throws IOException {
		out.writeUTF(name);
		out.writeUTF(password);
		out.defaultWriteObject();
	}
	
	// 부모 클래스의 멤버의 역직렬화를 수행함. 나머지는 일밭적인 역직렬화를 수행
	private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
		name = in.readUTF();
		password = in.readUTF();
		in.defaultReadObject();
	}
  • 자식 클래스에서 직접 구현

 

직렬화 대상에서 제외 (transient)

직렬화하고자 하는 객체의 클래스에 직렬화가 안 되는 객체에 대한 참조를 포함하고 있다면, 제어자 transient를 붙여서 직렬화 대상에서 제외되도록 할 수 있습니다.

public class UserInfo implements Serialiable{
    String name;
    transient String password; // 직렬화 대상에서 제외됨
    int age;
    
    transient Object obj = new Object(); // 직렬화 대상에서 제외
    
}

 

생성한 객체를 직렬화하여 파일(UserInfo.ser)에 저장하는 예제

public class UserInfo implements Serializable {
		
	String name;
	String password;
	int age;
	
	public UserInfo() {
		this("Unknown", "1111", 0);
	}
	
	public UserInfo(String name, String password, int age) {
		this.name = name;
		this.password = password;
		this.age = age;
	}

	@Override
	public String toString() {
		return "(" + name + "," + password + "," + age + ")";
	}
}
	@Order(1)
	@Test
	void UserInfoSerialTest() {
		printMethodName("UserInfoSerialTest");
		
		String fileName = "./src/ch15/ex_15_serialization/UserInfo.ser";
		try(FileOutputStream fos = new FileOutputStream(fileName);
			BufferedOutputStream bos = new BufferedOutputStream(fos);
				ObjectOutputStream out = new ObjectOutputStream(bos)){
			
			UserInfo u1 = new UserInfo("kim", "1234", 20);
			UserInfo u2 = new UserInfo("lee", "4567", 21);
			
			List<UserInfo> list = new ArrayList<UserInfo>();
			list.add(u1);
			list.add(u2);
			
			// 객체를 직렬화한다.
			out.writeObject(u1);
			out.writeObject(u2);
			out.writeObject(list);
			
			System.out.println("직렬화가 잘 끝났습니다.");
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

 

파일에 직렬화한 객체를 역직렬화하여 출력하는 예제

	@Order(2)
	@Test
	void UserInfoDeSerialTest() {
		printMethodName("UserInfoDeSerialTest");
		
		String fileName = "./src/ch15/ex_15_serialization/UserInfo.ser";
		try (FileInputStream fis = new FileInputStream(fileName);
				BufferedInputStream bis = new BufferedInputStream(fis);
					ObjectInputStream in = new ObjectInputStream(bis)){
						
						// 객체를 읽을 때는 출력한 순서와 일치해야한다.
						UserInfo u1 =(UserInfo) in.readObject();
						UserInfo u2 = (UserInfo) in.readObject();
						List<UserInfo> list = (ArrayList<UserInfo>) in.readObject();
						
						System.out.println(u1);
						System.out.println(u2);
						System.out.println(list);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
==========UserInfoDeSerialTest==========
(kim,1234,20)
(lee,4567,21)
[(kim,1234,20), (lee,4567,21)]

 

References

source code : https://github.com/yonghwankim-dev/java_study/tree/main/ch15
[도서] Java의 정석, 남궁 성 지음