본문 바로가기
독서/이펙티브 자바 3판

Ch03 클래스와 인터페이스

by Thinking 2023. 4. 25.

step15: 클래스와 멤버의 접근 권한을 최소화하라

 

 어설프게 셜계된 컴포넌트와 잘 설계된 컴포넌트의 가장 큰 차이는 바로 클래스 내부 데이터와 내부 구현 정보를 얼마나 외부 컴포넌트로부터 잘 숨겼느냐다. 잘 설계된 컴포넌트는 모든 내부 구현을 완벽히 숨겨, 구현과 API를 깔끔히 분리한다.

 정보 은닉의 장점은 정말 많다. 그중 대부분은 시스템을 구성하는 컴포넌트들을 서로 독립시켜 개발, 테스트, 최적화, 적용, 분석, 수정을 개별적으로 할 수 있게 해주는 것과 연관되어 있다.

 

 

 

장점

1st 시스템 개발 속도를 높인다. 여러 컴포넌트를 병렬로 개발할 수 있기 때문이다.

2nd 시스템 관리 비용을 낮춘다. 각 컴포넌트를 더 빨리 파악하여 디버깅할 수 있고, 다른 컴포넌트로 교체하는 부담도 적기 때문이다.

3rd 정보 은닉 자체가 성능을 높여주지는 않지만, 성능 최적화에 도움을 준다. 완성된 시스템을 프로파일링해 최적화할 컴포넌트를 정한 다음 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화 할 수 있기 때문이다.

4th 소프트웨어 재사용을 높인다. 외부에 거의 의존하지 않고 독자적으로 동작할 수 있는 컴포넌트라면 그 컴포넌트와 함께 개발되지 않은 낯선 환경에서도 유용하게 쓰일 가능성이 크기 때문이다.

5th 큰 시스템을 제작하는 난이도를 낮춘다. 시스템 전체가 아직 완성되지 않은 상태에서도 개별 컴포넌트의 동작을 검증할 수 있기 때문

 

 

 자바는 정보 은닉을 위한 다양한 장치를 제공한다. 그중 접근 제어 메커니즘은 클래스, 인터페이스, 멤버의 접근성(접근 허용 범위)을 명시한다. 각 요소의 접근성은 그 요소가 선언된 위치와 접근 제한자(private, protected, public)로 정해진다. 이 접근 제한자를 제대로 활용하는 것이 정보 은닉의 핵심이다.

 

 기본 원칙은 간단하다. 모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다. 달리 말하면, 소프트웨어가 올바로 동작하는 한 항상 가장 낮은 접근 수준을 부여해야 한다는 뜻이다. 

 

 

 

멤버에 부여할 수 있는 접근 수준은 네 가지다. 

 

1st private : 멤버를 선언한 톱레벨 클래스에서만 접근할 수 있다.

2nd package private : 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다. 접근 제한자를 명시하지 않았을 때 적용되는 패키지 접근 수준이다. (단 인터페이스의 멤버는 기본적으로 public이 적용된다.)

3rd protected : package-private의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근할 수 있다.

4th public : 모든 곳에서 접근할 수 있다.

 

정리

 프로그램의 요소의 접근성은 가능한 한 최소한으로 하라. 꼭 필요한 것만 골라 최소한의 public API를 설계하자. 그 외 클래스, 인터페이스, 멤버가 의도치 않게 API로 공개되는 일이 없도록 해야 한다. public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안된다. public static final 필드가 참조하는 객체가 불변인지 확인하자.

 

step16: public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라

 

 public 클래스는 절대 가변 필드를 직접 노출해서는 안 된다. 불변 필드라면 노출해도 덜 위험하지만 완전히 안심할 수 없다. 하지만 Package-private 클래스나 pirvate 중첩 클래스에서는 종종 (불변이든 가변이든) 필드를 노출하는 편이 나을때도 있다.

 

 중첩 클래스는 말 그대로 클래스 내에 정의된 클래스를 뜻한다. 추가로 중첩 클래스는 다시 static으로 선언되지 않은 중첩 클래스인 내부 클래스와 static으로 선언된 중첩 클래스인 정적 클래스로 나뉜다.

 

 

 

step17: 변경 가능성을 최소화하라

 

 불변 클래스란  간단히 말해 그 인스턴스의 내부 값을 수정할 수 없는 클래스다. 클래스를 불변으로 만들려면 5가지 규칙을 따르면 된다.

 

1st 객체의 상태를 변경하는 메서드를 제공하지 않는다.

 

