IT_Programming/Java

Effective JAVA™ Programming Language Guide - 1

JJun ™ 2007. 2. 15. 12:43
LONG

4. C 구문 바꾸기

 

 (19) 구조체를 클래스로 바꿔라.

   ☞ 모든 면에서 자바 프로그래밍 언어의 클래스가 더 낫기 때문에 C의 struct 구문은

       제거되었다. 구조체는 단순히 몇 개의 자료 필드를 단순히 하나로 묶는 역할만 하지만,

       클래스는 여기에 연산을 결합시켜 자료 필드 자체는 사용자로부터 감춘다.

       다시 말하면, 클래스는 자료 자체를 객체 안에 감추고 메소드를 통해서만 접근할 수 있게

       하여 구현자가 경우에 따라 자유롭게 자료를 표현할 수

       있게 해준다. 자바 프로그래밍 언어를 처음 접하는 C 프로그래머들은 클래스가 너무 무거워

       구조체를 대체할 수 없는 상황이 있다고 믿고 있지만 그런 경우는 없다.

 

 (20) union은 클래스 계층구조로 바꿔라.

   ☞ C의 union 구문은 한 가지 타입 이상의 자료를 저장할 수 있는 구조체를 정의할 때 쓴다.

      이런 구조체는 보통 최소한 union과 tag라는 두 개의 필드를 가진다. tag는 유니언이

      어떤 타입을 가지고 잇는지 알려주는 필드로, 보통 enum타입이다. union과 tag 필드를 가진

      유니언을 특별히 식별자가 있는 유니언(discriminates union)이라고도 한다.

      하지만 자바 프로그래밍 언어에는 다양한 종류의 객체를 하나로 표현할 수 있는 타입을

      정의하는 “서브타이핑”이라는 더 좋은 방법이 있기 때문에 union구문을 제공하지 않는다.

      식별자가 있는 유니언이란 결국 클래스 계층 구조를 모방한 것뿐이다.

      식별자가 있는 유니언을 클래스 계층구조로 바꾸기 위해서는 tag값에 따라 달라지는

      행동들을 추상메소드로 표현한 추상클래스를 만들어야 한다. 그 다음으로 식별자가 있는

      유니언이 표현할 수 있는 각 타입을 이 추상클래스를 상속받는 클래스로 각각 정의한다.

      마지막으로 각 하위 클래스에서 추상메소드를 알맞게 구현하면 된다.

      이런 클래스 계층 구조는 식별자가 있는 유니언보다 장점이 많다.

      첫째로 클래스 계층구조는 타입 안정성을 제공한다는 것이다.  

      둘째, 코드가 단순 명료하다는 것이다.

      셋째 전혀 별개로 동작하는 다양한 타입들에 대해서도 쉽게 확장할 수 있다는 것이다.

      마지막 네 번째 장점은 타입들 사이의 자연스러운 계층 관계를 반영할 수 있어서 유연성이

      커지고 컴파일 시점의 타입 검사를 더 잘 할 수 있다는 것이다.

 

 (21) enum 구문은 클래스로 바꿔라.

   ☞ 타입안전열거 패턴은 열거타입의 구성요소를 표현하는 클래스를 public 생성자 없이

       정의한다. 대신, 열거타입의 각 상수에 해당하는 public static final필드를 하나씩 만든다.

       이 클래스의 객체를 생성하거나 상속 받을 수 있는 방법이 전혀 없기 때문에

       public static final필드를 통해 외부에 제공되는 객체만 존재할 수 있다.

       클래스 자체가 final은 아니지만 이 클래스를 상속 받을 수 있는 방법은 없다.

       상속을 받으려면 하위 클래스의 생성자에서 상위 클래스의 생성자를 호출해야 하는데,

       이 클래스에는 접근 가능한 생성자를 제공하지 않기 때문이다.

       타입안전 열거패턴은 컴파일 시점의 타입 안전성을 제공해준다.

       그리고 타입안전 열거 클래스에 새로운 상수를 추가해도 클라이언트 코드를 다시 컴파일

       할 필요가 없다.

       또한 이 클래스에 무슨 메소드든 추가할 수 있기 때문에 인터페이스를 구현할 수도 있다.

       타입 안전 열거 패턴은 int 열거 패턴에 비해 장점이 매우 많다. 몇 가지 단점이 있지만,

       이것들은 주로 열거 상수들의 집합을 만들어야 할 때나, 주어진 자원이 매우 한정적일 때만

       일어나는 문제이다. 따라서 열거타입이 필요하다면 항상 타입안전 열거 패턴을 써야 한다.

       타입 안전 열거 패턴을 쓰는 API가 int 열거 패턴을 쓰는 API보다 프로그래머가 쓰기 쉽다.

       자바 플랫폼 API에서 타입 안전 열거 패턴을 쓰지 않았던 것은 단지 API를 작성하던 시점에

       이 패턴을 몰랐기 때문이다. 마지막으로, 열거타입의 필요성은 상대적으로 적어야 한다는

       것을 지겹더라도 또 이야기하고 싶다. 서브클래싱으로 열거타입의 기능을 거의 다

       제공할 수 있기 때문이다.   

 

 (22) 함수 포인터를 클래스와 인터페이스로 바꿔라.

   ☞ C에서는 주로 전략 패턴을 구현하기 위해서 함수 포인터를 쓴다.

      자바 프로그래밍 언어에서는 전략 인터페이스를 선언하고 구체적인 전략 클래스가

      이 인터페이스를 구현하면 된다. 구체적인 전략 클래스를 단 한번만 쓴다면 익명 클래스로

      만들어야 한다. 만약, 구체적인 전략 클래스를 다시 써야 한다면 이 클래스를

      private 정적 멤버 클래스로 만들고 전략 인터페이스 타입의 public static final 필드로

      만들어야 한다.

 

 

 

