Effective Java (2) JAVA

3. 객체 생성과 파괴

객체를 생성하고 파괴하는 방법을 다룰 것이다. 객체를 생성해야 할 때와 생성하지 말아야할 때, 객체를 생성하는 방법과 생성을 피할 수 있는 방법, 객체가 정확한 시기에 파괴되었다는 것을 확인할 수 있는 방법, 객체를 파괴하기 전에 반드시 처리해야 하는 작업들을 관리하는 방법들에 대해 생각해 볼 것이다.

3.1. 생성자 대신 static factory method를 고려하라
 1. 장점
   - static factory method는 생성자와 달리 알맞은 이름을 줄 수 있다.
   - 생성자와 달리 호출될 때마다 새로운 객체를 생성하지 않아도 된다.
     이를 이용하면 특정 시점에 존재하는 인스턴스들을 엄격하게 관리할 수 있다.

     1) 특정 클래스가 싱글톤 이라는 것을 보장
     2) 내용이 동등한 불변 클래스의 인스턴스가 여러 개 존재하지 않는다.
   - 생성자는 자신이 정의된 클래스의 인스턴스만 리턴할 수 있지만, static factory method는 자신이 선언된 것과 같은 타입의
     인스턴스는 모두 리턴할 수 있다.

 2. 단점
   - 메소드를 정의한 클래스가 public 이나 protected 생성자를 제공하지 않으면, 다른 클래스가 이 클래스를 상속받을 수 없다.
   - 다른 스택틱 메소드와의 차이를 명시할 수 없다.

3.2. private 생성자를 써서 싱글톤을 유지하라
 1. 모든 조건을 고려해 보고, 영원히 싱글톤으로 남는 클래스라면 스태틱 필드를 쓰고, 그렇지 않으면 스태틱 팩토리 메소드를 쓰는 것이 좋다.
 2. 싱글톤을 보장하려면, 반드시 readResolve 메소드를 제공해야한다. 그렇지 않으면, 인스턴트들을 역직렬화 할 때마다 새로운 인스턴스가 생겨난다.

   Private Object readResolve() throws ObjectStreamException {
       /*
         * 진짜 인스턴스만 리턴한다.
         * 역직렬화로 태어난 가짜 인스턴스들은 태어나자마자 GC에게 던져진다.
         */

        Return INSTANCE;
   }

3.3. private 생성자로 인스턴스를 만들지 못하게 하라
 다른 클래스에서 해당 클래스를 생성 또는 상속하지 못하게 할 때 사용한다.


3.4. 쓸데없는 객체를 중복 생성하지 마라
 아주 특별한 경우가 아니라면, 동등한 기능을 하는 객체를 필요할 때마다 생성하는 것보다는 한 객체를 재사용하는 것이 더 낫다.
 재사용이 속도나 효율적인 코드 생산에 이바지 한다.


3.5. 쓸모 없는 객체 참조는 제거하라
 쓸모 없는 객체를 참조하게 된다면 이는 GC 대상에서 제외되므로 메모리 누수가 발생된다.
 보통 자신만의 메오리 영역을 가지는 클래스를 쓸 때, 프로그래머는 메모리 누수에 대해 항상 생각해야 한다.
 구성요소를 비울 때마다 null을 대입하여 보관하던 객체 참조를 제거해야 한다. (쓸모 없는 참조에 null 을 대입해 버리면 된다.)

