선언부에 "implements Serializable"을 붙이는 순간 이 클래스는 더 이상 싱글턴 클래스가 아니다. readObject 메서드는 새로 생성된 객체를 반환하는데, 이 객체는 클래스가 초기화될 당시에 만들어진 객체와 같은 객체가 아니다.  readResolve를 이용하면 readObject가 만들어낸 객체를 다른 것으로 대체할 수 있다. 역지렬화가 끝나서 만들어진 객체에 대해 이 메서드가 호출되는데, 새로 만들어진 객체 대신, 이 메서드가 반환하는 객체가 사용자에게 반환된다는 것이다. 


 싱글턴 객체처럼, 개체 통제를 위해 readResolve를 활용할 때는, 객체 참조 자료형으로 선언된 모든 객체 필드를 반드시 transient로 선언해야 한다. 이러한 규칙을 지키지 않으면 아래의 오류가 발생할 수 있디/


 하지만 readResolve 메서드가 실행되기 전에 역직렬화된 객체에 대한 참조를 가로 칠 수 있게된다.


 위의 클래스를 작성한 다음 아래의 수작업으로 만들어낸 스트림을 역직렬화하여 두개의 싱글턴 객체를 만들어 내는 코드다.


ENUM클래스를 이용하여 직렬화 가능한 객체 통제 클래스를 구현하면, 선언된 상수 이외의 다른 객체는 존재할 수 없다는 보장이 생긴다. JVM이 해주는 보장이므로 믿을 수 있다.



요약

개체 수와 관련된 불변식을 강제하고 싶을 때는 가능하면 enum을 이용해야 한다. 그렇지 못할 경우, readResolve메서드를 구현해야 하며, 클래스의 모든객체 필드는 기본 자료형으로 하거나 , transient로 선언해야 한다.



readObject 메서드가 실질적으로는 public 생성자나 마찬가지다. 생성자를 구현할 떄와 같은 점에 주의해야 한다는 것이다. 생성자와 마찬가지로 인자의 유혀성을 검사해야 하고 필요하다면 인자를 방어적으로 복사해야 한다. 

 

 즉, readObject는 바이트 스트림을인자로 받는 생성자다. 문제는 인공적으로 만들어진 바이트 스트림을 readObject에 인자로 넘길 때 생긴다. 클래스 불변식을 위반하는 객체를 만들어 낼 수 있게 되는 것이다. 


요약

readObject 메서드를 구현할 때는 public 생성자를 구현할 때와 같은 마음가짐을 가지라는 것이다. 어떤 바이트 스트림이 주어지더라도 유효한 객체가 생성될 수 있도록 해야 한다. 바이트 스트림이 실제로 직렬화된 객체일 것이라 가정하지 마라.


Serializable을 구현한 클래스를 만들면서 기본 직렬화 형식을 그대로 이용하면, 기존 구현을 완전히 내버리기란 불가능해진다. 그 직렬화 형식에 영원히 갇히게 된다는 뜻이다. 어떤 직렬화 형ㅅ힉이 적절할지 따져보지도 않고 기본 직렬화 형식을 그대로 받아들이지 말아야 한다. 


 기본 직렬화 형식은 그 객체의 물리적 표현이 논리적 내용과 동일할 때만 적절하다. 예를 들어 아래의 클래스를 보자.


 논리적으로 위의 클래스는 어떤 사람의 이름은 성,이름,그리고 중간이름으로 나타내며, 실제 물리적인 구현도 그렇게 되어 있다. 따라서 기본 직렬화형식으로 직렬화를 하는 것이 적절한 경우이다.

위의 클래스는 문자열의 리스트를 나타내는 클래스다. 객체의 물리적 표현 형태(양방향 연결리스트)가 논리적 내용과 많이 다를 경우 기본직렬화 형식을 받아들이면 문제가 생길 수 있다.

  • 공개 API가 현재 내부 표현 형태에 영원히 종속된다. 위 예제에서 private로 선언된 StringList,Entry 클래스는 public API의 일부분이 되어 버린다. 다음번 릴리즈에는 다른 내부 표현 방식을 채택하더라도 StringList클래스는 여전히 연결리스트 표현을 입력으로 사용해야 할 것이다.
  • 너무 많은 공간을 차지하는 문제가 생길 수 있다. 위 예제의 기본 직렬화 형식에는 리스트 항목 사이의 모든 연결 정보가 쓸데없이 들어가 있다. 이런 정보는 구현 세부사항으로, 직렬화 형식에 포함시킬 가치가 없다.
  • 너무 많은 시간을 소비하는 문제가 생길 수 있다. 많은 양의 그래프를 순회할 수 있다. 위의 예제에서는 next를 따라가는 정도로 충분.
  • 스택 오버플로 문제가 생길 수 있다. 기본 직렬화 절차는 재귀적인 객체 그래프 순회를 필요로 하는데, 설사 객체 그 그래프의 크기가 과도한 수준이 아니라해도 오버플로가 생길 수 있다.