5. 메소드

 

 (23) 인자의 유효성을 검사하라.

   ☞ 유효성 검사를 통해 인자가 유효하지 않을 때 적절한 예외를 발생시키면,

       빠르고 깔끔하게 메소드를 종료할 수 있다. 인자값을 검사하지 않는다면,

       무슨 일이 발생할지 모른다. 메소드가 이상한 예외를 발생시키면서 수행을 멈출 수도 있고,

       오류는 나지 않지만 잘못된 결과를 내는 것과 같은 더 나쁜 상황에 빠질 수도 있다.

       하지만 가장 나쁜 상황은 마치 아무 문제 없는 것처럼 정상적으로

       메소드는 끝났지만, 객체의 상태가 이상해져서 불특정시점에 불특정 위치에서 오류가

       나는 것이다.

       메소드나 생성자를 만들때 인자에 어떤 조건이 있는지 반드시 검토해야 한다.

       이 제약 조건들을 문서화하고 메소드 본문의 시작부분에서 이 제약조건을 검사해야 한다.

       이것을 습관화하라.

       조금만 신경써서 이 사소한 작업을 해놓으면, 인자가 제약조건을 어겼을 때 충분히

       보상받을 수 있다.

      (하지만 인자들을 함부로 제야하는 것이 좋다는 것은 아니다. 오히려, 가능하다면 최대한으로

       관대하게 메소드를 설계해야 한다. 메소드가 제대로 동작한다면 인자에 적용하는 제약은

       적으면 적을수록 좋다. 하지만, 어떤 종류의 제약은 구현하는 추상화에 어쩔 수 없이

       원래부터 내재된 경우도 있다.)     


 (24) 필요한 경우 방어 복사하라.

   ☞ 클래스의 모든 클라이언트는 항상 불변규칙을 깨뜨리기 위해 최선을 다한다고 가정하고,

       프로그래밍 할 때 항상 방어하는 자세를 가져야 한다.

       생성자에 전달되는 변경가능 인자들을 방어복사 해야 하고, 원본 대신에 복사본으로

       인스턴스를 만들어야 한다. 그리고 인자의 유효성을 검사하기 전에 먼저 복사하고 나서

       원본이 아닌 복사본의 유효성을 검사한다는 것에 주목해야 한다. 이상해 보이는가?

       하지만 이렇게 해야 할 이유가 있다.

       이렇게 해야 인스턴스가 생성될 때 인자를 검사하는 시점과 복사하는 시점 사이의

       “공격당하기 쉬운 창가”에 머물러 있는 동안 다른 쓰레드가 생성자에 전달한 인자를 바꿔도

       불변규칙을 지킬 수 있기 때문이다.

 

 (25) 메소드 시그니처를 신중하게 설계하라.

   ☞ 메소드 이름을 신중하게 결정하라. 이해하기 쉽고, 같은 패키지에 잇는 다른 클래스나

      인터페이스들과 일관성 있는 이름을 짓도록 노력해야 한다.

      그리고 편리한 메소드를 제공하기 위해 너무 애쓰지 마라.

      모든 메소드는 “자기 나름대로의 역할”을 해야한다.

      메소드가 너무 많으면 클래스를 배우고, 쓰고, 문서화하고, 시험하고, 관리하기 어려워진다.

      만약, 인터페이스에 메소드가 너무 많다면 사용자뿐만 아니라 구현자에게도 영향을 주므로

      어려움은 두배로 커진다. 여러분이 만든 타입이 지원해야 하는 행위 하나하나에 완벽하게

      기능을 발휘하는 메소드를 제공하라. 정말 자주 쓰이는 경우에만 여러 기능을 한번에

      처리하는 “속기” 메소드를 제공할지 고민하라.

      만약 조금이라도 의심이 생기면 “속기”메소드를 그냥 제거하라.

      인자를 너무 많이 받지 마라. 인자는 적을수록 좋다.

      현실적으로 인자가 세 개를 넘으면 문제가 있다고 본다. 이것이 일반 규칙이다.

      대부분의 프로그래머는 인자 개수가 세 개를 넘으면 기억하지 못한다.

      만약, API가 인자 개수가 세 개보다 많은 메소드들로 이루어졌다면,

      이 API를 명세문서 없이는 거의 쓸 수 없을 것이다.

      특히, 동일한 타입의 인자가 죽 이어져 있으면 훨씬 더 위험하다.

      인자의 순서를 기억하기 힘들다는 것도 문제이지만 실수로 순서를 바꿔 인자에 값을 넣으면

      컴파일도 잘 되고, 실행도 되지만 분명히 이상한 행동을 할 것이다. 인자의 길이를 줄이는

      두가지 방법이 있다. 그 중 하나는 많은 인자를 받는 메소드를 인자의 일부만 받는

      여러 개의 메소드로 나누는 것이다. 마지막으로 인자들을 모아서 보관하는 헬퍼 클래스도

      인자 수를 줄일 수 있는 좋은 방법이다. (보통 정적 멤버 클래스로 헬퍼 클래스를 만든다.)

 

 (26) 메소드를 중복 정의할 때는 신중하라.

   ☞ 메소드를 중복 정의 할 수 있다는 것을 반드시 중복정의 하라는 것으로 절대 받아들이지

       말아야 한다. 인자 수가 같은 메소드가 여러 개 생기도록 중복정의 하지 말아야 한다.

       하지만, 이 규칙을 적용할 수 없는 경우도 있다. 이런 경우에도, 최소한 같은 수의

       인자 집합이 간단한 타입 변환만 하면 다른 중복 정의 메소드에 전달될 수 있는 상황만은

       피해야 한다. 기존 클래스가 새로운 인터페이스를

       구현하는 것과 같이 이런 상황을 피할 수 없다면, 같은 수의 인자를 받는 중복 정의 메소드들

       이 똑같이 행동하도록 만들어야 한다. 이 규칙들을 지키지 않고 중복정의 메소드를 만들어

       놓으면, 대부분의 프로그래머들은 이 중복정의 메소드들을 제대로 쓰지 못할 뿐더러,

       왜 제대로 동작하지 않는지조차 모른 채 고민만 할 것이다.

 

 (27) 널(NULL)이 아닌 길이가 0(zero-length)인 배열을 리턴하라.

   ☞ 다음과 같은 메소드를 만드는 경우를 많이 볼 수 있다.

      private List cheeseInStock = ...;

     /*

        상점에 남아있는 모든 치즈의 배열을 리턴하거나,

        팔 수 있는 치즈가 없으면 null을 리턴한다.

     */

     public Cheese[] getCheeses()

     {

         if(cheeseInStock.size()==0)

              return null;

         ....................

     }

     팔 수 있는 치즈가 남아 있지 않다고 특별하게 처리할 이유는 없다.

     다음과 같이 간단하게 처리할 수 있는 것을

     if(Array.asList(shop.getCheeses()).contains(Cheese.STILTON))

       {System.out.println("Jolly good");}

     다음과 같이 복잡하게 null인지 확인까지 해주어야 한다.     

     Cheese[] cheeses = shop.getCheeses();

     if(cheeses != null && Array.asList(shop.getCheeses()).contains(Cheese.STILTON))

     {System.out.println("Jolly good");}

     길이가 0인 배열을 리턴할 수 있는 곳에서 null을 리턴하는 메소드를 쓸 때마다,

     이렇게 필요 없는 코드가 반복되어야 한다. 게다가 null을 처리하는 코드를 실수로 빼먹기

     쉽기 때문에 오류가 일어나기 쉽다. 배열을 리턴하는 메소드에서 null을 리턴할 이유가

     전혀 없다. null을 리턴할 상황이라면 길이가 0인 배열을 리턴하면 된다.

     (배열의 길이와 배열 자체가 분리되어 리턴되는 C 프로그래밍 언어의 영향을 받아 null을

      리턴해온 것 같다. C에서는 길이가 0으로 리턴하더라도 배열을 할당하는 데는 전혀 이득을

      주지 못한다.)

 

 (28) 외부에서 제공하는 API의 모든 구성요소에 대해 문서화 주석을 달아라.

   ☞ 문서화 주석은 API를 문서화 할 수 있는 가장 훌륭하고 효율적인 방법이다. 외부에 제공하는

      모든 API에 대해 반드시 문서와 주석을 달아야 한다. 또, 문서화 주석을 작성하는 표준규칙을

      지켜 일관성 있는 문서를 만들어야 한다. HTML 문서 조각을 문서화 주석에 포함시킬 수

      있고, HTML 메타문자는 반드시 이스케이프 처리해야 한다는 것도 기억해야 한다.

