Android Story

[ Android ] 객체 직렬화 ObjectInputStream / ObjectOutputStream

WhiteDuck 2016. 1. 19. 10:52

객체 직렬화 

  Java 객체 직렬화는 JDK1.1 때부터 제공된 엄청난 기능으로, Java 인스턴스를 디스크에 저장하거나 네트워크로 전송하기 위해 바이트 배열로 전환하고, 또 역으로 그렇게 저장/ 전송된 바이트 배열을 다시 Java 인스턴스로 전환하는 기술이다. 4 본질적으로, 직렬화라는 개념은 객체 그래프를 얼린(mashalling) 뒤, 디스크나 네트워크 같은 매체로 이동하고, 이동된 정보를 다시 객체 그래프로 해동(unmashalling)하는 과정을 의미한다. 이 모든 과정은 ObjectInputStream/ObjectOutputStream과, 신뢰할 수 있는 메타데이터, 그리고 직렬화하려는 클래스에 Serializable 인터페이스를 구현하도록 한 프로그래머의 의 지에 의해 마술같이 처리된다객체를 분해하여 전송하기 위해 행하는 동작이다. 직렬화는 보통 다음과 같은 진행과정을 거친다.


JunitTest
- SerTest


public class SerTest {

@Test public void serializeToDisk() {

try {

// 1. marshalling

com.tedneward.Person ted = new com.tedneward.Person("Ted", "Neward", 39);

com.tedneward.Person charl = new com.tedneward.Person("Charlotte",

 "Neward", 38);

ted.setSpouse(charl);

charl.setSpouse(ted);

                        // 2. Serialization

FileOutputStream fos = new FileOutputStream("tempdata.ser");

ObjectOutputStream oos = new ObjectOutputStream(fos);

oos.writeObject(ted);

oos.close();

}

catch (Exception ex) {

fail("Exception thrown during test: " + ex.toString());

}

try {

FileInputStream fis = new FileInputStream("tempdata.ser");

ObjectInputStream ois = new ObjectInputStream(fis);

com.tedneward.Person ted = (com.tedneward.Person) ois.readObject();

ois.close();


// 3. unmarshalling

assertEquals(ted.getFirstName(), "Ted");

assertEquals(ted.getSpouse().getFirstName(), "Charlotte");

// Clean up the file

new File("tempdata.ser").delete();

}

catch (Exception ex) {

fail("Exception thrown during test: " + ex.toString());

}

}

}




 1) Marshalling

 마샬링(marshalling)은 데이터를 바이트의 덩어리로 만들어 스트림에 보낼 수 있는 형태로 바꾸는 변환 작업을 뜻한다.

  자바에서 마샬링을 적용할 수 있는 데이터는 원시 자료형(boolean, char, byte, short, int, long, float, double)와 객체 중에서 Serializable 인터페이스를 구현한 클래스로 만들어진 객체이다.


  객체는 원시 자료형과 달리 일정한 크기를 가지지 않고 객체 내부의 멤버 변수가 다르기 때문에 크기가 천차만별로 달라진다. 이런 문제점을 처리할 수 있는게 ObjectOutputStream 클래스이다.

 

 2) 직렬화 (Serialization) :  

 마샬링으로 바이트로 분해된 객체는 스트림을 통해서 나갈 수 있는 준비가 되었다. 앞에서 언급한대로 객체를 마샬링하기 위해서는 Serializable 인터페이스를 구현한 클래스로 만들어진 객체에 한해서만 마샬링이 진행될 수 있다.


Serializable 인터페이스는 아무런 메소드가 없고 단순히 Java Virtaul Machine에게 정보를 전달하는 의미만을 가진다.


* 직렬화가 가능한 객체의 조건

  (1) 기본형 타입(boolean, char, byte, short, int, long, float, double)은 직렬화가 가능

  (2) Serializable 인터페이스를 구현한 객체여야 한다. (Vector 클래스는 Serializable 인터페이스구현)

  (3) 해당 객체의 멤버들 중에 Serializable 인터페이스가 구현되지 않은게 존재하면 안된다.

  (4) transient 가 사용된 멤버는 전송되지 않는다. (보안 변수 : null 전송)