객체의 모든 필드가 transient일 때는 defaultWriteObject나 defaultReadObject를 호출하지 않는 것도 기술적으로 가능하긴 하지만 권장하는 사항은 아니다. 나중에 [비-transient객체 필드를 추가하더라도 상위 및 하위 호환성이 유지되기 때문이다.


기본 직렬화 형식 사용 여부에 상관없이, 객체를 직렬화 할 때는 객체의 상태 전부를 읽는 메서드에 적용할 동기화 수단을 반드시 적용해야 한다.



어떤 직렬화 형식을 이용하건, 직렬화 가능 클래스를 구현할 때는 직렬 버전UID를 명시적으로 선언해야 한다.


요약

직렬화 가능한 클래스를 만들기로 했다면 직렬화 형식에 대해서도 심각하게 고민해야 한다는 것이다. 기본 직렬화 형식은 그 형태가 객체의 논리적 상태에 부합할 때만 이용하라. 그렇지 않다면 객체의 논리적 상태를 적절히 표현하는 사용자 정의 직렬화 형식을 만들라. 일단 공개된 메서드는 향후 버전에서 제거할 수 없듯이, 직렬화 형식에 포함시킨 필드는 나중에 제거할 수 없다.





Serializable 구현과 관련된 가장 큰 문제는 일단 클래스를 릴리즈하고 나면 클래스 구현을 유연하게 바꾸기 어려워진다는 것이다. Serializable을 구현하면 그 클래스의 바이트 스트림 인코딩(직렬화방식)도 공개 API의 일부가 되어 버린다. 사용자 정의 직렬화형식을 설계하지 않고 기본 형식을 그대로 이용할 경우, 직렬화 형식은 영원히 클래스의 원래 내부 표현방식에 종속된다. 다시 말해, 기본 직렬화 형식을 받아 들이면 그 클래스의  private와 pacakage-private 객체 필드도 공개 API가 된다는 것이다. 정보은닉을 위해 필드의 접근 권한은 최소화해야 한다는 원칙은 효력을 잃게 된다.


Serialiable을 구현하는 생기는 두 번째 문제는, 버그나 보안 취약점이 발생할 가능성이 높다는 것이다. 보통 객체는 생성자를 통해 생성한다. 직렬화는 언어 외적인 객체 생성 메커니즘이다. 역직렬화는 생성자와 동일한 이슈를 갖고 있는 "숨은 생성자"다. 역직렬화 과정에 관계된 생성자가 명시적으로 존재하지 않기 때문에 생성자가 만족하는 모든 불변식을 보장해야 하는데, 일반적으로 이러한 과정을 놓치는 경우가 많다. 


Serializable을 구현하는 생기는 세 번째 문제는, 새 버전 클래스를 내놓기 위한 테스트 부담이 늘어난다. 직렬화 가능 클래스를 수정할 때는, 새 릴리즈에서 만들고 직렬화한 객체를 예전 릴리즈에서 역직렬화할 수 있는지, 그 역도 가능한지 검사하는 것이 중요하다. 또한 객체 원래의 기능도 문제가 없는지 테스트해봐야 한다. 


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


내부 클래스는 Serializable 을 구현하는 안된다. 내부 클래스에는 바깥 객체에 대한 참조를 보관하고 바깥 유효범위의 지연 변수 값을 보관하기 위해 컴파일러가 자동으로 생성하는 인위생성 필드가 있다. 익명클래스나 지역 클래스 이름과 마찬가지로, 언어 명세서에는 이런 필드가 클래스 정의에 어떻게 들어 맞는지 나와있지 않다. 따라서 내부 클래스의 기본 직렬화 형식은 정의될 수 없다. 인위생성 필드 때문에 ..? 



스레드 시스템이 제공하는 기본적인 추상화 단위 가운데는 스레드, 락, 모니터 이외에도 스레드 그룹이라는 것이 있다. 스레드 그룹은 원래 애플릿을 격리시켜 보안 문제를 피하고자 고안된 것이었으나, 그 목적을 다루지 못했다.

요약

스레드 그룹에는 쓸만한 기능이 별로 없으며, 그나마 쓸만하다고 싶은 기능에는 결함이 있다. 



실행할 스레드가 많을 때, 어떤 스레드를 얼마나 오랫동안 실행할지 결정하는 것은 스레드 스케줄러다. 제대로 된 운영체제라면 공평한 결정을 내리려 애쓰겠지만, 그 정책은 바뀔 수 있다. 따라서 좋은 프로그램이라면 스케줄링 정책에는 의존하지 말아야 한다 정확성을 보장하거나 성능을 높이기 위해 스레드 스케줄러에 의존하는 프로그램은 이식성이 떨어진다.


 그렇다면 스케줄러에 의존성을 낮으면서 이식성이 좋은 프로그램은 어떻게 개발하여야 할까? 실행 가능 스레드의 평균적 수가 프로세서 수보다 너무 많아지지 않도록 하는 것이다. 그렇게 했을 때 스레드 스케줄러가 할 수 있는 일이라고는 그저 더 이상 실행할 수 없을 때까지 실행 가능한 스레드를 실행하는 것뿐이다. 


 실행 가능 스레드의 갯수를 최소한으로 유지하려면 어떻게 해야할까? 그것은 스레드가 필요한 일을 하고 나서 다음에 할 일을 기다리게 만드는 것이다. 스레드는 필요한 일을 하고 있지 않을 때는 실행 중이어서는 안된다. 

요약

프로그램의 정확성을 스레드 스케줄러에 의존하지 말아야 한다. 그런 프로그램은 잔정적이지도 않고 이식성이 보장되지 않는다. 


초기화 지연은 필드 초기화를 실제로 그 값이 쓰일 때까지 미루는 것이다. 값을 사용하는 곳이 없다면 필드는 결코 초기화되지 않을 것이다. 초기화 지연은 기본적으로 최적화 기법이지만, 객체 초기화 과정에서 발생하는 해로우 순환성을 해소하기 위해서도 사용된다. 


 초기화 지연 기법이 어울리는 곳이 따로 있다. 필드 사용 빈도가 낮고 초기화 비용이 높다면 쓸만할 것이다. 다중 스레드 환경에서 초기화 지연 기법을 구현하는 것은 까다롭다. 두 개 이상의 스레드가 그런 필드를 공유할 때는 반드시 적절한 동기화를 해주어야 한다. 


아래는 초기화 순환성 문제를 해소하기 위해서 동기화된 접근자를 사용한 초기화 기법이다.


 성능 문제 때문에 정적 필드 초기화를 지연시키고 싶을 때는 초기화 지연 담당 클래스 숙어를 사용하라. 

 또는 이중 검사 숙어를 사용하라. 이 숙어를 사용하면 초기화가 끝난 필드를 이용하기 위해 락을 걸어야 하는 비용을 없앨 수 있다.

이중 검사 숙어의 변종중에 여러번 초기화 되어도 상관없을 경우 사용하면 좋다.



요약

대부분의 필드 초기화는 지연시키지 않아야 한다. 더 좋은 성능을 내거나, 해로운 초기화 순환성을 제거할 목적으로 필드 초기화를 지연시키고자 할 때는, 적절한 초기화 지연 기술을 이용하라. 객체 필드에는 이중 검사 숙어를 적용하고, 정적 필드에는 초기호 ㅏ지연 담당 클래스 숙어를 적용하라. 여러번 초기화 되어도 상관없는 필드라면 단일 검사 숙어도 고려해볼만 하다.



스레드 안전성에 대해서는 문서에 synchronized 키워드가 있는지 보면 알 수 있다고 하는데, 보통 Javadoc이 만드는 문서에는 synchronized 키드워가 들어가지 않는데, 거기에는 이유가 있다. synchronized 키워드는 메서드의 구현 상세에 해당하는 정보이며, 공개 API의 일부가 아니기 때문이다. 병렬적으로 사용해도 안전한 클래스가 되려면, 어떤 수준의 스레드 안전성을 제공하는 클래스 인지 문서에 명확하게 남겨야 한다.


  • 변경 불가능 - 이 클래스로 만든 객체들은 상수다. 따라서 외부적인 동기화 메커니즘 없이도 병렬적 이용이 가능하다. String, Long, BigInteger 등이 그 예다.
  • 무조건적 스레드 안전성 - 변경이 가능하지만 적절한 내부 동기화 메커니즘을 갖추고 있어서 외부적으로 동기화 메커니즘을 적용하지 않아도 병렬적으로 사용할 수 있다. Random, ConcurrentHashMap이 그 예다.
  • 조건부 스레드 안전성 - 무조건전 스레드 안전성과 거의 같은 수준이나, 몇몇 스레드는 외부적 동기화가 없이는 병렬적으로 사용할 수 없다. 
  • 스레드 안전성 없음 - 해당 객체들은 변경이 가능하다. 이러한 객체들을 병렬적으로 이용하려면 메서드를 호출하는 부분을 클라이언트가 선택한 외부적 동기화 수단으로 감싸야 한다. synchronized를 써야 한다는 뜻인가..?

요약

모든 클래스는 자신의 스레드 안전성 수준을 문서로 분명히 남겨야 한다. 


+ Recent posts