Java 직렬화란?
- 직렬화: 자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 데이터 변환하는 기술
- Java Object -> Byte Array -> File or DB or Network
- 역직렬화: 바이트로 변환된 데이터를 다시 객체로 변환하는 기술
- File or DB or Network -> Byte Array -> Java Object
-
즉, JVM에의 메모리(힙 또는 스택)에 있는 객체 데이터를 바이트 형태로 변환하는 기술과 데이터를 객체로 변환하여 JVM으로 상주시키는 형태
- serialVersionUID
- serialVersionUID값을 저장할 때 클래스 버전이 맞는지 확인하기 위한 용도입니다.
- 직렬화: Java Object -> Byte Array로 변환 당시 serialVersionUID 도 저장
- 역직렬화: Byte Array -> Java Object로 변환 당시 serialVersionUID 체크, 다른 경우 Exception 발생
- 만약 직렬화할 때 사용한 serialVersionUID의 값과 역직렬화 하기 위해 사용했던 serialVersionUID값이 다르다면 InvalidClassException이 발생할 수 있습니다.
- serialVersionUID값을 저장할 때 클래스 버전이 맞는지 확인하기 위한 용도입니다.
- InvalidClassException 발생
- 클래스의 Serial 버전이 다른 경우
- 알 수 없는 데이터 타입을 포함한 경우
- 기본 생성자가 없는 경우
직렬화 사용방법
- Serializable을 구현하여 사용한다.
public class Member implements Serializable{ private String name; private int age; public Member() {} public Member(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { // TODO Auto-generated method stub return String.format("Member name: %s, age: %s", this.name, this.age); } }
- 직렬화 및 역직렬화
public static void main(String[] args) { Member memberTest01 = new Member("TEST01", 10); Member memberTest02 = new Member("TEST02", 20); byte[] serializedMember = null; // 직렬화 try(ByteArrayOutputStream baos = new ByteArrayOutputStream() ) { try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { oos.writeObject(memberTest01); oos.writeObject(memberTest02); serializedMember = baos.toByteArray(); } } catch(IOException e) { e.printStackTrace(); } // 역직렬화 try(ByteArrayInputStream bais = new ByteArrayInputStream(serializedMember) ) { try (ObjectInputStream ois = new ObjectInputStream(bais)) { Member returnMemberTest01 = (Member)ois.readObject(); Member returnMemberTest02 = (Member)ois.readObject(); System.out.println(returnMemberTest01); System.out.println(returnMemberTest02); } } catch(IOException e) { e.printStackTrace(); } }
- 출력 값
Member name: TEST01, age: 10 Member name: TEST02, age: 20
- 출력 값
- 파일에 쓰고(직렬화) 읽어오기(역직렬화)
public static void main(String[] args) { Member memberTest01 = new Member("TEST01", 10); Member memberTest02 = new Member("TEST02", 20); // 직렬화 try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testMember.txt"))) { oos.writeObject(memberTest01); oos.writeObject(memberTest02); } catch(IOException e) { e.printStackTrace(); } // 역직렬화 try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testMember.txt"))) { Member returnMemberTest01 = (Member)ois.readObject(); Member returnMemberTest02 = (Member)ois.readObject(); System.out.println(returnMemberTest01); System.out.println(returnMemberTest02); } catch(IOException e) { e.printStackTrace(); } catch(ClassNotFoundException e) { e.printStackTrace(); } }
- 파일에 쓰인 내용
ы sr "com.naver.exam.serializable.MemberV?3봎~ I ageL namet Ljava/lang/String;xp t TEST01sq ~ t TEST02
- 읽어온 내용(console)
Member name: TEST01, age: 10 Member name: TEST02, age: 20
- 파일에 쓰인 내용
serialVersionUID 가 없을 때, 무슨 문제가 발생할까?
- 위 예제에서 serialVersionUID 를 작성하지 않아도 정상 동작했다.
- 하지만 요구사항이 추가되어 Member에 address 인스턴스 변수를 추가했다.
public class Member implements Serializable{ private String name; private int age; private String address; public Member() {} public Member(String name, int age) { this.name = name; this.age = age; } public Member(String name, int age, String address) { this.name = name; this.age = age; this.address = address; } @Override public String toString() { // TODO Auto-generated method stub return String.format("Member name: %s, age: %s, address: %s", this.name, this.age, this.address); } }
- 변경 전에 저장되어 있던 testMember.txt에서 읽어오면 address는 null로 제대로 읽어올까?
// 역직렬화 try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testMember.txt"))) { Member returnMemberTest01 = (Member)ois.readObject(); Member returnMemberTest02 = (Member)ois.readObject(); System.out.println(returnMemberTest01); System.out.println(returnMemberTest02); } catch(IOException e) { e.printStackTrace(); } catch(ClassNotFoundException e) { e.printStackTrace(); }
- 하지만, java.io.InvalidClassException 가 발생하며 제대로 읽어오지 못했다.
- 이유
- serialVersionUID를 명시하지 않는 경우 클래스의 기본 해쉬값을 사용한다.
- 메소드, 변수 추가-삭제, 타입 변경등 클래스가 변경되는 경우 serialVersionUID가 변경되기 때문에 일치하지 않는 클래스로 인식하여 InvalidClassException이 발생
결론
- serialVersionUID를 명시하고 관리하여 해당 이슈를 해결해야 한다.
- 변경이 잦은 클래스 정보에 경우 직렬화를 사용하지 않는 것이 좋다.
- 기본적으로 직렬화시에 기본적으로 타입에 대한 정보 등 클래스의 메타 정보도 갖고 있기 때문에 상대적으로 다른 포맷에 비해 용량이 크다.
** 참고[직렬화]: 우아한형제들, 자바 직렬화, 그것이 알고싶다. 훑어보기편
** 참고[직렬화]: 우아한형제들, 자바 직렬화, 그것이 알고싶다. 실무편
** 참고[serialVersionUID]: https://ktko.tistory.com/entry/JAVA-%EA%B0%9D%EC%B2%B4%EC%9D%98-%EC%A7%81%EB%A0%AC%ED%99%94Serializable-serialVersionUID
** 참고[InvalidClassException]: https://tomining.tistory.com/80