직렬화와 역직렬화
정의
직렬화
말그대로 객체를 직렬화하여 전송 가능한 형태로 만드는 것을 의미한다. 객체들의 데이터를 연속적인 데이터로 변형하여 Stream을 통해 데이터를 읽도록 해준다. 이것은 주로 객체들을 통째로 파일로 저장하거나 전송하고 싶을 때 주로 사용된다.
역직렬화
직렬화된 파일 등을 역으로 직렬화하여 다시 객체의 형태로 만드는 것을 의미한다. 저장된 파일을 읽거나 전송된 스트림 데이터를 읽어 원래 객체의 형태로 복원한다.
직렬화는 다음과 같은 특징을 가진다.
- 프로그램이 종료되어도 객체의 데이터는 파일로 변환하여 저장되어 있기 때문에 언제든지 불러서 다시 객체로 변환할 수 있으며 외부로 보내서 데이터를 공유할 수 있음
- Java의 기본 라이브러리를 사용하지 않더라고 여러 형태(CSV, JSON, 일반 파일 등)로 수행이 가능
- Java에서 제공하는 직렬화 기능은 오직 Java 프로그램끼리만 공유가 가능한 데이터이며 코드 수정을 하거나 자바 버전이 달라 클래스 속성이 바뀌게 되면 사용할 수 없음
- Java의 JVM에서 자동으로 처리해주기 때문에 수신부와 송신부의 운영체제가 달라도 아무런 상관이 없음
직렬화/역직렬화의 수행 방법
기본적으로 클래스의 객체를 직렬화시키기 위해선 Serializable 인터페이스를 implements 해줘야 한다.
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 인터페이스를 확인해보면 아무것도 없는 것을 확인할 수 있다. 그저 현재 클래스의 객체가 직렬화가 제공되어야 함을 JVM에게 알려주는 역할만 수행한다.
그렇다면 실제 직렬화는 어떻게 수행될까? 객체 직렬화는 직렬화하고자 하는 객체에 직렬화를 수행해는 ObjectOutputStream과 직렬화한 내용을 저장할 .txt 파일을 생성해주는 FileOutputStream을 통해 수행된다.
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());
}
}
이전에 직렬화를 수행하여 변환한 객체의 정보가 원래대로 복구된 것을 확인할 수 있다.
만약 직렬화 시에 특정 정보를 저장하고 싶지 않다면, 저장하고 싶지 않은 멤버에 Transient나 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 + "]";
}
}
직렬화가 사용되는 이유
직렬화는 JVM의 메모리에만 상주되어 있는 객체 데이터를 그대로 영속화(persist)가 필요할 때 사용된다. 시스템이 종료되더라도 없어지지 않는 장점을 가지며 영속화된 데이터이기 때문에 네트워크로 전송도 가능하다. 그리고 필요할 때 직렬화 된 객체 데이터를 가져와서 역직렬화하여 객체를 바로 사용할 수 있게 된다.
- 서블릿 세션들은 대부분 세션의 Java 직렬화를 지원하고 있다. 단순히 세션을 서블릿 메모리 위에서 운용한다면 직렬화가 필요없지만, 파일로 저장하거나 세션 클러스터링, DB를 저장하는 옵션등을 선택하게 되면 세션 자체가 직렬화되어 저장되어 전달된다.
- 캐시에서도 직렬화는 사용된다. 주로 DB를 조회한 후 가져온 데이터 객체 같은 경우 실시간 형태로 요구하는 데이터가 아니라면 메모리, 외부 저장소, 파일 등을 저장소를 이용해서 데이터 객체를 저장한 후 동일한 요청이 오면 DB를 다시 요청하는 것이 아니라 저장된 객체를 찾아서 응답하게 된다. 캐시를 이용하면 DB 리소스를 절약할 수 있기 때문에 많은 시스템에서 자주 활용되고, 이렇게 캐시할 부분을 자바 직렬화된 데이터를 저장해서 사용된다.
직렬화의 버전 관리
직렬화를 하게 되면 내부에서 자동으로 SerialVersionUID라는 고유의 번호를 생성하여 관리한다. 이 식별 ID는 클래스를 직렬화, 역직렬화 과정에서 동일한 특성을 갖는지 확인하는데 사용된다. 그래서 클래스 내부 구성이 수정될 경우, 기존에 직렬화한 SUID와 현재 클래스의 SUID 버전이 다르기 때문에 이를 인지하고 InvalidClassException 예외가 발생시켜 값 불일치 되는 현상을 미연에 방지한다.
그러나 실제로 개발 중에는 클래스의 네트워크로 객체를 직렬화하여 전송하거나 협업을 하는 경우 수신자와 송신자 모두 같은 버전의 클래스를 가지고 있어야 할텐데, 만일 클래스가 조금만 변경사항이 있으면 모든 사용자에게 재배포해야 하는 애로사항이 생겨 프로그램을 관리하기 어렵게 만든다.
따라서 직렬화 클래스는 왠만한 상황에선 serialVersionUID 를 직접 명시해주어 클래스 버전을 수동으로 관리하는 것을 권장하는 편이다. SUID를 직접 명시해주면 클래스의 내용이 변경되어도, 클래스의 버전이 시스템이 자동 생성된 값으로 변경되지 않기 때문이다. 이외에도 런타임에 SUID를 생성하는 시간도 많이 잡아먹기 때문에 미리 명시를 강력히 권장되는 바이다.
serialVersionUID 는 아래와 같이 private static final 제어자로 선언해야 하며 타입은 long 이다.
class Member implements Serializable {
// serialVersionUID 꼭 명시 할 것
private static final long serialVersionUID = 123L;
private String name;
private int age;
private String address;
// private String email; // 새로 추가한 클래스 구성 요소
...
}
직렬화의 주의 사항
Java 직렬화를 사용할 경우 역직렬화를 할 때 예외가 생길 수 있다는 사실을 인지하고 반드시 예외 처리를 해야한다.
직렬화 시 참조 형식의 변수는 사용할 수 없다. (참조하는 주소가 계속 바뀌기 때문)
역직렬화는 생성자 없이 객체를 생성하는 방식이므로, 보안에 취약하다.
또한 자주 변경되는 비즈니스적인 데이터를 Java 직렬화을 사용하지 않는다. 긴 만료시간을 가지는 데이터는 JSON 등 다른 포맷을 사용하여 저장해야 한다. (문자열 형태의 직렬화)
직렬화의 다른 종류 - 문자열 형태의 직렬화
- 직접 데이터를 문자열 형태로 확인 가능한 직렬화 방법. 범용적인 API나 데이터를 변환하여 추출할 때 많이 사용된다.
- 표형태의 다량의 데이터를 직렬화시 CSV가 많이 쓰인다.
- 구조적인 데이터를 직렬화 할 때는 이전에는 XML을 많이 사용했지만 최근에는 JSON을 많이 사용하고 있다
- CSV
- 데이터를 표현하는 가장 많이 사용되는 방법 중 하나로 콤마(,) 기준으로 데이터를 구분하는 방법
김배민,deliverykim@baemin.com
- 자바에서 사용방법 예제에서는 문자열로 단순히 변경했다.
자바에서는 Apache Commons CSV, opencsv 등의 라이브러리 등을 이용할 수 있습니다. Member member = new Member("김배민", "deliverykim@baemin.com", 25); // member객체를 csv로 변환 String csv = String.format("%s,%s,%d",member.getName(), member.getEmail(), member.getAge()); System.out.println(csv);
- JSON
- 최근에 가장 많이 사용하는 포맷으로 자바스크립트(ECMAScript)에서 쉽게 사용 가능하고,
다른 데이터 포맷 방식에 비해 오버헤드가 적기 때문에 때문에 인기가 많다.{ name: "김배민", email: "deliverykim@baemin.com", age: 25 }
- 자바에서 사용방법
JSON도 물론 이렇게 직접 문자열을 만들일은 거의 없습니다.Member member = new Member("김배민", "deliverykim@baemin.com", 25); // member객체를 json으로 변환 String json = String.format( "", member.getName(), member.getEmail(), member.getAge()); System.out.println(json);
자바에서는 Jackson, GSON 등의 라이브러리를 이용해서 변환할 수 있습니다. (Jackson은 스프링 내장)
- 최근에 가장 많이 사용하는 포맷으로 자바스크립트(ECMAScript)에서 쉽게 사용 가능하고,
출처