2nd 클래스를 확장할 수 없도록 한다. 하위 클래스에서 부주의하게 혹은 나쁜의도로 객체의 상태를 변하게 만드는 사태를 막아준다. 상속을 막는 대표적인 방법은 클래스를 final로 선언하는 것이지만, 다른 방법도 뒤에 살펴볼 것 이다.

 

3rd 모든 필드를 final로 선언한다. 시스템이 강제하는 수단을 이용해 설계자의 의도를 명확히 드러내는 방법이다. 새로 생성된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작하게끔 보장하는데도 필요하다.

 

4th 모든 필드를 private으로 선언한다. 필드가 참조하는가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아준다. 기술적으로는 기본 타입 필드나 불변 객체를 잠조하는 필드를 public final로만 선언해도 불변 객체가 되지만 이렇게 하면 다음 릴리스에서 내부 표현을 바꾸지 못하므로 권장하지는 않는다.

 

5th 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야한다. 이런 필드는 절대 클라이언트가 제공한 객체 참조를 가리키게 해서는 안되며, 접근자 메서드가 그 필드를 그대로 반환해서도 안된다. 생성자, 접근자, readObject 메서드 모두에서 방어적 복사를 수행하라.

 

 

 15,17 아이템을 종합하여 보면, 다른 합당한 이유가 없을때 모든 필드는 private final이어야 한다. 모든 클래스를 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자. 생성자는 불변식 설정이 모두 완료된, 초가화가 완벽히 끝난 상태의 객체를 생성해야 한다. 확실한 이유가 없다면 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도 public 사용을 지양하자.

 

 

 

step18: 상속보다는 컴포지션을 사용하라

 

 상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다. 잘못 사용하면 오류를 내기 쉬운 소프트웨어를 만든다. 상위 클래스와 하위 클래스를 모두 같은 프로그래머가 통제하는 패키지 안에서라면 상속도 안전한 방법이다. 하지만 일반적인 구체 클래스를 패키지 경계를 넘어, 다른 패키지의 구체 클래스를 상속하는 일은 위험하다. 상기하자면,  이 책에서 '상속'은 (클래스가 다른 클래스를 확장하는) 구현 상속을 말한다.

 

 메서드 호출과 달리 상속은 캡슐화를 깨뜨린다. 다르게 말하면, 상위 클래스가 어떻게 구현되었느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다. 상위 클래스는 릴리스마다 내부 구현이 달라질 수 있으며, 그 여파로 코드 한 줄 건드리지 않은 하위 클래스가 오동작할 수 있다는 말이다. 이러한 이유로 상위 클래스 설계자가 확장을 충분히 고려하고 문서화도 제대로 해두지 않으면 하위 클래스는 상위 클래스의 변화에 발맞춰 수정돼야만 한다.

 

 다행히 여러 문제를 모두 피해가는 묘안이 있다. 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자. 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 컴포지션이라 한다. 새 클래스의 인스턴스 메서드들은 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환한다. 이 방식을 전달(forwarding)이라 하며, 새 클래스의 메서드들을 전달 메서드(forwarding method)라 부른다. 그 결과 새로운 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어나며, 심지어 기존 클래스애 새로운 메서드가 추가되더라도 전혀 영향받지 않는다.

 

 상속은 반드시 하위 클래스가 상위 클래스의 '진짜' 하위 타입인 상황에서만 쓰여야 한다. 다르게 말하면, 클레스 B가 클래스 A와 is-a 관계일 때만 클래스 A를 상속해야 한다. 클래스 A를 상속하는 클래스 B를 작성하려 한다면 "B가 정말 A인가?"라고 자문해보자. "그렇다"고 확신할 수 없다면 B는 A를 상속해서는 안된다. 대답이 "아니다"면 A를 private 인스턴스로 두고, A와는 다른 API를 제공해야 하는 상황이 대다수다. 컴포지션을 써야 할 상황에서 상속을 사용하는 건 내부 구현을 불필요하게 노츨하는 꼴이고, 결과 API가 내부 구현에 묶이고 그 클래스의 성능도 영원히 제한된다.

 

정리

 상속은 강력하지만 캡슐화를 해친다는 문제가 있다. 상속은 상위 클래스와 하위 클래스가 순수한 is-a 관계일 때만 써야 한다. is-a 관계일 때도 안심할 수만은 없는 게, 하위 클래스의 패키지가 상위 클래스와 다르고, 상위 클래스가 확장을 고려해 설계되지 않았다면 여전히 문제가 될 수 있다. 상속의 취약점을 피하려면 상속 대신 컴포지션과 전달을 사용하자. 특히 래퍼 클래스로 구현할 적당한 인터페이스가 있다면 더욱 그렇다. 래퍼 클래스는 하위 클래스보다 견고하고 강력하다.

 

