개발 기록
> [Java] 직렬화 (Serializable) 에 대해서 1. 개념과 구현 본문

■
1. 개념
직렬화: 객체를 정적 바이트 스트림으로 변환하는 기술
역직렬화 : 바이트로 변환된 데이터를 다시 객체로 변환하는 기술
여기서 객체는 JVM(Java Virtual Machine 이하 JVM)의 메모리에 상주(힙 또는 스택)되어 있는 객체 데이터를 말한다.
직렬화된 객체는 외부의 자바 시스템에서도 사용할 수 있고 데이터베이스에 저장하거나 네트워크를 통해 전송할 수 있다. (파일, 통신이 주 용도)
■
2. 직렬화
(1) 직렬화 조건
① 자바 기본(primitive) 타입과 java.io.Serializable 인터페이스를 상속받은 객체는 직렬화 할 수 있는 기본 조건을 가진다.
② Static Field 는 Class 에 속하기 때문에 Serialize 되지 않는다.
③ 클래스가 Serializable interface 구현하면 모든 하위 클래스도 직렬화 가능하다.
④ 객체가 다른 객체에 대한 참조를 갖고 있는 경우 해당 객체도 직렬화 가능 인터페이스를 별도로 구현해야 한다. 그렇지 않으면 NotSerializedException이 발생한다.
* Serialize하는 과정에 제외하고 싶은 경우 transient 키워드를 사용하면 된다.
* Java 직렬화는 I/O 스트림을 많이 사용한다. 스트림을 닫는 것을 잊어 버리면 리소스 누수가 발생하기 때문에 꼭 stream 을 닫아야한다. (리소스 누출 방지 : try-with-resources)
(2) 직렬화 구현
▶ 객체를 직렬화하기 위해 ObjectOutputStream 클래스 의 writeObject() 메서드를 호출한다.
writeObject() 메서드는 직렬화 가능한 개체를 가져와 이를 바이트 시퀀스(스트림)로 변환한다.
public final void writeObject(Object o) throws IOException;
▶ 직렬화 가능한 개체를 만들기 위해 Serializable interface 구현
/**
* 직렬화할 회원 클래스
*/
@Data
public class User implements Serializable {
private String name;
private String email;
private int age;
}
▶ 예제 1. Person 유형의 객체를 로컬 파일에 저장한다. 직렬화 중에 바이트 스트림으로 변환되어 파일에 저장된다.
@Test
public void whenSerializingAndDeserializing_ThenObjectIsTheSame() ()
throws IOException, ClassNotFoundException {
User user = new User("김회원", "user@email.com", 25);
// 데이터를 저장하기 위한 파일 경로
FileOutputStream fileOutputStream
= new FileOutputStream("yourfile.txt");
// user 개체를 파일로 직렬화
// User 개체 의 상태를 "user.txt" 파일 에 저장하기 위해 ObjectOutputStream을 사용
ObjectOutputStream objectOutputStream
= new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(person);
objectOutputStream.flush();
objectOutputStream.close();
}
▶ 예제 2. 객체를 직렬화하여 바이트 배열(byte []) 형태로 변환한다.
User user = new User("김회원", "user@email.com", 25);
byte[] serializedMember;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(user);
// serializedMember -> 직렬화된 user 객체
serializedMember = baos.toByteArray();
}
}
// 바이트 배열로 생성된 직렬화 데이터를 base64로 변환
System.out.println(Base64.getEncoder().encodeToString(serializedMember));
}
(3) Serial Version UID
☞ 직렬화 가능한 각 클래스와 연결하는데 사용한다
☞ 저장된 바이트 스트림(역직렬화)을 읽어 해석할 경우 읽은 바이트 스트림이 해당 객체와 동일한 속성을 가지며 따라서 직렬화에 호환되는지 검사한다. serialVersionUID 다를경우 java.io.InvalidClassException 을 발생한다.
☞ 직렬화 가능 클래스에 serialVersionUID가 없으면 JVM 이 런타임에 자동으로 생성한다. 하지만 컴파일러에 따라 생성된 클래스가 다르므로 예기치 않은 InvalidClassExceptions가 발생할 수 있어서 각 클래스에서 serialVersionUID를 선언하는 것이 좋다. (더 알아보기)
(4) 직렬화 사용처
ⓛ Hibernate, JPA
② RMI( Remote Method Invocation, 원격 메소드 호출 )
③ EJB( Enterprise JavaBeans; EJB )
④ JMS( Java Message Service ) 등
★ Java 직렬화 장점
- 간단하면서도 확장 가능하다.
- 객체 유형과 속성을 직렬화된 형태로 유지한다.
- 원격 개체에 필요한 마샬링 및 역마샬링을 지원하도록 확장 가능하다.
- Java 에서 기본으로 제공하므로 외부 라이브러리가 필요하지 않다.
■
3. 역직렬화
(1) 역직렬화 조건
① 직렬화 대상이 된 객체의 클래스가 클래스 패스에 존재해야 하며 import 되어 있어야 한다.
* 중요한 점은 직렬화와 역직렬화를 진행하는 시스템이 서로 다를 수 있다는 것을 반드시 고려해야 한다.
② 자바 직렬화 대상 객체는 동일한 serialVersionUID 를 가지고 있어야 합니다.
(2) 역직렬화 구현
▶ 역직렬화를 수행하기 위해 ObjectInputStream 클래스의 readObject() 메서드를 사용한다. readObject() 메서드는 바이트 스트림을 읽고 이를 다시 Java 객체로 변환한 다음 원래 개체로 다시 변환한다.
public final Object readObject()
throws IOException, ClassNotFoundException;
▶ 예제 1. user 개체를 파일로 직렬화 한 값을 다시 읽어들인다.
FileInputStream fileInputStream
= new FileInputStream("yourfile2.txt");
ObjectInputStream objectInputStream
= new ObjectInputStream(fileInputStream);
Employee e2 = (Employee) objectInputStream.readObject();
objectInputStream.close();
▶ 예제 2. user 개체를 직렬화한 바이트 배열(byte []) 값을 역직렬화한다.
// 직렬화 예제에서 생성된 base64 데이터
String base64Member = "...생략";
byte[] serializedMember = Base64.getDecoder().decode(base64Member);
try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedMember)) {
try (ObjectInputStream ois = new ObjectInputStream(bais)) {
// 역직렬화된 Member 객체를 읽어온다.
Object objectMember = ois.readObject();
Member member = (Member) objectMember;
System.out.println(member);
}
}
■
4. 직렬화 유효성 검증
(1) try
해당 객체가 java.io.Serialized 또는 java.io.Externalized 의 인스턴스인지 확인한다. 가장 간단하지만 객체 직렬화를 보장하지 않는다.
@Test(expected = NotSerializableException.class)
public void whenSerializing_ThenThrowsError() throws IOException {
Address address = new Address();
address.setHouseNumber(10);
FileOutputStream fileOutputStream = new FileOutputStream("yofile.txt");
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
objectOutputStream.writeObject(address);
}
}
(2) Spring core core Serialization Utils
Spring core SerializationUtils 의 serialize () 메서드를 사용한다. 이 메소드는 직렬화 가능하지 않은 객체를 허용하지 않는다.
- 유형 변환을 통해 직렬화할 수 없는 Address 객체를 직렬화하려고 하면 런타임 시 ClassCastException 이 발생한다.
Address address = new Address();
address.setHouseNumber(10);
org.springframework.util.SerializationUtils.serialize((Serializable) address);
*유사한 방식으로는 Apache Commons SerializationUtils 이 있다.
(3) 사용자 정의 Serialization Utils
public static boolean isSerializable(Class<?> it) {
boolean serializable = it.isPrimitive() || it.isInterface() || Serializable.class.isAssignableFrom(it);
if (!serializable) {
return false;
}
Field[] declaredFields = it.getDeclaredFields();
for (Field field : declaredFields) {
if (Modifier.isVolatile(field.getModifiers()) || Modifier.isTransient(field.getModifiers()) ||
Modifier.isStatic(field.getModifiers())) {
continue;
}
Class<?> fieldType = field.getType();
if (!isSerializable(fieldType)) {
return false;
}
}
return true;
}
참고
https://www.baeldung.com/java-serial-version-uid
https://www.baeldung.com/java-serialization-approaches
https://www.baeldung.com/java-serialization
https://www.baeldung.com/java-validate-serializable
'JAVA' 카테고리의 다른 글
| > [Java] 직렬화 (Serializable) 에 대해서 3. Java 객체 직렬화/역직렬화 방법(Json, YAML, binary ) - GSON, YAML Bean, Protocol Buffers 등 (0) | 2020.12.14 |
|---|---|
| > [Java] 직렬화 (Serializable) 에 대해서 2. 의문점과 답변 정리 (0) | 2020.10.02 |
| >[Java] static, final, 상수 개념과 사용 (0) | 2020.07.19 |
| > JVM(Java Virtual Machine): 아키텍처 및 성능 분석 (0) | 2020.07.13 |
| >[Java] Java 객체지향 프로그래밍(OOP, Object-Oriented Programming)에 대하여 (0) | 2020.07.13 |