ARTICLE  

1. 객체를 생성하고 파괴하기

 

  (1) 생성자 대신 스태틱 팩토리 메소드를 고려하라.

   ☞  public 스태틱 팩토리 메소드는 단순히 자신이 정의된 클래스의 인스턴스를 리턴하는

        메소드로 public static으로 정의한다. 스태틱 팩토리 메소드는 생성자와 달리 이름을

        줄 수 있다.

        생성자에 전달되는 인자들만으로 그 생성자가 리턴하는 객체의 특징을 알기 어렵다.

        하지만.. 스태틱 팩토리 메소드는 이름을 정할 수 있기에 리턴하는 객체의 특징을 설명할 수

        있다는 것이다. 그리고 스태틱 팩토리 메소드는 생성자와 달리 호출될 때마다 새로운

        객체를  생성하지 않아도 된다. 불변 클래스(immutable class) 같은 경우에

        스태틱 팩토리 메소드를 쓰면, 미리 만들어 놓은 인스턴스를 계속 제공할 수도 있고,

        동일한 객체가 쓸데없이 생성되는 것을 막기 위해 생성한 인스턴스를 캐시에 저장해 놓고,

        필요할 때마다 캐시에 저장된 인스턴스를 꺼내서 제공할 수 있다. 마지막으로 생성자는

        자신이 정의된 클래스의 인스턴스만 리턴할 수 있지만, 스태틱 팩토리 메소드는 자신이

        선언되는 것과 같은 타입의 인스턴스는 모두 리턴할 수 있다.

        스태틱 팩토리 메소드를 쓰면 리턴할 객체의 타입을 유연하게 선택할 수 있다.     

   

  (2) private 생성자를 써서 싱글톤을 유지하라.

   ☞ 싱글톤이란 정확히 하나의 인스턴스만 만들어지는 클래스로 원래부터 유일할 수 밖에 없는

       비디오 출력이나 파일 시스템과 같은 시스템 구성요소들을 주로 표현한다.

       싱글톤은 두가지 방법으로 구현할 수 있는데 우선 public static final 멤버 필드를 쓰는

       방법을 살펴보자. 

       public class elvis

       {

          public static final elvis INSTANCE = new elvis();

          private Elvis()

          {

             ..............

          }

             ..............

       }

       다음은 public static final 멤버 필드 대신에 public 스태틱 팩토리 메소드를 쓰는 방법이다.

       public class elvis

       {

          public static final elvis INSTANCE = new elvis();

          private Elvis()

          {

             ..............

          }

          public static Elvis getInstance()

          {

              return INSTANCE;

          }

             ..............

       }

       public static final 멤버 필드를 쓰면 클래스를 구성하는 멤버 선언만으로

       그 클래스가 싱글톤이라는 것이 명확하게 드러나며, 이 멤버 필드는 final이므로

       항상 동일한 객체를 참조한다. 또 스태틱 팩토리 메소드를 쓰는 것보다 성능이 약간 낫기는

       하지만, 웬만한 JVM이라면 스태틱 메소드를 인라인 호출(inline call)로 처리하므로

       성능차이를 크게 느낄 수 없을 것이다. public 스태틱 팩토리 메소드를 쓰면 기존 클래스의

       API는 그대로 유지하면서 싱글톤의 구현을 마음대로 바꿀 수 있다.

       모든 조건을 고려해 볼 때, 영원히 싱글톤으로 남는 클래스라면 스태틱 필드,

       그렇지 않으면 스태틱 팩토리 메소드를 쓰는 것이 좋다.

       싱글톤 클래스가 직렬화를 제공한다면, 단순히 이 클래스의 선언부에

       implements Serializable이란 구문을 더하는 것만으로는 부족하다.

       싱글톤을 보장하려면, 반드시 readResolve 메소드를 제공해야 한다.

       그렇지 않으면 인스턴스들을 역직렬화할 때마다 새로운 인스턴스가 생겨나서

       가짜 클래스날뛰는 꼴이 된다. 이것을 막으려면 클래스에 readResolve 메소드를

       추가해줘야 한다.

       private Object readResolve() throws ObjectStreamException

       {

          /*

              진짜 elvis만 리턴한다.

              역직렬화로 태어난 가짜 elvis들은 태어나자마자

              Garbage Collector에게 던져 버린다.

          */

          return INSTANCE;

       }

 

  (3) private 생성자로 인스턴스를 만들지 못하게 하라.

   ☞ 유틸리티 클래스들은 처음부터 인스턴스를 생성할 수 없게 설계되엇을 것이다.

       사실, 이런 클래스의 인스턴스는 아무런 의미가 없다. 하지만 생성자를 만들어

       놓지 않으면 컴파일러가 자동으로 기본 생성자인 인자 없는 public 생성자를 만들어

       버린다는 것을 기억해야 한다. 이렇게 자동으로 만들어진 생성자도 사용자 입장에서 보면

       다른 생성자와 다를 것이 없다.

       API 제공자의 의도와 달리 인스턴스를 만들 수 있는 것처럼 명세 문서가 발표된 클래스들을

       쉽게 찾을 수 있다. 그렇다면 어떤 클래스의 인스턴스를 아예 만들지 못하게 하는 방법은

       무엇일까?  방법은 의외로 간단한다. private 생성자 하나만 만들어주면 된다.

       public class UtilityClass

       {

          private UtilityClass()

          {} // 이 생성자는 호출되지 않는다..

          .......................

        }

       Utility 클래스의 생성자는 이 클래스 밖에서 접근할 수 없다. 따라서 Utility 클래스 내부에서

       생성자를 호출하지 않는다면, 절대 인스턴스가 생성되는 일은 없다.

       (단점: 상속이 불가능하다...접근할 수 있는 생성자가 없기에...)

 

  (4) 쓸데없는 객체를 중복 생성하지 마라.

   ☞ 아주 특별한 경우가 아닌 이상, 동등한 기능을 하는 객체를 필요할 때마다 생성하는

       것보다는 한 객체를 재사용하는 것이 더 낫다. 재사용이 속도도 더 빠르고 보기에도

       편하기 때문이다.

       (하지만 요즘 Garbage Collector의 성능이 좋아져서 가볍고 간단한 예제들은 객체풀을

        쓰지 않는 편이 더 나은 성능을 낸다. 방어복사가 필요한데도 이미 있는 객체를 그냥

        재사용해서 생기는 문제는 쓸데없이 중복된 객체를 생성해서 생기는 문제보다 훨씬 더

        심각한 것이라는 것을 명심하도록 하자..)

 

  (5) 쓸모없는 객체 참조는 제거하라.

   ☞ 보통 자신만의 메모리 영역을 가지는 클래스를 쓸 때, 프로그래머는 메모리 누수에 대해

       항상 생각해야 한다. 구성요소를 비울 때마다 null을 대입하여 보관하던 객체 참조를

       제거해야 한다.

       메모리 누구는 눈에 드러나는 오류가 아니기 때문에 잠복해 있을 가능성이 크다.

       메모리 누수는 아주조심스럽게 코드를 검토하거나 힙 성능검사기로 알려지 도구들을

       사용해야만 발견할 수 있다. 따라서 사전에 예방하는 것이 좋다.

 

  (6) 종료자를 쓰지마라.

   ☞ 종료자들의 행동은 예측이 불가능하고 위험하다. 사실 종료자를 쓸 필요는 없다.

       종료자들을 쓰면 프로그램이 이상하게 행동할 수도 있고, 성능이 떨어질 수 있으며,

       이식성에도 문제가 발생할 수 있다. 반드시 써야하는 몇 안되는 경우를 제외하고는

       종료자를 안 쓰는 것이 철칙이다.

 

 

 