step19: 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라

 

 상속을 고려한 설계와 문서화란 정확히 무엇을 뜻할까? 메서드를 정의하면 어떤 일이 일어나는지를 정확히 정리하여 문서로 남겨야 한다. 달리 말하면, 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다. 클래스의 API로 공개된 메서드에서 클래스 자신의 또 다른 메서드를 호출할 수도 있다. 그런데 마침 호출되는 메서드가 재정의 기능 메서드라면 그 사실을 호출하는 메서드의 API 설명에 적시해야 한다. 어떤 순서로 호출하는지, 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 한다.

('재정의 가능'이란 public 과 protected 메서드 중 final이 아닌 모든 메서드를 뜻한다.)

 

 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 '유일'하다. 널리 쓰일 클래스를 상속용으로 설계한다면 문서화한 내부 사용 패턴과, protected 메서드와 필드를 구현하면서 선택한 결정에 책임져야 한다. 이 결정들이 그 클래스의 성능과 기능에 족쇄가 될 수 있기에, 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.

 

 상속을 허용하는 클래스가 지켜야 할 제약이 몇 개 더 있다. 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 기능 메서드를 호출해서는 안 된다. 이는 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다. 이때 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않는다.

 

정리

 상속용 클래스를 설계하기란 결코 만만치 않다. 클래스 내부에서 스스로를 어떻게 사용하는지(자기사용 패턴) 모두 문서로 남겨야 하며, 일단 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 한다. 그러지 않으면 그 내부 구현 방식을 믿고 활용하던 하위 클래스를 오동작하게 만들 수 있다. 다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected로 제공해야 할 수도 있다. 그러니 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 나을 것이다. 상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록 만들면 된다.

 

step20: 추상 클래스보다는 인터페이스를 우선하라

 

 자바가 제공하는 다중 구현 메커니즘은 인터페이스와 추상 클래스, 이렇게 2가지다. 둘의 가장 큰 차이는 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다는 점이다. 자바는 단일 상속만 지원하니, 추상 클래스 방식은 새로운 타입을 정의하는데 큰 제약을 안게 된 것이다. 

 

 기존 클래스에도 손쉽게 인터페이스를 구현해넣을 수 있다. 또한 인터페이스는 mixin 정의에 안성맞춤이다. 믹스인이란 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래의 '주된 타입' 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.

 Comparable은 자신을 구현한 클래스의 인스턴스들끼리는 순서를 정할 수 있다고 선언하는 mixin 인터페이스다. 이처럼 대상 타입의 주된 기능에 선택적 기능을 '혼합' 한다고 해서 믹스인이라 부른다. 하지만 추상 클래스는 믹인을 정의할 수 없다. 이유는 앞서와 같이 기존 클래스에 덧씌울 수 없기 때문이다. 클래스는 두 부모를 섬길 수 없고, 클래스 계층구조에는 믹스인을 삽입하기에 합리적인 위치가 없기 때문이다.

 

 

인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.

 

 타입을 계층적으로 정의하면 수많은 개념을 구조적으로 잘 표현할 수 있지만, 현실에서는 계층을 엄격히 구분하기 어려운 개념도 있다. 특히 조합 폭발(combinato-rial explosion)이라 부르는 현상이다. 이는 거대한 클래스 계층구조에는 공통 기능을 정해놓은 타입이 없으니, 자칫 매개변수 타입만 다른 메서드들을 수없이 많이 가진 거대한 클래스를 낳을 수 있다.

 

 

 

래퍼 클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다.

 

 타입을 추상 클래스로 정의해두면 그 다팁에 기능을 추가하는 방법은 상속뿐이다. 상속해서 만든 클래스는 래퍼 클래스보다 활용도가 떨어지고 깨지기는 더 쉽다. 한편 인터페이스와 추상 골격 구현 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취하는 방법도 있다. 인터페이스로는 타입을 정의하고, 필요하면 디폴트 메서드 몇개도 함께 제공한다. 골격 구현 클래스는 나머지 매서드들까지 구현한다. 이가 바로 템플릿 메서드 패턴이다.

 

 

