본문 바로가기
프로그래밍 놀이터/디자인 패턴, 리펙토링

[Effective Java] Serializable 인터페이스를 분별력 있게 구현하자.

by 돼지왕 왕돼지 2017. 3. 21.
반응형

 [Effective Java] Serializable 인터페이스를 분별력 있게 구현하자.


API, BigInteger, binary compatibility, component, Date, default value, deserializing, Effective JAVA, enclosing instance, extralinguistic mechanism, gui component, httpservlet, implements serializable, inner class, Interface, invalidclassexception, invalidobjectexception, jvm, object serialization, ObjectInputStream, ObjectOutputStream, Private, Public, putfields, readfields, readobjectnodata, Remote Method Invocation, rmi, semantic compatibility, serial version, Serializable, Serialization, serializing, serialVersionUID, SESSION, static final long, static 맴버 클래스, stream unique identifier, synthetic field, throwable, UID, vm, [Effective Java] Serializable 인터페이스를 분별력 있게 구현하자., 감춰진 생성자, 값 생성 참여, 값 클래스, 객체, 객체 내부, 객체 생성, 객체 전송, 객체 직렬화, 결함, 고유 식별 번호, 고품질 직렬화 형태, 구현, 긴 기간, 내부 클래스, 노력, 대응 필드, 디스크 저장, 디코딩, 디폴트 값, 런타임, 매개 변수, 매개 변수가 없고 접근 가능한 생성자, 매개변수, 맴버, 명시적 생성자, 바이트 스트림, 방치, 번호, 보안상 허점, 복구, 복원, 복잡한 절차, 부담, 부담 증대, 불법적 접근, 불변 규칙, 불변성 손상, 사용 어렵고, 상속을 위해 설계된 클래스, 새 버전의 클래스 배포, 생성 중, 생성자, 서버, 서브 클래스, 선언부, 소스 코드 지저분, 수퍼 클래스, 스레드 풀, 시리얼라이져블, 시스템, 신중하게 설계, 실제 비용, 실질적인 이점, 언어 영역을 벗어나는 방식, 역직렬화, 영속성, 영향, 외곽 인스턴스, 외부 공격자, 원격 통신, 유지보수, 유효범위, 인코딩, 인터페이스, 인터페이스 이름, 자동 생성, 자동화 테스트, 저장, 전송, 접근, 접근 가능, 접근 가능한 생성자, 정의 불분명, 주의 사항, 증대, 지역 변수들의 값, 직렬화, 직렬화 버전, 직렬화 버전 uid, 직렬화 수반, 직렬화 프록시, 참조, 초기 개발 비용, 초기화, 캐싱, 컴파일러, 컴포넌트, 클라이언트, 클래스 내부 구현, 클래스 이름, 클래스 진화, 테스트 자동화, 표준 통신 회선 수준 객체 표현, 프레임워크, 호환성, 확장, 활동적인 개체




-
객체 직렬화(object serialization) API 는
객체를 바이트 스트림으로 인코딩하고, 인코딩된 바이트 스트림으로부터 객체를 복원(디코딩) 하는 프레임워크이다.


-
객체를 바이트 스트림으로 인코딩하는 것을 직렬화(serializing)이라 하고,
그 반대의 절차를 역직렬화(deserializing)이라고 한다.


-
객체가 일단 직렬화되면, 인코딩된 객체는 향후에 역직렬화 하기 위해 하나의 실행 중인 VM 에서 다른 VM 으로 전송되거나 디스크에 저장될 수 있다.
직렬화는 원격 통신을 위한 표준 통신 회선 수준의 객체 표현을 제공한다.
직렬화 프록시는 effective java 의 직렬화 주제중 가장 주목할만한 내용이며, 직렬화에 대한 많은 함정을 피할 수 있게 해준다.


-
클래스의 인스턴스가 직렬화될 수 있게 하려면 implements Serializable 을 클래스 선언부에 추가하면 된다.
클래스를 직렬화 가능하도록 만드는 것은 장기간에 걸친 유지보수 측면에서 비용이 상당히 들어간다.


