IT/Programming / / 2023. 4. 25. 15:47

<Effective Java> RULE 74 Serializable 인터페이스를 구현할 때는 신중하라

반응형

 

클래스 선언부에 “implements Serializable”만 붙히면 직렬화 가능한 객체를 만드는 클래스를 구현할 수 있을 때도 있다.

· 직렬화 가능 클래스를 만들기 위해서 당장해야할 것은 적지만, 장기적으로 해야할 일은 많다.

 

Serializable 인터페이스를 구현하면 어떻게 되나?

클래스를 릴리즈하고 나면 클래스 구현을 유연하게 바꾸기 어려워진다.

· 구현하고 나면 그 클래스의 바이트 스트림 인코딩(직렬화 형식)도 공개 API의 일부가 되어 버린다.

· 널리 배포된 클래스의 직렬화 형식은 일반적으로는 영원히 지원해야 한다.

· 기본 직렬화 형식을 받아 들이면 그 클래스의 private와 package-private객체 필드도 공개 API가 된다.

ㄴ 정보 은닉을 위해 필드 접근 권한을 최소화 해야한다는 원칙규칙13은 효력을 잃게 된다.

· 따라서 오랫동안 사용할 수 있는 고품질 직렬화 형식을 주의 깊게 설계해야 한다.

 

버그나 보안 취약점이 발생할 가능성이 높아진다.

· 직렬화는 언어 외적인 객체 생성 메커니즘이다.

· 생성자가 만족하는 모든 불변식을 보장해야한다. 그리고 생성중인 객체의 내부에 공격자가 접근할 수 없도록 해야 한다.

· 기본 역질렬화 메커니즘을 그래도 사용할 경우, 객체는 불변식 훼손이나 불법 접근 문제에 쉽게 노출된다.

 

새 버전 클래스를 내놓기 위한 테스트 부담이 늘어난다.

· 직렬화 가능 클래스를 수정할 때는 새 릴리즈에서 만들고 직렬화환 객체를 예전 릴리즈에서 역직렬화 할 수 있는지, 그리고 그 역도 가능하는지 검사를 해야한다.

· 테스트양은 직렬화 가능 클래스의 수에 릴리즈 수를 곱한 값에 비례하며 굉장히 커질 수 있다.

ㄴ 이진 호환성 외에 의미 호환성 테스트도 해야한다.

ㄴ 직렬화-역직렬화가 정상적으로 이뤄지는지 확인해야할 뿐 아니라, 그 결과로 만들어지는 객체가 원래 객체의 충실한 사본인지도 확인해야 한다.

 

Serializable 인터페이스의 특징?

직렬화 버전 UID 스트림 고유 식별자는 직렬화가 어떻게 클래스 진화를 가로막는지 보여주는 사례이다.

· 자동으로 생성되는 식별번호는 클래스 이름, 해당 클래스가 구현하는 모든 인터페이스의 이름, public & protected멤버에 영향을 받는다.

· 이중 하나라도 바뀌면 UID값이 바뀌게 된다.

 

직렬화인터페이스를 구현한다는것은 가벼운 작업이 아니다.

· 직렬화 인터페이스를 구현하면 실질적인 혜택이 있다.

ㄴ 직렬화를 통해 객체를 전송하거나 지속성 매체에 저장하는 프레임워크에 사용할 클래스라면 직렬화는 필수다.

ㄴ 기본적인 규칙은 Date나 BigInteger같은 클래스는 직렬화를 구현해야 한다는것이다.

ㄴ 스레드풀과 같은 활성 객체를 나타내는 클래스는 직렬화를 구현할 일이 거의 없다.

 

그럼 어떻게 해야하나?

계승에 염두에 두고 설계하는 클래스는 직렬화는 구현하지 않는 것이 바람직하다. 또한 인터페이스는 가급적 직렬화를 계승하지 말아야 한다.

· 계승을 고려해 설계된 클래스 중 직렬화를 구현한 것은 Throwable, Component, HttpServlet등이 있다.

 

주의사항?

객체 필드를 갖는 클래스를 직렬화 가능하고 계승 가능한 클래스로 구현할 때 주의사항