* 직렬화 Tip

  (1) 직렬화는 리팩토링을 허용한다.

 직렬화는 클래스가 어느 정도 변경되거나, 심지어 리팩토링이 되어도 괜찮은데, 웬만하면 ObjectInputStream이 적절하게 처리해 주기 때문이다. Java 객체 직렬화 규격이 자동적으로 처리할 수 있는 변경사항들은 다음과 같다.  클래스에 새로운 필드 추가  static 필드를 non-static 필드로 변경하기  transient 필드를 non-transient 필드로 변경하기 이것 외의 변경, 즉 non-static을 static으로 바꾸거나 non-transient를 transient로 바꾸거 나, 존재하던 필드를 제거하는 등의 작업은 하위 호환성을 위해 추가적인 작업을 수행해 야 한다.


  직렬화는 메소드 명, 필드명, 필드 유형, 접근지시자, 파일명 등 직렬화하려는 소스 파일 에 대한 모든 내용에 기반하여 계산된 해시(hash)값을 이용하고, 이 해시값과 직렬화된 스트림의 해시값을 비교하여 처리한다. Java 런타임이 두 가지 클래스의 유형이 결과적으로 동일한지 알려면, 이 두 번째 (PersonV2) 혹은 그 이후 버전을 Person 클래스들이 원래의 Person 클래스와 동일한 직 렬화 버전 해시값이 적용되어 있어야 한다. 그리고 이 값은 클래스 내에 private static final long serialVersionUID 필드에 저장되도록 되어 있다. 그러므로 우리가 해야 할 일은, serialVersionUID 필드를 선언하고 거기에 특정 해시값을 입력하는 것인데, 이 값은 JDK 의 serialver 명령어를 이용하여 얻을 수 있다. serialVersionUID 값을 얻었고 그 값을 클래스에 적용하였다면, 기존 Person 객체가 직렬 화되어 있다고 하더라도 PersonV2 객체로 역직렬화를 수행할 수 있을 뿐만 아니라 반대 6 직렬화되어 있는 클래스와 역직렬화할 클래스로 직렬화된 PersonV2 객체에서 Person 객체로 역직렬화를 수행할 수도 있게 된다.


  (2) 직렬화는 안전하지 않다. 가끔 Java 개발자들이 직렬화된 이진(binary) 형식의 데이터가 완벽하게 문서화되어 있고, 그것을 바탕으로 온전한 객체를 만들 수 있다는 것에 깜짝 놀라거나 불편해하는 것을 본 적이 있다. 실제로, 직렬화된 이진 스트림을 콘솔창에 출력하는 것 만으로 클래스의 형태 나 저장되어 있는 데이터의 형태를 확인할 수 있다. 이 말은 (직렬화된 데이터가) 보안적으로 문제가 있을 수 있음을 의미한다. RMI8를 이용 하여 원격의 메소드를 호출할 경우, private으로 지정된 필드 내의 값이 평범한 텍스트 형 태 그대로 소켓 스트립에 전송되는 것을 볼 수 있는데, 이는 분명히 가장 간단한 보안 규정을 위반한 하는 것이다. 다행히도 Java 객체 직렬화 기능은 직렬화 과정을 가로채어 직렬화하거나 역직렬화하는 과정에서 특정 필드를 암호화할 수 있도록 하는 기능을 제공한다. 아래에서 설명하겠지 만, 직렬화할 객체에 writeObject 메소드안에 보안 알고리즘을 추가함으로써 이 처리를 수행할 수 있다.


  (3) 직렬화된 데이터를 서명하거나 봉인할 수 있다. 위의 팁에서는 직렬화된 데이터를 암호화하거나 데이터가 변조를 막기 위해 어떤 작업을 하지는 않았고, 단지 노출되는 것을 막기만 하였다. 물론 writeObject나 readObject 메소 드를 이용할 때 암호화 방식이나 서명관리 방식을 이용하여 데이터를 감출 수도 있겠지 만, 이보다 더 좋은 방법이 있다. 만약 전체 객체를 암호화하거나 서명하기를 원한다면 그 객체를 javax.crypto.SealedObject 래퍼 클래스나 java.security.SignedObject 래퍼 클래스 안에 집 어넣기만 하면 된다. 두 클래스 모두 직렬화가 가능하기 때문에 SealedObject 클래스를 이용하여 감싸는 것 만으로 원래 객체를 “선물상자” 안에 넣는 것 같은 효과를 얻을 수 있다. 이 과정에서 암호화 처리를 하기 위해 대칭키가 필요한데, 이 키는 각각 독립적으 로 관리되어야 한다. 비슷한 방식으로 SignedObject를 이용하면 데이터에 대한 검증을 수행할 수 있는데, 마찬가지로 대칭키가 별개로 관리되어야 한다. 이 두 클래스를 같이 사용하면 암