2. 모든 객체의 공통 메소드 구현하기

 

  (7) equals 메소드를 재정의할 때 표준 구현계약을 지켜라.

   ☞ 좋은 equals 메소드를 만드는 비법을 알아보자.

       ⓐ == 연산자를 써서 인자가 this 객체를 참조하는지 검사한다.

           만약 그렇다면 true를 리턴한다. 비교작업이 복잡하다면 이 검사를 하는 것이 좋다.

       ⓑ instanceod 연산자를 써서 인자의 타입이 올바른지 검사한다.

           만약 타입이 틀리다면 false를 리턴한다.

       ⓒ 인자를 정확한 타입으로 변환한다.

       ⓓ 주요 필드에 대해 인자의 필드와 this 객체의 해당 필드 값이 동등한지 검사한다.

           모두 동등하다면 true를 리턴하고, 하나라도 동등하지 않다면 false를 리턴한다.

       ⓔ equals 메소드를 만들었다면, 대칭성, 매개성, 일관성을 지키는지 확인한다.

       [주의사항: equals 메소드를 재정의하면 hashCode 메소드도 반드시 재정의 해야 한다.

                      불안정한 자원에 의존하는 equals 메소드를 작성하지 마라.

                      equals 메소드의 인자를 Object 차입이 아닌 다른 타입으로 선언하지 마라.]

     

 

  (8) equals 메소드를 재정의하면 hashCode 메소드도 반드시 재정의하라.

   ☞ hashCode 메소드를 제대로 재정의하지 않으면 많은 버그가 생길 수 있다.

       equals 메소드를 재정의하고도 hashCode 메소드를 재정의하지 않으면

       Object.hashCode의 표준 구현계약을 어기는 것이다.

       hashCode 메소드의 표준 구현계약을 어긴 클래스를 해시 알고리즘을 기반으로 동작하는

       HashMap, HashSet,Hashtable과 같은 컬렉션과 같이 쓰면 문제가 생긴다.

       hashCode 메소드를 재정의하지 않으면 두번째 항목을 위반하기 쉽다.

       논리적으로 동등한 객체는 반드시 해시코드가 같아야 한다.

       equals 메소드를 재정의하면, 물리적으로 다른 두 객체를 논리적으로는 동등한 것으로

       판단할 수 있다. 하지만, 만약 hashCode 메소드를 재정의하지 않으면 호출되는

       Object.hashCode 메소드는 두 객체의 논리적 동등성을 전혀 알 수 없기 때문에 공통점이

       전혀 없는 객체로 판단할 것이다. 따라서 Object.hashCode 메소드가 서로 다른 두 개의

       정수를 리턴할 수 있기 때문에, API문서에 정의된 "equals(Object)메소드의 리턴값이

       true인 두 객체의 hashCode 메소드는 같은 정수값을 리턴해야 된다."는 조항이 깨지게

       된다.      

 

  (9) toString 메소드는 항상 재정의하는 것이 좋다.

   ☞ 정형화된 문자열 형식을 명세하는 것과 상관없이 toString 메소드가 리턴하는 문자열에

       포함된 모든 정보에 접근할 수 있는 방법을 제굥하는 것이 좋다.

 

 (10) clone 메소드는 신중하게 재정의하라.

   ☞ Cloneable 인터페이스와 관련된 모든 문제점을 고려해 볼 때,  다른 인터페이스들은

      이 인터페이스를 extends 하지 말아야 하고 상속을 위해 설계한 클래스도 역시

      이 인터페이스를 implements 하지 말아야 한다. 실제로 더 나은 방법이 많이 있기 때문에

      고급 프로그래머들은 값싸게 배열을 복사하는 것과 몇가지 특이한 경우가 아니라면

      절대 clone 메소드를 재정의하거나 쓰지 않는다.

      상속을 위해 설계한 클래스에 제대로 동작하는 protected clone 메소드가 없다면,

      이 클래스의 하위 클래스는 Cloneable 인터페이스를 implements해도 소용없다.

 

 (11) Comparable 인터페이스의 구현을 고려하라.

   ☞ 어떤 클래스가 Comparable 인터페이스를 구현하여, Comparable 타입이 되는 순간,

      이 클래스는 수많은 일반 알고리즘들과 컬렉션 구현체들과 함께 쓸 수 있다.

      아주 작은 노력만으로 어마어마한 혜택을 누릴 수 있는 것이다.

      실제로 자바 플랫폼 라이브러리의 모든 값클래스는 이 인터페이스를 구현하고 있다.

 

 

 

