1. 직렬화(Serialization)란?
- 자바 직렬화란 자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 데이터 변환하는 기술과바이트로 변환된 데이터를 다시 객체로 변환하는 기술(역직렬화)을 아울러서 이야기합니다.
- 시스템적으로 이야기하자면 JVM(Java Virtual Machine 이하 JVM)의 메모리에 상주(힙 또는 스택)되어 있는 객체 데이터를 바이트 형태로 변환하는 기술과 직렬화된 바이트 형태의 데이터를 객체로 변환해서 JVM으로 상주시키는 형태를 같이 이야기합니다.
참조 : https://techblog.woowahan.com/2550/
2. 직렬화를 하기 위한 Serializable
Serializable 인터페이스를 열어보면 선언된 변수나 메소드가 없는것을 확인할 수 있다.
public interface Serializable {
}
아무런 구현해야 할 메소드 없는 Serializable 인터페이스는 생성한 객체를 파일로 저장하거나, 저장한 객체을 읽을 필요가 있을때 사용한다.
개발자가 작성한 클래스가 파일에 읽거나 쓸 수 있도록 하거나, 다른 서버로 보내거나 받을 수 있도록 하려면 반드시 Serializable 인터페이스를 구현해야 한다.
먼저 Serializable 인터페이스를 구현하는 DTO 클래스를 생성한다.
public class SerialDto implements Serializable {
private String id;
private String pw;
private String name;
private int phoneNum;
private boolean vip;
private long point;
public SerialDto(String id, String pw, String name, int phoneNum, boolean vip, long point) {
this.id = id;
this.pw = pw;
this.name = name;
this.phoneNum = phoneNum;
this.vip = vip;
this.point = point;
}
@Override
public String toString() {
return "[id = " + id + ", pw = " + pw + ", name = " + name +
", phoneNum = " + phoneNum + ", vip = " + vip + ", point = " + point + " ]";
}
}
다음으로 파일을 저장하고 읽는 메소드와 Main() 메소드를 생성한다.
public class ManageObject {
public static void main(String[] args) {
//파일 저장 경로
String path = "C:"+File.separator+"Users"+File.separator+"ksw"+File.separator+"IdeaProjects"+File.separator+"objtest"+File.separator+"serial.obj";
//경로에 저장될 객체
SerialDto dto = new SerialDto("aaa","123","cool",11112222,true,2000L);
ManageObject mo = new ManageObject();
mo.saveObj(path,dto);
//mo.loadObject(path);
}
public void saveObj(String path,SerialDto dto){
FileOutputStream fos = null;
ObjectOutputStream oos = null;
try {
fos = new FileOutputStream(path);
oos = new ObjectOutputStream(fos);
oos.writeObject(dto);
System.out.println("Object Write Success");
}catch (Exception e){
e.printStackTrace();
}finally {
if (oos != null){
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public void loadObject(String path){
FileInputStream fis = null;
ObjectInputStream ois = null;
try {
fis = new FileInputStream(path);
ois = new ObjectInputStream(fis);
SerialDto dto = (SerialDto)ois.readObject();
System.out.println(dto);
}catch (Exception e){
e.printStackTrace();
}finally {
if (ois != null){
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
saveObj() 메소드 부터 살펴보면 String형 경로와, SerialDto타입 객체를 매개변수로 받아
FileOutputStream 클래스로 저장할 파일 경로를 생성하고 ObjectOutputStream 클래스로 파일에 객체를 저장한다.
실행화면을 보면 에러없이 잘 컴파일 된것을 확인할 수 있다.
이제 Main() 내부의 loadObject() 메소드의 주석을 풀고 String형 경로를 매개변수로 받는 loadObject()에서
FileInputStream 클래스로 파일 경로를 읽고, ObjectInputStream 클래스로 객체를 읽어온다.
그다음 readObject() 메소드를 사용해서 ois에 담긴 객체를 형변환 하여 SerialDto 타입 객체에 담아준다.
toString() 메소드를 실행시켜보면
SerialDto 클래스에 Overriding 했던 toString() 메소드가 실행되는 것을 확인할 수 있다.
이번엔 SerialDto 클래스에 멤버변수 하나를 추가하고 컴파일 해보자
public class SerialDto implements Serializable {
private String id;
private String pw;
private String name;
private int phoneNum;
private boolean vip;
private long point;
private String memeberType;
public SerialDto(String id, String pw, String name, int phoneNum, boolean vip, long point) {
this.id = id;
this.pw = pw;
this.name = name;
this.phoneNum = phoneNum;
this.vip = vip;
this.point = point;
}
@Override
public String toString() {
return "[id = " + id + ", pw = " + pw + ", name = " + name +
", phoneNum = " + phoneNum + ", vip = " + vip + ", point = " + point +" ]";
}
}
컴파일시 에러메세지와 함께 제일 첫줄에
stream classdesc serialVersionUID = 7928359776344400683, local class serialVersionUID = -3178177334575669092
라는 serialVersionUID가 다르다는 InvalidClassException 예외 메시지가 출력된다.
이는 Serializable 객체의 형태가 변경되면 컴파일시에 serialVersionUID가 다시 생성되므로 이와같은 문제가 발생한다.
serialVersionUID라는 값이 사용되는 예를 들면
- A 서버에 B 서버로 SerialDto 클래스를 전송한다고 가정하자
- 전송하는 A 서버에도 SerialDto 클래스가 있어야 하고, 전송받는 B 서버에도 SerialDto 클래스가 있어야만 한다.
- 하지만 A 서버의 SerialDto 클래스에는 변수가 2개 B 서버의 SerialDto 클래스에는 변수가 3개 있다면 제대로 처리하지 못하는 상황이 발생한다.
- 각 서버가 쉽게 해당 객체가 같은지 다른지를 식별하기 위해서 serialVersionUID 값으로 관리해준다.
- 즉, 이름이 같은 클래스여도 serialVersionUID가 다르면 다른 클래스로 인식한다.
따라서 저상황을 해결하려면 Serializable 객체(SerialDto 객체)에 serialVersionUID를 지정해주면 된다.
public class SerialDto implements Serializable {
static final long serialVersionUID = 1L;
private String id;
private String pw;
private String name;
private int phoneNum;
private boolean vip;
private long point;
private String memeberType;
public SerialDto(String id, String pw, String name, int phoneNum, boolean vip, long point) {
this.id = id;
this.pw = pw;
this.name = name;
this.phoneNum = phoneNum;
this.vip = vip;
this.point = point;
}
@Override
public String toString() {
return "[id = " + id + ", pw = " + pw + ", name = " + name +
", phoneNum = " + phoneNum + ", vip = " + vip + ", point = " + point +" ]";
}
}
serialVersionUID 지정해주고 Main() 메소드 내부에서 saveObj(), loadObj() 메소드를 사용해보자.
serialVersionUID가 지정된 상태로 다시한번 SerialDto 클래스를 수정하고 컴파일을 해보면
public class SerialDto implements Serializable {
static final long serialVersionUID = 1L;
private String id;
private String pw;
private String name;
private int phoneNum;
private boolean vip;
private long point;
private String memeberTypes;
public SerialDto(String id, String pw, String name, int phoneNum, boolean vip, long point, String memeberType) {
this.id = id;
this.pw = pw;
this.name = name;
this.phoneNum = phoneNum;
this.vip = vip;
this.point = point;
this.memeberTypes = memeberType;
}
@Override
public String toString() {
return "[id = " + id + ", pw = " + pw + ", name = " + name +
", phoneNum = " + phoneNum + ", vip = " + vip + ", point = " + point +", memberTypes = "+memeberTypes+" ]";
}
}
memberType 변수를 memberTypes로 바꾸고 컴파일을 하면 memberType 는 null값이 출력된다.
serialVersionUID 를 지정하면 예외는 발생하지 않지만 데이터가 꼬일 수 있기 때문에 데이터가 바뀌면 serialVersionUID를 변경해주어야 한다.
3. transient 예약어
SerialDto 클래스의 pw 변수와 phoneNum 변수에 transient 예약어를 추가하자
public class SerialDto implements Serializable {
static final long serialVersionUID = 1L;
private String id;
transient private String pw;
private String name;
transient private int phoneNum;
private boolean vip;
private long point;
private String memeberType;
public SerialDto(String id, String pw, String name, int phoneNum, boolean vip, long point, String memeberType) {
this.id = id;
this.pw = pw;
this.name = name;
this.phoneNum = phoneNum;
this.vip = vip;
this.point = point;
this.memeberType = memeberType;
}
@Override
public String toString() {
return "[id = " + id + ", pw = " + pw + ", name = " + name +
", phoneNum = " + phoneNum + ", vip = " + vip + ", point = " + point +", memberTypes = "+memeberType+" ]";
}
}
pw는 null이 phoneNum는 0이 출력되는것을 볼 수 있다.
transient 예약어는 객체를 저장하거나 다른 JVM으로 보낼 때, 선언한 변수는 Serializable의 대상에서 제외된다.
해당 객체를 생성한 JVM에서는 사용하는데 문제가 없다. 위의 예시처럼 패스워드나 전화번호등 보안상 중요한 변수나 저장할 필요가 없는 변수들은 transient 예약어를 사용할 수 있다.
'JAVA' 카테고리의 다른 글
객체지향 프로그래밍, 절차지향 프로그래밍 (0) | 2022.01.14 |
---|---|
Socket 을 활용한 네트워크 프로그래밍 (0) | 2022.01.03 |
자바 NIO (0) | 2021.12.30 |
컬렉션 프레임워크(Collection Framework) (4) - 해시테이블(Hashtable), 해시맵(HashMap) (1) | 2021.12.28 |
컬렉션 프레임워크(Collection Framework) (3) - Map (0) | 2021.12.28 |
컬렉션 프레임워크(Collection Framework) (2) - Set & Queue (0) | 2021.12.27 |