· 객체 필드가 기본값으로 초기화 되면 위배되는 불변식이 있는 경우에 아래 readObjectNoData 메소드를 클래스에 반드시 추가해야 한다.

// 상태 유지 계승 가능 직렬화 가능 클래스에 대한 readObectNoData 메소드
private void readObjectNoData() throws InvalidObjectExeception{
    throw new InvalidObjectException("Stream data required");
}
 

· readObejctNoData는 릴리즈 1.4부터 지원되는 메소드이며 기존 직렬화 가능 클래스에 새로운 직렬화 가능 클래스를 상위 클래스로 추가하는 드문 경우를 지원하기 위한 것

 

직렬화를 구현하지 않기로 결정할 때의 주의사항

· 계승을 고려해 설계하였으나 직렬화는 가능하지않는 클래스에 대해서 직렬화 가능 하위 클래스를 구현하는 것은 불가능 할 수 있다.

ㄴ 상위 클래스에 무인자 생성자가 없다면 직렬화 가능 하위 클래스 구현이 불가능하다.

· 계승을 고려해 설계한 직렬화 불가능 클래스에는 무인자 생성자를 제공하는 것이 어떨지 반드시 따져봐야 한다.

· 객체를 생성할 때는 불변식이 충족된 상태로 생성하는 것이 최선이다.

//직렬화가 불가능한 상태유지 클래스, 하지만 직렬화 가능한 하위 클래스를 만들 수 있다.
public abstract class AbstractFoo{
    private int x, y; //상태
    
    // 아래 enum과 필드는 초기화 과정을 추적하기 위한 것이다.
    private enum State { NEW, INITIALIZING, INITIALIZED };
    private final AtomicReference<State> init = 
        new AtomicReference<State>(State.NEW);
        
    public AbstractFoo(int x, int y) {initialize(x, y); }
    
    //이 생성자와 그 아래 메서드는 하위 클래스의 readObject메서드가 상태를 초기화
    // 할 수 있도록 하기 위한 것
    protected AbstractFoo() {}
    
    protected final void initialize(int x, int y) {
        if(!init.compareAndSet(State.NEW, State.INITIALIZING))
            throw new IllegalStateException("Already initialized");
            
        this.x = x;
        this.y = y;
        
        .. //원래 생성자가 하려던 나머지 작업
        init.set(State.INITIALIZED);
    }
    
    //이 메서드들은 하위 클래스의 writeObect메서드에 의해 객체가 수동적으로
    // 직렬화 될 수 있도록 내부상태 정보를 제공하는 역할을 한다.
    
    protected final int getX() { checkInit(); return x; }
    protected fianl int getY() { checkInit(); return y; }
    
    // 모든 public 및 protected 객체 메서드에서 반드시 호출해야 하는 메서드
    private void checkInit(){
        if(init.get() != State.INITIALIZED)
            throw new IllegalStateException("Uninitialized");
    }
    
    ..//
}
 

AbstractFoo의 모든 public 및 protected객체 메서드는 다른 작업을 하기 전에 반드시 checkInit메서드를 호출해야 한다.

· 잘못 작성된 하위 클래스가 객체초기화를 제대로 하지못한 상태에서 다른 메서드를 호출하면 깔끔하게 실패하도록 하기 위한 것.

 

init필드가 원자적 참조(AtomicReference)라는것에 주의하자.

· 어떤 악의적인 사용자가 와도 객체 무결성을 보존할 수 있도록 하기 위한 것이다.

// 직렬화 불가능 상태 유지 클래스의 직렬화 가능 하위 클래스
public class Foo extends AbstractFoo implements Seralizable {
    private void readObject(ObectInputStream 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(getX());
        s.writeInt(getY());
        
    }
    
    //생성자는 이 메커니즘과 상관 없음
    public Foo(int x, int y) { super(x, y); }
    
    private static final long seralVersionUID = 132114124L;
}
 

내부 클래스는 Serializable을 구현하면 안된다.규칙22

 

내부 클래스의 기본 직렬화 형식은 정의될 수 없다.

 

결론

· Serializable 구현이 쉽다는 주장은 사실이 아니다.

· 계승을 고려하여 설계하는 클래스라면 더욱 조심해야 한다.

ㄴ 무인자 생성자를 제공하라.

 

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유