직렬화, 역직렬화란?
객체지향 언어인 Java는 프로그램의 모든 데이터들이 객체로 이루어져 있다고 봐도 무방하다.
그렇다면 Java로 만든 프로그램의 데이터(객체)를 외부로 전송하려면 어떻게 해야 할까? 네트워크를 공부했다면 기본적으로 데이터인 객체 그 자체를 네트워크 상으로 전송할 수 없다는 것을 알 것이다.
이를 전송하기 위해선 객체 그 자체보단 조금 더 단순한 형태로 변환해야 할 것이다.
Java의 I/O 처리는 정수, 문자열, 바이트 단위의 처리만 지원하기 때문에 복잡한 객체의 내용을 저장/복원하거나 네트워크 상으로 전송하기 위해서는 객체의 내용을 I/O가 처리할 수 있는 형태로 변환해 줘야 한다.
Java에서 말하는 객체 직렬화는 이처럼 Java의 객체를 외부로 저장/복원하거나 네트워크 상으로 전송할 수 있도록 바이트 형태로 변환하는 기술을 의미한다.
즉, 객체가 아무리 복잡하여도 직렬화를 통해 객체를 바이트 형태로 변환하여 외부로 전송할 수 있는 것이다.
반대로 역직렬화는 직렬화를 통해 변환된 바이트 형태를 다시 원상태인 객체로 변환시키는 기술을 의미한다.
직렬화의 특징은 다음과 같다.
- 프로그램이 종료되어도 객체의 데이터는 파일로 변환하여 저장되어 있기 때문에 언제든지 불러서 다시 객체로 변환할 수 있으며 외부로 보내서 데이터를 공유할 수 있음
- Java의 기본 라이브러리를 사용하지 않더라고 여러 형태(CSV, JSON, 일반 파일 등)로 수행이 가능
- Java에서 제공하는 직렬화 기능은 오직 Java 프로그램끼리만 공유가 가능한 데이터이며 코드 수정을 하거나 자바 버전이 달라 클래스 속성이 바뀌게 되면 사용할 수 없음
- Java의 JVM에서 자동으로 처리해주기 때문에 수신부와 송신부의 운영체제가 달라도 아무런 상관이 없음
직렬화 조건
- java.io.Serializable 인터페이스를 상속받은 객체는 직렬화 할 수 있는 기본 조건이다.
class Person implements Serializable {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
다만, Serializable 인터페이스를 implements 하여도 따로 구현해야 할 내용은 없다. Serializable 인터페이스를 확인해보면 아무것도 없는 것을 확인할 수 있다.
Seializable 인터페이스는 현재 클래스의 객체가 직렬화가 제공되어야 함을 JVM에게 알려주는 역할만 수행한다.
그렇다면 Person 클래스의 객체를 직렬화를 통해 파일로 변환하여 저장해보자.
객체 직렬화는 직렬화하고자 하는 객체에 직렬화를 수행해주는 ObjectOutputStream과 직렬화한 내용을 저장할 .txt 파일을 생성해주는 FileOutputStream을 통해 수행된다.
다음과 같이 Serialization.txt 파일을 열어서 person 객체의 정보들을 직렬화시켜서 저장해준다.
public class Main {
public static void main(String[] args) throws IOException {
Person person = new Person("Libi", 26);
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Serialization.txt"))){
out.writeObject(person);
} catch (Exception e) {
}
}
}
수행하면 Serialization.txt에는 다음과 같이 변환된 바이트 형태의 내용이 저장되어 있다.
그렇다면 직렬화로 저장된 파일을 역직렬화를 통해 불러오도록 하자. 역직렬화도 직렬화와 비슷하게 수행된다.
객체 역직렬화는 불러온 파일에 역직렬화를 수행하여 객체로 복수시켜주는 ObjectInputStream과 .txt 파일을 불러오는 FileInputStream을 통해 수행된다.
public class Main {
public static void main(String[] args) throws IOException {
Person person = null;
try(ObjectInputStream in = new ObjectInputStream(new FileInputStream("Serialization.txt"))){
person = (Person) in.readObject();
} catch (Exception e) {
}
System.out.println(person.toString());
}
}
이전에 직렬화를 수행하여 변환한 객체의 정보가 원래대로 복구된 것을 확인할 수 있다.
현재 Person 클래스의 모든 정보를 직렬화를 통해 저장하였다. 만약 특정 정보를 저장하고 싶지 않다면 어떻게 해야 할까?
이는 저장하고 싶지 않은 멤버에 Transient or Static 키워드를 붙이면 제외할 수 있다.
class Person implements Serializable {
static String name;
transient int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
name과 age에 대한 정보가 저장되어 있지 않은 것을 확인할 수 있다. 역직렬화를 통해 해당 파일을 읽어보자.
아무런 정보가 없기 때문에 기본값으로 초기화되어 출력되는 것을 확인할 수 있다.
역직렬화를 수행할 때 주의해야 할 점들이 몇 가지 존재한다.
먼저 역직렬화를 수행하기 위해선 직렬화한 클래스의 속성과 역직렬화를 수행하는 클래스 속성이 일치해야 한다고 하였다. 그렇다면 Java가 어떻게 두 클래스의 속성이 일치하는지 판단할 수 있을까?
맞지 않는 상황 예시
기존 멤버 클래스
public class Member implements Serializable {
private String name;
private String email;
private int age;
// 생략
}
직렬화한 Data
rO0ABXNyABp3b293YWhhbi5ibG9nLmV4YW0xLk1lbWJlcgAAAAAAAAABAgAESQADYWdlSQAEYWdlMkwABWVtYWlsdAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgABeHAAAAAZAAAAAHQAFmRlbGl2ZXJ5a2ltQGJhZW1pbi5jb210AAnquYDrsLDrr7w=
멤버 클래스 속성이 추가된 상황
public class Member implements Serializable {
private String name;
private String email;
private int age;
//phone 추가
private String phone;
}
속성이 추가된 상황에서 직렬화한 Data를 역직렬화 한다면??
- java.io.InvalidClassException 발생
- 직렬화하는 시스템과 역직렬화하는 시스템이 다른 경우 발생
- 각 시스템에서 사용하고 있는 모델 버젼 차이 발생시 생기는 문제
이를 해결하기 위해 serialVersionUID 필드를 통해 판단한다. 이는 Serializable 인터페이스에 구현된 것으로, 직렬화를 선언한 클래스를 컴파일할 때 컴파일러가 클래스 구조 정보를 해시값으로 변환한 값을 자동으로 넣어준다.
Default 값으로 클래스의 기본 해시값을 가진다.
- Serializable 인터페이스의 필드 값
- 클래스에 수동으로 ID를 부여함으로써 내용에 관계없이 동일 클래스로 인식함
- 필수 값은 아니지만 클래스에 내용이 추가될 경우에도 역직렬화를 사용할 수 있게 해줌
즉, 이 값을 비교하여 두 클래스의 속성이 같은지를 판단해 준다.
class Person implements Serializable {
private static final long serialVersionUID = 1L;
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
또한, String → StringBuilder, int → long 등 잘못된 타입으로 받아들이면 Exception이 발생한다. 즉, 객체 직렬화는 타입에 대해 상당히 엄격하다는 것을 알 수 있다.
마지막으로 간단한 객체의 내용도 직렬화를 수행하면 데이터뿐만 아니라 다른 구조 데이터 등의 정보도 포함되기 때문에 데이터의 크기가 많이 커지는 단점이 있다.
따라서 웬만하면 내용 변경이 없을만한 클래스에 사용하는 것이 좋으며, 사용하더라도 장기 보관용으로는 적합하지 않다. 또한, 데이터 크기가 많이 차이 나기 때문에 긴 만료 시간을 가지는 데이터는 JSON 등 다른 포맷을 사용하여 저장하는 것이 낫다.
이러한 객체 직렬화는 Java에서 서블릿 세션(Servlet Session), 캐시(Cache), 자바 RMI(Remote Method Invocation) 등에서 사용된다.
서블릿 세션
- 세션을 서블릿 메모리 위에서 운용하면 직렬화가 필요하지 않지만, 파일로 저장하거나 세션 클러스터링, DB를 저장하는 옵션 등을 선택하면 세션 자체가 직렬화가 되어 저장된다.
캐시
- Encache, Redis, Memcached 라이브러리 시스템에 많이 사용된다.
자바 RMI(Remote Method Invocation)
- 원격시스템 간의 메시지 교환을 위해서 사용하는 자바에서 지원하는 기술.
JSON 직렬화, 역직렬화
지금까지는 객체 직렬화를 JVM과 ObjectInputStream/ObjectOutputStream에 위임하는 방식이었지만 XML, JSON과 같은 포맷을 이용한 직렬화도 가능하다. 이로 인한 장점은 다른 환경, 다른 언어로 만들어진 어플리케이션과도 통신이 가능해진다는 것이다.
JSON 파싱, 처리 라이브러리를 사용하여 자바 객체를 JSON으로 직렬화하거나 JSON 데이터를 자바 객체로 역직렬화할 수 있다. 여기서는 Jackson을 사용한다.
(1) Object to JSON(직렬화)
pom.xml
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.1</version>
</dependency>
java
List<Member> memberList = new ArrayList<>();
memberList.add(new Member(1001, "Kate", 30));
memberList.add(new Member(1002, "Jason", 23));
memberList.add(new Member(1003, "Aaron", 35));
ObjectMapper mapper = new ObjectMapper();
// object to json
mapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);
mapper.writeValue(new File("test.json"), memberList);
test.json
[
{
"id": 1001,
"name": "Kate",
"age": 30
},
{
"id": 1002,
"name": "Jason",
"age": 23
},
{
"id": 1003,
"name": "Aaron",
"age": 35
}
]
(2) JSON to Object(역직렬화)
java
ObjectMapper mapper = new ObjectMapper();
// json to object
mapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);
mapper.writeValue(new File("test.json"), memberList);
System.out.println(mapper.readValue(new File("test.json"), new ArrayList<Member>().getClass()));
configure()는 transient 처리를 위해 추가한 코드이다.
필드에 직접 @JsonIgnore 애노테이션을 붙여도 된다.
[{id=1001, name=Kate, age=30}, {id=1002, name=Jason, age=23}, {id=1003, name=Aaron, age=35}]
Process finished with exit code 0
ref
https://n1tjrgns.tistory.com/209
'Java 공부' 카테고리의 다른 글
자바의 데이터 타입(Primitive type, Reference type) (0) | 2022.02.28 |
---|---|
Java 컴파일 과정 (0) | 2022.02.28 |
동기화(Synchronized ) vs 비동기화(Asynchronized) / 블로킹(blocking)과 논블로킹(non-blocking) (0) | 2021.11.14 |
자바 스레드(Thread) 정리글 (0) | 2021.11.14 |
가비지 컬렉션(Garbage Collection)이란 (0) | 2021.11.13 |