추상 골격 클래스추상 클래스는 미완성 설계도와 비슷하다. 추상 클래스만으로는 인스턴스를 생성할 수 없고, 자식 클래스에서 상속 받아야만 완성할 수 있기 때문이다. 하지만 생각해보면 굳이 추상 클래스로 선언할 필요가 없다. abstrack인 키워드를 뺴고 일반 부모 클래스로 선언해도 상속하고 중복 멤버를 제거하는데 전혀 문제가 없기 때문이다. 

 

But -> 상위 클래스를 상속받은 하위 클래스가 메서드의 정의를 하지 않았다고 가정했을 때, 만약 추상 클래스의 추상 메소드로 상속 관계를 맺었다면 에디터에서 오류를 보여준다. 하지만 일반 클래스로 상속 관계를 맺는 상황에어 위와 같은 에러가 나타난다면 어떤 에러도 보여주자 않는다. 왜냐하면 일반 클래스의 메서드를 오버라이딩 하든 안하든 자유기 때문이다!

 

 

 디폴트 메서드 : 보통 인터페이스는 규칙을 잡거나, 서비스 플로우 로직을 잡는 데 사용하곤 하는데, 인터페이스를 구현하는 클래스에서는 메서드를 모두 구현해야하기 때문에 인터페이스에 메서드를 추가할 때 문제가 발생한다. 메서드 하나를 추가하려면 해당 인터페이스를 구현하는 모든 클래스에서는 해당 메서드 모두를 구현해줘야 하는데 이를 해결하기 위한 방법이 2가지다.

 

1st 인터페이스 내부에 정적 메서드(static method)를 사용하는 것

2nd 인터페이스의 기본 구현을 제공할 수 있도록 디폴트 메서드(default method) 기능을 사용한다.

 

정리

 일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다. 복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 꼭 고려해보아라. 골격 구현은 '가능한 한' 인터페이스의 디폴트 메서드로 제공하여, 그 인터페이스를 구현한 모든 곳에서 활용하도록 하는 것이 좋다. '가능한 한' 이라고 한 이유는, 인터페이스에 걸려있는 구현상의 제약 때문에 골격 구현을 추상 클래스로 제공하는 경우가 더 흔하기 때문이다.

 

 

step21: 인터페이스는 구현하는 쪽을 생각해 설계하라

 

 자바 라이브러리의 디폴트 메서드는 코드 품질이 높고, 범용적이라 대부분 상황에서 잘 작동한다. 하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어렵다. 디폴트 메서드는 컴파일에 성공하더라도 기존  구현체에 런타임 오류를 일으킬 수 있다. 자바 8은 컬렉션 인터페이스에 꽤 많은 디폴트 메서드를 추가했고, 기존에 짜여진 많은 자바 코드가 영향을 받은 것으로 알려졌다.

 

 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 피해야 한다. 추가하더라도 기존 구현체들과 충돌이 나지 않게 확인해야 한다. 반면 새로운 인터페이스를 만드는 경우라면 표준적인 메서드 구현을 제공하는데 아주 유용한 수단이며, 그 인터페이스를 더 쉽게 구현해 활용할 수 있게 해준다. 핵심은 디폴트 메서드라는 도구가 생겼더라도 인터페이스 설계할 때는 여전히 세심한 주의를 기울여야 한다.

 

 

step22: 인터페이스는 타입을 정의하는 용도로만 사용하라

 

 인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다. 클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지 클라이언트에게 얘기해주는 것이다. 인터페이스는 오직 이 용도로만 사용해야 한다.

 

상수 인터페이스 안티패턴은 인터페이스를 잘못 사용항 예다. 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아니라 내부 구현에 해당한다. 따라서 상수 인터페이스를 구현하는 것은 이 내부 구현을 클래스의 API로 노출하는 행위다.

안티패턴 : 비효율적이거나 생산성이 저해되는, '권장사항'의 반대편에 있는 소프트웨어 설계 관행

 

 만약 상수를 공개할 목적이라면 합당한 선택지가 있다.

 

1st 특정 클래스나 인터페이스와 연관된 상수라면 그 클래스나 인터페이스 자체에 추가해야 한다. 모든 숫자 기본 타입의 박싱 클래스가 대표적으로, Integer 와 Double에 선언된 MIN_VALUE 와 MAX_VALUE 상수가 이러한 좋은 예다. 

 

2nd 열거 타입으로 나타내기 적합한 상수라면 열거 타입으로 만들어 공개하면 된다. 

 

3rd 위가 아니라면 인스턴스화할 수 없는 유틸리티 클래스에 담아 공개하자.

