Search
Duplicate

Java 직렬화란 무엇인가? (Serializable)

태그
Java
Effective Java
Serializable
Kotlin
IntelliJ
공개여부
작성일자
2021/07/24
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 로 되어 있다면 문제가 없을 듯 하다
BeverageNameSerializable 을 구현하지 않았을 때 에러
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 을 사용한다

구현

직렬화는 ObjectInputStreamObjectOutputStream 을 사용한다.

직렬화

@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
다음엔 직렬화를 하는데 주의해야 할 점을 포스팅 해봐야겠다.

샘플 코드

테스트 코드

직렬화 시리즈

직렬화(Serializ), 역직렬화에 대한 개념 정리

Effective Java 86 Serializable 을 구현할지 신중히 결정하자

Effective Java 87 커스텀 직렬화 형태를 고려해보자

Effective Java 88 readObject 는 방어적으로 작성하라

TOP