-
만일 기본 직렬화 형태를 사용하고 나중에 그 클래스의 내부 구현을 변경한다면, 변경으로 인해 직렬화 형태가 호환되지 않을 수 있다.
ObjectOutputStream.putFields 와 ObjectInputStream.readFields 를 사용하면 원래의 직렬화 형태를 유지하면서 클래스 내부 구현을 변경 할 수 있다.
그러나 그 방법은 사용이 어렵고 소스 코드가 지저분해진다.
그러므로 긴 기간 동안 사용할 수 있는 고품질의 직렬화 형태를 신중하게 설계해야 한다.
초기 개발 비용은 더 들어가겠지만 그런 노력을 할만한 가치는 있다.


-
직렬화를 수반하면서 클래스 진화를 제약하는 간단한 예로 직렬화 버전(serial version) UID 라고 더 많이 알려진 스트림 고유 식별자(stream unique identifier)가 있다.
직렬화가 가능한 모든 클래스는 고유 식별 번호를 갖는다.
만일 static final long 필드인 serialVersionUID 를 명시적으로 선언하여 그 번호를 지정하지 않으면, 시스템에서 해당 클래스에 대해 복잡한 절차를 적용하여 런타임 시에 자동으로 생성한다.
이 때 그 클래스 이름, 그 클래스가 구현하는 인터페이스들의 이름, 그 클래스의 모든 public 과 private 맴버들이 자동으로 값을 생성할 때 영향을 준다.
만일 그것들 중 어느 것이라도 변경하면 자동으로 생성된 직렬화 버전 UID 가 변경된다.
명시적으로 직렬화 버전 UID 를 선언하는데 실패하면 클래스의 내부 구현 변경 시 호환성이 깨질 것이고 런타임 시에 InvalidClassException 예외 발생을 초래할 것이다.


-
Serializable 을 구현하는데 들어가는 두 번째 비용은 결함과 보안상의 허점을 증대시킨다는 데 있다.
일반적으로 객체는 생성자를 사용해서 생성된다.
그러나 직렬화는 언어 영역을 벗어나는 방식(extralinguistic mechanism)으로 객체를 생성한다.
기본적인 JVM 이 수행하는 deserializing 을 사용하건 또는 독자적인 deserializing 을 사용하건, 역직렬화를 할 때는 "감춰진 생성자"가 사용된다.
역직렬화와 연관되는 명시적 생성자가 없기 때문에 감춰진 생성자에서 모든 불변 규칙이 지켜지는지 확인해야 한다.
또 생성 중인 객체의 내부를 외부 공격자가 접근하지 못하도록 하는지를 확인해야 한다.
기본적인 역직렬화 메커니즘에 의존하면 불변성의 손상이나 불법적인 접근에 객체를 그대로 방치하는 꼴이 될 수 있다.


-
Serializable 을 구현하는데 들어가는 세 번째 비용은 새 버전의 클래스 배포와 관련해서 부담을 증대시킨다.
직렬화 가능한 클래스가 개정될 때는 새 버전의 인스턴스를 직렬화해서 구 버전의 인스턴스로 역직렬화 할 수 있는지, 또는 그 반대로 가능한지를 확인하는 것이 중요하다.
이런 테스트는 자동화하기도 어렵다.
binary compatibility 에 덧붙여 semantic compatibility 도 테스트해야 하기 때문이다.


-
Serializable 인터페이스의 구현은 쉽게 결정할 일이 아니다.
직렬화는 실질적인 이점을 제공하며, 객체의 전송이나 영속성을 직렬화에 의존하는 프레임워크와 관계되는 클래스라면 필수적이다.
또한 Serializable 을 반드시 구현해야 하는 다른 클래스의 컴포넌트로써 클래스를 굉장히 쉽게 사용할 수 있다.
그러나 실제 비용을 꼭 따져야 한다.


-
Date BigInteger 와 같은 값 클래스들은 대부분의 컬렉션 클래스들이 그러하듯 반드시 Serializable 을 구현해야 하며, 스레드 풀과 같은 활동적인 개체를 나타내는 클래스들은 Serializable 을 구현할 필요가 없다.
상속을 위해 설계된 클래스들은 Serializable 을 구현할 필요가 없으며, 그런 목적의 인터페이스 역시 Serializable 을 확장할 필요가 없다.
이 규칙을 지키지 않으면 Serializable 을 구현하는 클래스로부터 상속받거나 또는 Serializable 을 확장하는 인터페이스를 구현하는 누군가에게 엄청난 부담을 주게 된다.