정리

 인터페이스는 타입을 정의하는 용도로만 사용해야 한다. 상수 공개용 수단으로 사용하지 말자.

 

 

step23: 태그 달린 클래스보다는 클래스 계층구조를 활용하라

 

 태그 달린 클래스에는 단점이 가득하다. 여러 구현이 한 클래스에 혼합돼 있어서 가독성도 나쁘다. 필드들을 final로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화해야 한다. 한마디로, 태그 달린 클래스는 장황하고, 오류를 내기 쉬우며 비효율적이다.

위는 태그 달린 클래스의 예시 코드이다.

 다행히 자바와 같은 객체 지향 언어는 타입 하나로 다양한 의미의 객체를 표현하는 훨씬 나은 수단을 제공하는데 바로 클래스 계층 구조를 활용하는 서브타이핑이다. 태그 달린 클래스는 클래스 계층 구조를 어설프게 흉내낸 아류일 뿐이다. 그렇다면 태그 달린 클래스를 클래스 계층 구조로 바꾸는 방법을 알아보자.

 

클래스 계층구조로 바꾼 코드 예시이다.

 가장 먼저 계층 구조의 루트가 될 추상클래스를 정의하고, 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언한다. 그런 다음 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가한다. 모든 하위 클래스에서 공통으로 사용하는 데이터 필드들도 전부 루트 클래스로 올린다. 

 

 아래 코드는 태그 달린 클래스의 단점을 모두 날려버린다. 살아남은 필드들은 모두 final이고, 루트 클래스의 코드를 건드리리 않고도 다른  프로그래머들이 독립적으로 계층 구조를 확장하고 함께 사용할 수 있다. 타입이 의미별로 따로 존재하니 변수의 의미를 명시하거나 제한할 수 있고, 특정 의미만 매개변수로 받을 수 있다. 또한 타입 사이의 자연스러운 계층 관계를 반영할 수 있어서 유연성은 물론 컴파일타임 타입 검사 능력을 높여준다는 장점도 있다.

 

정리

 태그 달린 클래스를 써야하는 상황은 거의 없다. 새로운 클래스를 작성하는 데 태그 필드가 등장한다면 태그를 없애고 계증구조로 대처하는 방법을 생각해보자. 기존 클래스가 태그 필드를 사용하고 있다면 계층구조로 리팩터링하는 걸 고민해보자.

 

 

step24: 멤버 클래스는 되도록 static으로 만들라

 

 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자. static을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 갖게 된다. 이 참조를 저장하려면 시간과 공간이 소비되는데, 더 심각한 문제는 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다는 점이다. 익명 클래스에는 이름이 없다. 바깥 멤버 클래스의 멤도 아니고, 쓰이는 시점에 선언과 동시에 인스턴스가 만들어진다. 지역 클래스는 지역변수를 선언할 수 있는 곳이면 실질적으로 어디서든 선언할 수 있고, 유효 범위도 같다. 

 

정리

 중첩 클래스에는 네가지가 있으며, 각각 쓰임이 다르다. 메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기에 너무 길다면 멤버 클래스로 만든다. 멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적으로, 그렇지 않으면 정적으로 만들자. 중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한 곳이고 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다면 익명 클래스로 만들고, 그렇지 않으면 지역 클래스로 만들자.

 

 

step25: 톱 레벨 클래스는 한 파일에 하나만 담으라

 

 소스 파일 하나에 톱 레벨 클래스를 여러개 선언하면 한 클래스를 여러 가지로 정의할 수 있으며, 그중 어느 것을 사용할지는 어느 소스 파일을 먼저 컴파일 하냐에 따라 달라지기 때문에 위험하다. 해결책은 아주 간단한데, 톱레벨 클래스들을 서로 다른 소스 파일로 분리하면 그만이다.

 

정리

 소스 파일 하나에는 반드시 톱레벨 클래스 하나만 담게 되면, 소스 파일을 어떤 순서로 컴파일 하든 바이너리 파일이나 프로그램의 동작이 달라지는 일은 결코 일어나지 않을 것이다.

'독서 > 이펙티브 자바 3판' 카테고리의 다른 글

CH6 람다와 스트림  (0) 2023.05.15
CH05 열거 타입과 애너테이션  (0) 2023.05.05
CH04 제네릭  (0) 2023.05.02
Ch02 모든 객체의 공통 메서드  (0) 2023.04.25
Ch01 객체 생성과 파괴  (0) 2023.04.05