3. 클래스와 인터페이스

 

 (12) 클래스와 멤버에 대한 접근은 최소화하라.

   ☞ 접근성은 최대한 줄여야 한다. 아주 조심스럽게 최소의 public API를 설계한 다음,

      쓸데없는 클래스, 인터페이스, 멤버가 API의 일부가 되지 않도록 해야 한다.

      public static final 필드가 아니라면 클래스에 public 필드가 있어선 안된다.

      또, public static final 필드라 해도 이 필드는 기본타입이거나 불변 객체만 참조해야 한다.

 

 (13) 불변 클래스를 써라.

   ☞ 불변 클래스를 만들려면 다음과 같은 규칙을 따라야 한다.

       ⓐ 객체를 변경하는 메소드(변경자)를 제공하지 않는다.

       ⓑ 재정의할 수 있는 메소드를 제공하지 않는다.

       ⓒ 모든 필드를 final로 만든다.

       ⓓ 모든 필드를 private으로 만든다.

       ⓔ 가변 객체를 참조하는 필드는 배타적으로 접근해야 한다.

       불변 객체는 단순하고, 원래부터 다중 쓰레드 환경에서 안전하기 때문에 동기화를 할 필요가

       없다. 그리고 불변 객체는 자유롭게 공유될 수 있을 뿐만 아니라, 이 객체들의 내부구조까지

       도 공유할 수 있다.

       이 불변 객체는 다른 객체(가변객체든 불변객체든 상관없이)를 만들때 쓸 수 있는 아주

       중요한 구성요소가 될 수 있다. 하지만 이런 불변 클래스에도 단점이 있으니..

       그 단점은 바로 각각의 값에 대해 서로 다른 객체가 필요하다는 것이다..

       그러므로 모든 클래스는 특별한 이유가 없다면 불변 클래스로 만드는 것이 좋다.

 

 (14) 상속보다 컴포지션을 써라.

   ☞ 상속은 코드를 재사용할 수 있는 좋은 방법이긴 하지만, 상속을 잘못 쓰면 프로그램을

       강건하지 못하게 만들 수 있다. 같은 패키지 안에서는 한 프로그래머가 모든 클래스의

       구현을 책임지기 때문에 상속을 안전하게 쓸 수 있다. 또 특별히 상속을 위해 설계하고

       문서화된 클래스도 안전하게 상속 받을 수 있다. 하지만, 다른 패키지에 있는

       일반 클래스를 상속 받는다는 것은 위험하다.

      (↑구현상속에 관한 글) 메소드의 호출과 달리 상속은 캡슈화 규칙을 어긴다.

      하위클래스가 제대로 동작하려면 상위 클래스가 어떻게 구현되는지 자세히 알아야만 한다.

      상위클래스의 구현이 배포판마다 바뀔 때 코드를 수정하지 않으면 하위 클래스는 제대로

      동작하지 않는다. 따라서 만약 특별히 상속을 위해 설계하고 문서화한 클래스를 상속받지

      않는다면 하위 클래스도 상위 클래스와 함께 계속 진화해야만 한다.

      상속은 상위 클래스 API의 오류를 전파시키지만, 컴포지션은 이런 오류를 감춘 새로운 API를

      제공할 수 있다.

      컴포지션과 포워딩을 쓰면 상속의 폐해를 막을 수 있다. 특히, 적절한 인터페이스가 있어서

      래퍼 클래스를 구현할 수 있다면 금상첨화이다. 래퍼 클래스는 강건할 뿐만 아니라

      기능도 막강하다.

 

 (15) 상속받을 수 있도록 설계하고 문서화하라. 아니면 상속을 금지하라.

   ☞ 클래스 명세문서에는 재정의 가능한 메소드가 자기사용을 하고 잇는지도 반드시 명기해야

       한다. 또, public이나 protected 메소드나 생성자가 다른 재정의 가능한 메소드를

       호출한다면, 어떤 순서로 호출하는지, 호출결과가 다음 작업에 어떤 영향을 미치는지 반드시

       명기해야 한다. 클래스 명세문서에는 어떤상황에서 자신의 재정의 가능한 메소드를

       호출하는지에 대해 반드시 명기해야 한다.상속과 관련된 문제를 해결하는 가장 좋은 방법은

       안전하게 상속 받을 수 있도록 

       설계하지도 않고, 문서화하지도 않은 클래스는 아예 상속받지 못하게 만드는 것이다.

      ( 꼭 상속받을 수 있게 만들고 싶은 클래스가 있다면, 이러한 클래스의 메소드 내부에서

        절대 재정의 가능한 다른 메소드를 호출하지 않아야 하고,이 사실을 문서화 해야 한다.

        즉, 재정의 가능한 메소드의 자기 사용을 모두 제거해야 한다. 이렇게 해야 안전하게

        상속받을 수 있는 클래스를 만들 수 있다. 메소드를 재정의해도 다른 메소드의 행동에

        전혀 영향을 주지 않기 때문이다. )    

 

 (16) 추상클래스보다는 인터페이스를 써라.

   ☞ 이미 존재하는 클래스가 새로운 인터페이스를 구현하도록 고치는 일은 어려운 일이 아니다.

      인터페이스를 쓰면 깔끔하게 믹스인 타입을 정의할 수 있다.

      그리고 계층이 없는 타입 프레임워크를 만들 수 있을 뿐만 아니라, 안전하고 강력하게

      클래스에 새로운 기능을 더할 수 있다.

      추상 클래스를 쓰는 것이 인터페이스를 쓰는 것보다 좋은 점이 한가지 있긴 있다.

      인터페이스를 발전시켜 가는 것보다 추상클래스를 발전시켜 가는 것이 훨씬

      더 쉽다는 것이다.

      다시 정리하면 다양한 구현이 가능한 타입을 정의할 때 인터페이스를 쓰는 것이 좋다.

      단, 쉽게 기능을 추가할 수 이쓴 것이 유연성과 강력함보다 더 중요한 경우에는

      추상 클래스를 쓰는 것이 좋다. 하지만 그 한계를 이해하고 수용할 수 있을때만

      추상클래스를 사용해야 한다.

      외부에 제공하는 중요한 인터페이스에 대한 기본 뼈대 구현을 제공하는 것이 좋다. 

      public 인터페이스는 신중하게 설계해야 하고, 다양하게 구현을 통한 철저한 시험을 거친 후

      발표해야 한다.     

 

 (17) 인터페이스는 타입을 정의할 때만 써라.

   ☞ 인터페이스는 타입을 정의할 때만 써야 한다. 상수를 제공하기 위해 쓰지 말아야 한다.

      상수 인터페이스 패턴은 아주 형편없이 인터페이스를 쓰는 것이다. 클래스 내부에서 쓰는

      상수는 자세한 구현 방식에 해당하는 것이다. 상수 인터페이스 패턴을 쓰면 이런 자세한

      구현방식을 외부에 제공하는 API에 그대로 드러내게 된다. API 사용자에게는 클래스가

      상수 인터페이스를 implements 하든 말든 전혀 상관할 바가 아니다. 상수 인터페이스는

      오히려 사용자들을 혼란스럽게 만들 수 있다. 더욱 나쁜 것은 이 상수들이 끝까지 지켜야

      하는 약속이 된다는 것이다.

 

 (18) 중첩 클래스는 정적 멤버 클래스로 정의하라.

   ☞ 자바 프로그래밍 언어에는 4가지 종류의 중첩 클래스가 있고, 각각 용도도 다르다.

       한 메소드에서만 쓰는 것이 아니거나, 너무 길어서 한 메소드에 넣기 힘들다면 멤버클래스로

       만드는 것이 좋다.

       멤버 클래스의 인스턴스에서 자신을 감싼 인스턴스의 참조가 필요한 경우에만 비정적 멤버

       클래스로 만들고, 나머지의 경우에는 모두 정적 멤버 클래스로 만든다. 메소드 안에

       중첩 클래스를 만들어야 하는 경우에 딱 한 곳에서만 쓰거나 이미 이 클래스를 특징 짓는

       타입이 이미 존재한다면 익명 클래스로 만들고, 그렇지 않다면 지역 클래스로 만들어야

       한다.

 

 

 

effective java.hwp
0.09MB