-
Serializable 을 구현하면서 상속을 위해 설계된 클래스에는 Throwable, Component, HttpServlet 이 포함된다.
Throwable 이 Serializable 을 구현하는 이유는 RMI (remote method invocation ) 시 발생된 예외를 서버로부터 클라이언트에게 전달해야 하기 때문.
Component 는 GUI 컴포넌트들이 전송, 저장, 복구될 수 있어야 하기 때문이고,
HttpServlet 은 세션 상태를 캐싱 할 수 있어야 하기 때문이다.


-
직렬화 가능하고 확장 가능하면서 인스턴스 필드들을 갖는 클래스를 만들 때는 반드시 알아두어야 할 주의 사항이 있다.
만일 그 클래스의 인스턴스 필드들이 디폴트 값으로 초기화되는 경우 그 클래스의 불변 규칙이 깨진다면
readObjectNoData 메소드를 그 클래스에 반드시 추가해야 한다.

private void readObjectNoData() throws InvalidObjectException{

throw new InvalidObjectException("Stream data required" );

}






-
클래스에서 Serializable 을 구현하지 않는다는 결정을 할 경우 주의 사항이 하나 있다.
만일 상속을 위해 설계된 클래스가 직렬화 될 수 없다면, 그 클래스의 서브 클래스도 직렬화 될 수 없다.
특히, 접근 가능하면서 매개 변수가 없는 생성자를 수퍼 클래스가 제공하지 않는다면 불가능하다.
따라서 상속을 위해 설계된 직렬화가 불가능한 클래스에도 매개 변수 없는 생성자의 제공을 고려해야 한다.

자신은 직렬화 불가능하지만 직렬화 가능한 서브 클래스를 둘 수 있는 매개 변수 없는 생성자를 추가하는 방법은 다음과 같다.

// 자신은 직렬화 불가능하지만, 직렬화 가능한 서브 클래스를 허용한다.

public abstract class AbstractFoo{

protected int x, y; // 인스턴스 상태


public AbstractFoo( int x, int y ){

initialize( x, y );

}


// 다음의 생성자와 메소드는 서브 클래스의 readObject 메소드에서 인스턴스 초기화를 할 수 있게 한다.

protected AbstractFoo(){ }


protected final void initialize( int x, int y ){

// TODO initialize 되었는지 check 로직 필요

this.x = x;

this.y = y;

}

}


public class Foo extends AbstractFoo implements Serializable{


private static final long serialVersionUID = 18238912381L;


private void readObject(ObjectInputStream s) throws IOException, CLassNotFoundException{

s.defaultReadObject();


int x = s.readInt();

int y = s.readInt();

initialize( x, y );

}


private void writeObject(ObjectOutputStream s) throws IOException{

s.defaultWriteObject();


s.writeInt(x);

s.writeInt(y);

}


// 이 녀석은 직렬화 메커니즘에서 사용되지 않는다

public Foo( int x, int y ){

super( x, y );

}

}




-
내부 클래스(Inner Class) 에는 Serializable 을 구현하면 안 된다.
내부 클래스는 컴파일러가 생성한 대용 필드(synthetic field)를 사용하는데, 이 필드에는 외곽 인스턴스(enclosing instance)에 대한 참조와 외곽 인스턴스의 유효범위에 있는 지역 변수들의 값을 저장한다.
그러므로 내부 클래스의 기본 직렬화 형태는 정의가 불분명하다.
그러나 static 맴버 클래스는 Serializalbe 을 구현할 수 있다.



Summary


Serializable 인터페이스의 구현이 쉽다는 것은 허울이다.
클래스를 잠깐만 쓰고 버릴 거라면 몰라도, 그렇지 않다면 Serializable 의 구현은 신중하게 결정해야 한다.
상속을 위해 설계된 클래스는 특별한 주의가 필요하다.
그런 클래스의 경우 서브 클래스에서 Serializable 을 구현할 수 있게 하는 것과 구현을 막는 것을 절충하는 설계관점이 필요한데, 그것은 매개 변수가 없고 접근 가능한 생성자를 제공하는 것이다.
그리고 그렇게 함으로써 서브 클래스에서 Serializable 을 구현할 수 있다.




반응형

댓글