호화하거나 봉인하기 위해 큰 스트레스를 받지 않고 한 꺼번에 데이터를 봉인하고 서명할 수도 있다. 괜찮지 않나?


  (4) Serialization can put a proxy in your stream 가끔은 클래스의 주요 정보들로부터 나머지 부가 정보들이 얻어질 수 있는 경우도 있다. 이럴 경우에는 클래스 전체를 직렬화할 필요가 없다. 물론 이런 부가 정보들을 transient 로 처리할 수도 있지만, 그렇다고 하더라도 각각의 필드에 접근하는 메소드들은 그 필드 에 접근할 때마다 해당 필드가 적절하게 초기화되었는지 명시적으로 확인하는 코드가 추 가되어야 한다. 지금 얘기하고 있는 주제가 직렬화에 대한 것이므로, 이런 경우에는 경량 클래스 (lightweight)나 프록시(proxy)를 이용하여 직렬화하는 것이 더 바람직할 수 있다. 원래 Person 클래스에 writeReplace 메소드를 구현함으로써 다른 종류의 클래스가 직렬화될 수 있도록 할 수 있다. 역으로, 역직렬화 과정에서 readResolve 메소드가 구현되어 있는 것이 확인되면, 이 메소드가 원래의 클래스 형태로 리턴되도록 할 것이다.


프록시를 이용하여 직렬화/역직렬화하기 위에서 설명한 대로, writeReplace 메소드와 readResolve 메소드를 같이 이용하면 Person 클래스의 데이터 중의 일부를 PersonProxy 클래스에 입력하여 직렬화하고, 나중에 역직 렬화할 때 다시 되돌이킬 수 있다.


Proxy Serialization


class PersonProxy

 implements java.io.Serializable {

public PersonProxy(Person orig) {

data = orig.getFirstName() + "," + orig.getLastName() + "," + orig.getAge();

if (orig.getSpouse() != null) {

Person spouse = orig.getSpouse();

data = data + "," + spouse.getFirstName() + "," + spouse.getLastName() + ","

+ spouse.getAge();

}

}

public String data;

private Object readResolve()

throws java.io.ObjectStreamException {

String[] pieces = data.split(",");

Person result = new Person(pieces[0], pieces[1], Integer.parseInt(pieces[2]));

if (pieces.length > 3) {

result.setSpouse(new Person(pieces[3], pieces[4], Integer.parseInt

(pieces[5])));

result.getSpouse().setSpouse(result);

}

return result;

}

}

public class Person

 implements java.io.Serializable {

public Person(String fn, String ln, int a) {

this.firstName = fn;

this.lastName = ln;

this.age = a;

}

public String getFirstName() {

return firstName;

}

public String getLastName() {

return lastName;

}

public int getAge() {

return age;

}

public Person getSpouse() {

return spouse;

}

private Object writeReplace()

throws java.io.ObjectStreamException {

return new PersonProxy(this);

}

public void setFirstName(String value) {

firstName = value;

}

public void setLastName(String value) {

lastName = value;

}

public void setAge(int value) {

age = value;

}

public void setSpouse(Person value) {

spouse = value;

}

public String toString() {

return "[Person: firstName=" + firstName +

" lastName=" + lastName +

" age=" + age +

" spouse=" + spouse.getFirstName() +

"]";

}

private String firstName;

private String lastName;

private int age;

private Person spouse;

}



 (5) 믿더라도 검증해보라11 직렬화되어 이진 형식으로 전달된 데이터가 최초에 직렬화될 때의 스트림 형식 그대로일 것이라고 가정하는 것이 나쁜 것은 아니지만, 전직 미국 대통령이 지적했듯이, “믿더라도 검증해” 보아야 한다. 직렬화된 객체에 대해 역직렬화한 후에는 반드시 해당 값이 적절한 범위 내에 있는지 확 인해야 한다는 말이다. 이를 위해 ObjectInputValidation 인터페이스를 구현하고 validateObject 메소드를 재정의하면 된다. 뭔가 데이터가 잘못된 것처럼 보이면 InvalidObjectException을 던지면 된다.


 3) 언마샬링 (unmarshalling)

 언마샬링은 객체 스트림을 통해서 전달된 바이트 덩어리를 원래의 객체로 복구하는 작업이다. 이 작업을 제대로 수행하기 위해서는 반드시 어떤 객체 형태로 복구할지 형 변환을 정확하게 해주어야 한다.



참고


1) 객체 직렬화 : http://hyeonstorage.tistory.com/252

2) 객체 직렬화 : 

https://www.google.co.kr/url?sa=t&rct=j&q=&esrc=s&source=web&cd=4&ved=0ahUKEwiDlums1rTKAhUFupQKHfBDD5IQFgguMAM&url=http%3A%2F%2Fnoritersand.tistory.com%2Fattachment%2Fcfile30.uf%4026613D375537C8D71B6149.pdf

반응형