3.6. 종료자들을 쓰지 마라
 종료자는 호출되자마자 즉각 실행되지 않는다. 어떤 객체에 대한 모든 참조가 없어진 후, 이 객체의 종료자가 수행될 때까지 시간은 얼마나 걸릴지 아무도 모른다. 따라서 알맞은 시간 안에 반드시 수행해야 하는 작업을 종료자에서 처리하면 절대 안 된다.(중요한 영속 상태를 갱신하는 작업을 종료자에서 처리하지 말아야 한다.)
 명시적인 종결처리 메소드를 제공하고, 이 클래스의 인스턴스를 쓰고 난 다음에 반드시 이 메소드를 호출하게 하라. 또, 이 클래스에 각 인스턴스가 종료되었는지 알 수 있는 private 필드를 두어야 한다. 명시적인 종결처리 메소드는 종결처리를 끝마치면 해당 인스턴스를 더 이상 쓸 수 없다는 사실을 이 필드에 기록해 두어야 한다. 다른 메소드들은 항상 이 필드를 먼저 확인하고, 이미 해당 인스턴스가 더 이상 쓸 수 없는 상태라면 IllegalStateException 을 던져야 한다.
 명시적인 종결처리 메소드는 즉각적인 종결처리를 위해 try-finally 구문과 함께 쓰는 것이 좋다. 이때 명시적인 종결처리 메소드는 finally 블록에서 호출해야 어떤 상황에서도 명시적인 종결처리 메소드가 호출된다는 것을 보장할 수 있다.

 Foo foo = new Foo(…);
 try {
     //foo 로 뭔가 작업하고

 } finally {
     Foo.terminate(); // 명시적인 종결처리 메소드
 }

 종료자를 쓰는 경우는?
 첫째, 앞에서 설명한 명시적인 종결처리 메소드를 호출하는 것을 잊었을 경우를 대비한 “안전망” 역할을 한다. 물론, 종료자가 바로바로 호출된다거나 반드시 호출된다는 보장은 없지만, 만약 클라이언트가 명시적인 종결처리 메소드를 호출하지 않았을 때, 중요한 자원을 아예 반환하지 않는 것보다 늦게라도 반환할 수 있는 가능성을 열어 두는 편이 더 낫다.
 예를 들면, InputStream, OutputStream, Timer 등의 클래스에는 이런 “안전망” 역할을 하는 종료자들이 정의되어 있다.
 둘째, native peer와 관련된 객체를 쓸 때 종료자를 쓴다. native peer란 일반 자바 객체가 native 메소드 호출을 통해 자신의 역할을 맡길 수 있는 native 객체를 의미한다.


4. 클래스와 인터페이스

강건하고(robust), 유연하고(flexible), 쓸모있게(usable) 만들기 위해 이 도구들을 어떻게 써야 하는지 알아보도록 하겠다.

4.1. 클래스와 멤버에 대한 접근은 최소화하라
우선 클래스의 public API를 신중하게 모두 설계한 다음, 나머지 멤버들은 모두 습관처럼 private로 만들어야 한다.
즉, 현재 개발한 패키지를 다시 한번 살표보고 클래스 사이의 의존성을 더 줄일 수 있는 방법은 없는지 생각해 보아야 한다.

4.2. 불변 클래스를 써라
불변 클래스(immutable class)란 인스턴스의 내용을 절대로 바꿀 수 없는 클래스이다.
이 클래스의 각 인스턴스가 저장하는 정보는 인스턴스를 생성할 때 단 한번 만든 후에 인스턴스가 소명될 때까지 변하지 않는다.
불변 클래스를 만들려면 다음과 같은 규칙을 따라야 한다.

1. 객체를 변경하는 메소드를 제공하지 않는다.
2. 재정의 할 수 있는 메소드를 제공하지 않는다.
3. 모든 필드를 final로 만든다.
4. 모든 필드를 private로 만든다.
5. 가변 객체를 참조하는 필드는 배타적으로 접근해야 한다.


4.3. 상속보다 컴포지션을 써라
상속이 비록 강력한 기능이긴 하지만 캡슐화를 위반하기 때문에 문제를 발생시킬 수 있다.
상위 클래스와 하위 클래스가 정말로 서브타입 관계가 있을 때만 상속을 쓸 수 있다.
하지만 이런 경우라도 하위 클래스와 상위 클래스가 다른 패키지에 있거나 상위 클래스가 상속을 위해 설계되지 않았다면 상속받은 하위 클래스에 문제가 생기기 쉽다. 컴포지션과 포워딩을 쓰면 상속의 폐해를 막을 수 있다. 특히, 적절한 인터페이스가 있어서 래퍼 클래스를 구현할 수 있다면 금상첨화이다. 래퍼 클래스는 강건할 뿐만 아니라 기능도 막강하다.


4.4. 상속받을 수 있도록 설계하고 문서화하라. 아니면 상속을 금지하라
클래스 명세문서에 메소드를 재정의하면 발생할 수 있는 모든 파급 효과를 자세하고 정확하게 기술해야 한다. 관례에 따라, 이런 경우에 메소드 명세문서 마지막 부분에 “이번 구현에서는” 으로 시작하는 설명을 달아 놓는 것이 좋다.

4.5. 추상 클래스보다는 인터페이스를 써라
인터페이스를 쓰면 깔끔하게 mixin 타입을 정의할 수 있다. 즉, 어떤 클래스가 자신의 기본 타입 외에 새로운 기능을 위해 마음대로 골라서 추가할 수 있는 타입이다.


덧글

댓글 입력 영역