Effective java 에서 직렬화를 최대한 사용하지 말라는 내용이 있었으나 팀에선 Serializable 을 구현한 클래스를 매우 자주 보게 되었다. 가장 큰 이슈는 보안에 관련한 문제였고, 이것을 명확히 알고 사용한다면 상당히 유용하다고 하여 알아보도록 한다.
직렬화: 데이터를 Stream 으로 전송할 수 있는 상태로 만든다 (ex. 파일)
역직렬화: Stream 으로 전송 받은 데이터를 객체로 만든다
직렬화의 전제 조건은 Serializable 을 구현하는 것이다.
public class Beverage implements Serializable {
}
Java
복사
음료 라는 class 는 직렬화가 가능하다
public class Coffee extends Beverage {
}
Java
복사
직렬화 클래스를 상속받으면 해당 클래스도 직렬화가 가능해진다.
IntelliJ 에서 serialVersionUID 생성하기
serialVersionUID 를 편하게 만들고 싶다면 preference → inspection → Serializable class without 'serialVersionUID' 를 체크하고
클래스 이름에 커서를 갖다 두면 이러한 메세지 창이 나오는데 Add 'serialVersionUID' field 를 클릭하면 된다.
serialVersionUID 필드를 생성했다.
직렬화 대상에서 제외하기 (transient)
보안상의 이슈, 혹은 직렬화로 보여줄 필요가 없는 내부 정보를 숨길 때 사용한다.
HashMap 내부에 숨겨진 field 들
실제로 HashMap 을 사용하면서 이 값들을 직접 제공하는 일은 없다. 모두 public API 로 제공하거나 내부적으로 error 을 확인하여 fail fast 용도로 사용한다.
다른 객체를 member 로 갖고 있다면
public class Beverage implements Serializable {
private static final long serialVersionUID = -2869694270928466514L;
private transient String partnerName;
private LocalDateTime orderAt;
private BeverageName name;
private BeverageSize size;
}
Java
복사
음료에 1급 객체로 객체를 표현한다
•
직렬화가 불가능한 경우: 멤버 변수 중 Serializable 을 구현한 것이 없다면 안된다.
◦
단, transient 로 되어 있다면 문제가 없을 듯 하다
BeverageName 에 Serializable 을 구현하지 않았을 때 에러
•
enum 필드는 상관 없이 수행된다.
•
참조하는 객체가 Serializable 을 구현하고 있는지 확인할 필요가 있다.
참고(HashMap 의 직렬화)
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
}
Java
복사
직렬화에 사용하는 writeObject, ObjectOutputStream 을 사용한다
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
Java
복사
역직렬화에 사용하는 readObject ObjectInputStream 을 사용한다
구현
직렬화는 ObjectInputStream 과 ObjectOutputStream 을 사용한다.
직렬화
@Test
@DisplayName(value = "직렬화 과정을 수행한다")
@Order(value = 0)
void serializeTest() throws FileNotFoundException {
FileOutputStream fos = new FileOutputStream(FILE_PATH);
BufferedOutputStream bos = new BufferedOutputStream(fos);
try (ObjectOutputStream out = new ObjectOutputStream(bos)) {
Beverage juice = new Beverage(new BeverageName("orange juice"), BeverageSize.TALL);
Beverage soda = new Beverage(new BeverageName("soda"), BeverageSize.VENTI);
List<Beverage> beverageList = new ArrayList<>();
beverageList.add(juice);
beverageList.add(soda);
out.writeObject(juice);
out.writeObject(soda);
out.writeObject(beverageList);
} catch (Exception e) {
e.printStackTrace();
}
}
Java
복사
테스트 코드로 직렬화를 하여 file 을 만들어본다.
역직렬화
@Test
@DisplayName(value = "역직렬화를 수행한다")
@Order(value = 1)
void deserialize() throws FileNotFoundException {
FileInputStream fis = new FileInputStream(FILE_PATH);
BufferedInputStream bis = new BufferedInputStream(fis);
try(ObjectInputStream in = new ObjectInputStream(bis)) {
Beverage beverage1 = (Beverage)in.readObject();
Beverage beverage2 = (Beverage)in.readObject();
List<Beverage> list = (List<Beverage>) in.readObject();
System.out.println(beverage1.toString());
System.out.println(beverage2.toString());
System.out.println("count :: " + list.size());
System.out.println(list);
assertThat(beverage1.beverageName()).isEqualTo("orange juice");
assertThat(beverage1.beverageSize()).isEqualTo(BeverageSize.TALL);
assertThat(beverage2.beverageName()).isEqualTo("soda");
assertThat(beverage2.beverageSize()).isEqualTo(BeverageSize.VENTI);
assertThat(list.size()).isEqualTo(2);
} catch (Exception e) {
e.printStackTrace();
}
}
Java
복사
직렬화로 만든 파일을 객체로 만들어본다
실행 결과
생성된 파일에 기록된 내용은 이러하다
��sr-me.yevgnenll.concurrent.serializable.Beverage�,�C�UͮLnamet3Lme/yevgnenll/concurrent/serializable/BeverageName;LorderAttLjava/time/LocalDateTime;Lsizet3Lme/yevgnenll/concurrent/serializable/BeverageSize;xpsr1me.yevgnenll.concurrent.serializable.BeverageName��ۨ��;LnametLjava/lang/String;xptorange juicesr
java.time.Ser�]��"H�xpw�,7$�@x~r1me.yevgnenll.concurrent.serializable.BeverageSizexrjava.lang.EnumxptTALLsq~sq~tsodasq~ w�,7$z�x~q~tVENTIsrjava.util.ArrayListx����a�Isizexpwq~q~x
Plain Text
복사
다음엔 직렬화를 하는데 주의해야 할 점을 포스팅 해봐야겠다.