4장 - 클래스와 인터페이스
15. 클래스와 멤버의 접근 권한을 최소화하라
클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼느냐가 잘 설계된 컴포넌트의 기준이다.
모든 내부 구현을 완벽히 숨겨, 구현과 API를 깔끔히 분리한다.
오직 API를 통해서만 다른 컴포넌트와 소통하며 서로의 내부 동작 방식에는 전혀 개의치 않는다.
정보 은닉, 캡슐화라고 하는 이 개념은 소프트웨어 설계의 근간이 되는 원리다.
정보 은닉의 장점
- 시스템 개발 속도를 높인다. 여러 컴포넌트를 병렬로 개발할 수 있게 때문이다.
- 시스템 관리 비용을 낮춘다. 컴포넌트를 빨리 파악, 디버깅 속도를 높이며 컴포넌트 교체 부담도 줄인다.
- 성능 최적화에 도움을 준다. 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화할 수 있기 때문
- 소프트웨어 재사용성을 높인다.
- 큰 시스템을 제작하는 난이도를 낮춰준다.
톱레벨 클래스와 인터페이스에 부여할 수 있는 접근 수준은 package-private과 public 두 가지다.
public.으로 선언하게 되면 API가 되며 package-private으로 선언하면 해당 패키지 안에서만 사용할 수 있다.
패키지 외부에서 쓸 이유가 없다면 package-private으로 선언하자.
public으로 선언하면 영원히 API로서 관리해야 한다.
public 가변 필드를 갖는 클래스는 일반적으로 스레드 안전하지 않다.
핵심정리
프로그램 요소의 접근성은 가능한 한 최소한으로 하라.
꼭 필요한 것만 골라 최소한의 public API를 설계하자. 그 외에는 클래스, 인터페이스, 멤버가 의도치 않게 API로 공개되는 일이 없도록 해야 한다.
public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안된다!
public static final 필드가 참조하는 객체가 불변인지 확인하라.
16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
public 필드만 모여있는 클래스는 필드에 직접 접근할 수 있어 캡슐화의 이점을 제공하지 못한다.
또 API를 수정하지 않고는 내부 표현을 바꿀 수 없고, 불변식을 보장할 수 없으며, 외부에서 필드에 접근할 때 부수 작업을 수행할 수도 없다.
철저한 객체 지향 프로그래머는 이런 클래스를 상당히 싫어해서 필드들을 모두 private으로 바꾸고 public 접근자를 추가한다.
public 클래스라면 이 방식이 확실히 맞다. 접근자를 제공함으로서 클래스 내부 표현 방식을 언제든 바꿀 수 있는 유연성을 얻을 수 있다.
하지만 package-private 클래스 혹은 private 중첩 클래스라면 데이터 필드를 노출한다 해도 하등의 문제가 없다.
그 클래스가 표현하려는 추상 개념만 올바르게 표현해주면 된다.
이 방식은 클래스 선언 면에서나 이를 사용하는 클라이언트 코드 면에서나 접근자 방식보다 훨씬 깔끔하다.
17. 변경 가능성을 최소화하라
불변 클래스란
그 인스턴스 내부 값을 수정할 수 없는 클래스다. 불변 인스턴스에 간직된 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다.
String, BigInteger 같은 값 클래스들이 여기 속한다.
이 클래스들을 불변으로 설계한 데는 그럴만한 이유가 있다. 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.
클래스를 불변으로 만드는 다섯 가지 규칙
- 객체의 상태를 변경하는 메서드를 제공하지 않는다.
- 클래스를 확장할 수 없도록 한다.
- 상속을 막는 대표적인 방법은 final로 선언하는 것
- 모든 필드를 final로 선언한다.
- 모든 필드를 private으로 선언한다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
클래스의 모든 메서드들이 인스턴스 자신은 수정하지 않고 새로운 인스턴스를 만들어 반환하는 방법을 함수형 프로그래밍이라 한다.
이 방법으로 프로그래밍하면 코드에서 불변이 되는 영역의 비율이 높아지는 장점을 누릴 수 있다.
불변 클래스의 단점으로는 값이 다르면 반드시 독립된 객체로 만들어야 한다는 것이다.
자신을 상속하지 못하게 하는 가장 유연한 방법은 모든 생성자를 private혹은 package-private으로 만들고 public 정적 팩터리를 제공하는 방법이다.
getter가 있다고 해서 무조건 setter를 만들지는 말자. 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.
객체가 가질 수 있는 상태의 수를 줄이면 그 객체를 예측하기 쉬워지고 오류가 생길 가능성이 줄어든다.
다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
18. 상속보다는 컴포지션을 사용하라
상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다. 잘못 사용하면 오류를 내기 쉬운 소프트웨어를 만들게 된다.
상위, 하위 클래스 모두 같은 프로그래머가 통제하면 상속도 안전한 방법이다.
하지만 일반적인 구체 클래스를 패키지 경계를 넘어 다른 패키지의 구체 클래스에 상속하는 일은 위험하다.
메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.
다르게 말하면 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다.
상위 클래스는 릴리스마다 내부 구현이 달라질 수 있으며, 그 여파로 코드 한 줄 건드리지 않은 하위 클래스가 오동작할 수 있다는 말이다.
하위 클래스가 상위 클래스의 메서드를 재정의할 때 그 메서드 안에서 상위 클래스의 메서드를 사용하게 되면 오동작을 일으킬 가능성이 많다.
메서드를 재정의하는 대신 새로운 메서드를 추가하면 괜찮으리라 생각할 수 있지만, 새로운 릴리스에서 상위 클래스에 같은 시그니쳐의 다른 반환타입을 가진 메서드가 추가되면 컴파일조차 되지 않는다.
기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자.
기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 컴포지션(Composition; 구성) 설계라고 한다.
상속을 해야할 때는 하위 클래스가 상위 클래스의 “진짜” 하위 타입인 상황에서(is-a)만 쓰여야 한다.
19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
메서드를 재정의하면 어떤 일이 일어나는지를 정확히 정리하여 문서로 남겨야 한다.
달리 말하면, 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야 한다.
상속용 클래스를 설계하기란 결코 만만치 않다. 클래스 내부에서 스스로를 어떻게 사용하는지 모두 문서로 남겨야 하며, 일단 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 한다.
그렇지 않으면 그 내부 구현 방식을 믿고활용하던 하위 클래스를 오동작하게 만들 수 있다.
다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드들을 protected로 제공해야 할 수도 있다.
그러니 클래스를 확장해야 할 명확한 이유가 떠오르지 않는다면 상속을 금지하는 편이 나을 것이다.
상속을 금지하려면 클래스를 final로 선언하거나, 생성자 모두를 외부에서 접근할 수 없도록 만들면 된다.
20. 추상 클래스보다는 인터페이스를 우선하라
자바가 제공하는 다중 구현 메커니즘은 인터페이스와 추상 클래스, 이렇게 두 가지다.
자바 8부터 인터페이스도 default 메서드를 제공할 수 있게 되어 이제는 두 메커니즘 모두 인스턴스 메서드를 구현 형태로 제공할 수 있다.
한편, 둘의 가장 큰 차이는 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다는 점이다.
자바는 단일 상속만 지원하니, 추상 클래스 방식은 새로운 타입을 정의하는 데 커다란 졔약을 안게 되는 셈이다.
기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있다. 메서드를 추가하고 implements 구문만 추가하면 끝이다.
하지만 추상 클래스는 끼워넣기가 어려운것이 일반적이다.
만약 두 클래스가 같은 추상 클래스를 확장하길 원한다면, 그 추상 클래스는 계층 구조상 두 클래스의 공통 조상이어야 한다.
안타깝게도 이 방식은 클래스 계층구조에 커다란 혼란을 일으킨다.
새로 추가된 추상 클래스의 모든 자손이 이를 상속하게 되는 것이다.
인터페이스는 믹스인 정의에 안성맞춤이다.
믹스인이란 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래의 ‘주된 타입’ 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.
인터페이스는 여러개를 동시에 구현할 수 있지만 추상 클래스는 아니다.
한편, 인터페이스와 추상 골격 구현 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취하는 방법도 있다.
인터페이스로는 타입을 정의하고, 필요하면 디폴트 메서드도 함께 제공한다.
그리고 골격 구현 클래스는 나머지 메서드들까지 구현한다.
이렇게 해두면 단순히 골격 구현을 확장하는 것만으로 이 인터페이스를 구현하는데 필요한 일이 대부분 완료된다.
바로 템플릿 메서드 패턴이다.
관례상 인터페이스 이름이 Interface이면 그 골격 구현 클래스의 이름은 AbstractInterface로 짓는다. (예: AbstractCollection. AbstractSet, AbstractList..)
21. 인터페이스는 구현하는 쪽을 생각해 설계하라
자바 8 전에는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메서드를 추가할 방법이 없었다.
디폴트 메서드가 추가되고나서 그 길이 열렸지만 모두 매끄럽게 연동되리라는 보장이 없다.
디폴트 메서드는 구현 클래스에 대해 아무것도 모른 채 합의 없이 무작정 ‘삽입’될 뿐이다.
생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법이다.
디폴트 메서드는 (컴파일에 성공하더라도) 기존 구현체에 런타임 오류를 일으킬 수 있다.
자바 8은 컬렉션 인터페이스에 꽤 많은 디폴트 메서드를 추가했고, 그 결과 기존에 짜여진 많은 자바 코드가 영향을 받은 것으로 알려졌다.
기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 피해야 한다.
22. 인터페이스는 타입을 정의하는 용도로만 사용하라
인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다.
달리 말해, 클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에 얘기해주는 것이다.
인터페이스는 오직 이 용도로만 사용해야 한다.
상수를 정의해놓은 상수 인터페이스는 삼가하도록 하자. 대신 인스턴스화 할 수 없는 유틸리티 클래스로 만들자
23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라
두 가지 이상의 의미를 표현할 수 있으며, 그중 현재 표현하는 의미를 태그 값으로 알려주는 클래스를 본 적이 있을 것이다. (예: enum 타입으로 태그 값을 지정하는 클래스. 사각형인지 원인지)
태그 달린 클래스에는 단점이 너무 많다.
쓸데없는 코드들이 너무 많고, 가독성도 나쁘다. 필드들을 final로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화해야 한다. 인스턴스의 타입만으로는 현재 나타내는 의미를 알 길이 전혀 없다. 또 메모리도 많이 사용한다.
한 마디로 태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다.
태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류일 뿐이다.
태그 달린 클래스를 만들지 말고 클래스 계층구조 방식으로 만들자
Exceptions 태그를 달고 있는 Exception 클래스에 대해 다시 한 번 생각해보고, 클래스 계층구조로 리팩토링 하는 방법을 고민해보자
24. 멤버 클래스는 되도록 static으로 만들라
중첩 클래스란 다른 클래스 안에 정의된 클래스를 말한다.
중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다.
중첩 클래스의 종류는 정적 멤버 클래스, (비정적) 멤버 클래스, 익명 클래스, 지역 클래스 이렇게 네 가지다.
정적 멤버 클래스란 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하고는 일반 클래스와 똑같다.
정적 멤버 클래스는 다른 정적 멤버와 똑같은 접근 규칙을 적용받는다. 예컨대 private으로 선언하면 바깥 클래스에서만 접근할 수 있는 식이다.
정적 멤버 클래스와 비정적 멤버 클래스의 구문상 차이는 단지 static이 붙어있고 없고 뿐이지만, 의미상 차이는 의외로 꽤 크다.
비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암북적으로 연결된다.
그래서 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.
정규화된 this란 클래스명.this 형태로 바깥 클래스의 이름을 명시하는 용법을 말한다.
따라서 개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다.
비정적 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없기 때문이다.
비정적 멤버 클래스의 인스턴스와 바깥 인스턴스 사이의 관계는 멤버 클래스가 인스턴스화될 때 확립되며, 더 이상 변경할 수 없다.
이 관계는 바깥 클래스의 인스턴스 메서드에서 비정적 멤버 클래스의 생성자를 호출할 때 자동으로 만들어지는게 보통이지만, 드물게는 직접 바깥 인스턴스의 클래스.new MemberCalsss(args)를 호출해 수동으로 만들기도 한다.
예상할 수 있듯 이 관계 정보는 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하며, 생성 시간도 더 걸린다.
비정적 멤버 클래스는 어댑터를 정의할 때 자주 쓰인다. 즉 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용하는 것이다.
예컨대 Map 인터페이스의 구현체들은 보통 (keySet, entrySet, values 메서드가 반환하는) 자신의 컬렉션 뷰를 구현할 때 비정적 멤버 클래스를 사용한다.
멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.
static을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 갖게 된다. 앞서도 얘기했듯 이 참조를 저장하려면 시간과 공간이 소비된다.
더 심각한 문제는 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다는 점이다. 멤버 클래스가 공개된 클래스의 public이나 protected 멤버라면 정적이냐 아니냐는 두 배로 중요해진다.
API가 되니 향후 릴리스에서 static을 붙이면 하위 호환성이 깨진다.
익명 클래스에는 당연히 이름이 없다. 또한 익명 클래스는 바깥 클래스의 멤버도 아니다.
멤버와 달리, 쓰이는 시점에 선언과 동시에 인스턴스가 만들어진다. 코드의 어디서든 만들 수 있다.
익명 클래스는 응용하는 데 제약이 많은 편이다. 선언한 지점에서만 인스턴스를 만들 수 있고, instanceOf 검사나 클래스의 이름이 필요한 작업은 수행할 수 없다.
지역 클래스는 네 가지 중첩 클래스 중 가장 드물게 사용된다.
25. 톱레벨 클래스는 한 파일에 하나만 담으라
소스 파일 하나에 톱레벨 클래스를 여러 개 선언하더라도 자바 컴파일러는 불평하지 않는다.
하지만 아무런 득이 없을 뿐더러 심각한 위험을 감수해야 하는 행위다.
이렇게 하면 한 클래스를 여러 가지로 정의할 수 있으며, 그중 어느 것을 사용할지는 어느 소스 파일을 먼저 컴파일하냐에 따라 달라지기 때문이다.