클래스와 인터페이스
Effective JAVA
ITEM15: 클래스와 멤버의 접근 권한을 최소화하라
- 사실 두말하면 잔소리다.
- 정보 은닉, 캡슐화는 소프트웨어 설계의 근간이 되는 원리이다.
- public 등
API
는 하위 호환을 위해 영원히 관리해줘야 한다.
특히나 자바의 경우에는
- 패키지 외부에서 쓸 이유가 없다면
package-private
으로 선언하자. - 한 클래스에서만 사용하는 package-private 클래스나 인터페이스를
private static
으로 중첩해서 사용하자. 이 경우 바깥 클래스 하나에서만 접근할 수 있다. - public일 필요가 없는 클래스의 접근 수준을 package-private로 좁혀두자.
Serializable
을 구현한 클래스에서는 그 필드들도 의도치 않게 공개 API가 될 수 있다.- Overriding을 할때는 상위 클래스에서의 접근 수준보다 좁게 지정할 수 없다. 이는
리스코프 치환법칙
이다. - 해당 클래스가 추상 개념을 완성하는데 꼭 필요한 구성요소로서의 상수라면
public static final
필드로 공개해도 좋다. 이 경우 대문자와 언더스코어(_)를 통해 명명하고 값은 반드시 값이나 불변 객체이어야 한다. 길이가 0이 아닌 배열
은 참조이므로 변경 가능하다. 주의해야 한다.
모듈 시스템
- Java9부터 도입되었다.
- 모듈은 패키지의 묶음이다. 패키지 중 공개(export)할 것들을 (관례상
module-info.java
파일에 선언한다. - 접근 지정자와 상관없이 위 파일에 공개되지 않은 패키지는 외부에서 접근할 수 없다.
- JDK 자체가 이를 적극 활용하였다. 자바 라이브러리에서 공개하지 않은 패키지들은 외부에서 접근할 수 없다.
ITEM16: public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
- 오히려 너무 당연한 말이라서 public으로 써도 좋을만한 사례만 정리한다.
package-private
클래스 혹은private
중첩 클래스라면 데이터 필드를 노출한다해도 문제가 없다.- 그 클래스가 표현하려는 추상 개념만 올바르게 표현해주면 된다.
- 클라이언트도 어차피 이 클래스를 포함하는 패키지 안에서만 동작하는 코드일 뿐이기 때문이다.
- 이는 접근자 방식보다 깔끔하다.
ITEM17: 변경 가능성을 최소화하라
- 불변 클래스란 그 인스턴스의 내부 값을 수정할 수 없는 클래스이다.
- 아래 다섯가지 규칙을 따르면 불변 클래스라 할 수 있다.
객체의 상태를 변경하는 메소드를 제공하지 않는다.
클래스를 확장할 수 없도록 한다
모든 필드를 final로 선언한다.
모든 필드를 private으로 선언한다.
자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
불변 객체의 예
public final class Complex {
private final double re;
private final double im;
// 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
public Complex plus(Complex c) {
return new Complex(re + c.re, im + c.im);
}
}
불변 객체의 장점
- 불변 객체는 단순하다.
- 근본적으로 스레드에 안전하여 따로 동기화할 필요가 없다. 따라서 안심하고 공유할 수 있다.
- 정적 팩토리 메소드 등을 통해 인스턴스를 중복 생성하지 않도록 캐싱할 수 있다.
불변 객체의 단점
- 단점으로는 원하는 객체를 완성하기까지 단계가 많고, 그 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제가 있을 수 있다는 것이다. 대표적인 예가
String
과 같은 클래스이다. - 이런 경우
가변 동반 클래스(companion class)
를 package-private으로 두곤 한다. - String의 경우 가변 동반 클래스로
StringBuilder
를 두고 있다.
함수형 프로그래밍
- 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 말한다.
- 위 코드에서
plus
는new
를 통해 새로운 객체를 반환한다. 따라서 내부 상태가 변하지 않으므로 함수형 프로그래밍이다.
ITEM18: 상속보다는 컴포지션을 사용하라
- 이번도 너무 당연한 이야기다. C++에서도 상속은
friend
다음으로 강력한 결합이다. - 메소드 호출과는 달리 상속은 캡슐화를 깨뜨린다.
- 상속은 결함까지도 상속함을 주의한다. 또한 어떻게
overriding
될지 모르기에 미래의 동작을 예측할 수 없다. - 기존 클래스를 새로운 클래스의 구성요소로 쓴다고 하여 이러한 설계를
컴포지션(composition)
이라 한다. - 기존 클래스의 API를 호출하는 것을
forwarding
, 그 호출하는 메소드를forwarding method
라고 부른다. - 상속은 반드시 하위 클래스가 상위 클래스의 진짜 하위 타입인 상황(
is-a
)에서만 쓰여야 한다.
SELF 문제
- 컴포지션은 거의 단점이 없지만 Callback 형태의 구조에서 취약점이 있다.
- 내부 객체는 자신을 감싸고 있는 wrapper의 존재를 모르므로 자신(this)의 참조를 넘기게 되고, 콜백 때 wrapper가 아닌 자신이 호출되게 된다. 이러한 문제를
SELF
문제라 하며 주의해야 한다.
ITEM19: 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
- 상속은 ITEM18에서 보듯 위험한 기술이지만 그만큼 강력한 기술이기도 하다.
- 사용한다면
protected
메소드를 최소화하는게 좋지만, 너무 적게 노출해서 상속으로 얻는 이점마저 없애지 않도록 주의해야 한다. - 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 유일하다.
- 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메소드를 호출해서는 안된다. 상위 클래스의 생성자가 먼저 호출되고, 어떻게 동작할지 하위 클래스에 달려 있어 장담할 수 없다.
Cloneable
이나Serializable
인터페이스를 구현한 클래스는 상속을 허용하면 일반적으로 좋지 않다.
상속을 금지하는 방법
- 클래스를
final
로 선언하면 상속을 금지할 수 있다. - 모든 생성자를
private
이나package-private
으로 선언하고 public 정적 팩토리를 만들어주는 방법도 있다.
ITEM20: 추상 클래스보다는 인터페이스를 우선하라
- 인터페이스가 선언된 메소드를 모두 정의하면 다른 어떤 클래스를 상속했든 같은 타입을 취급된다.
인터페이스는 믹스인(mixin) 정의에 안성맞춤이다.
- 믹스인을 구현한 클래스에 원래의 ‘주된’ 타입 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.
- 예컨대
Comparable
은자신을 구현한 클래스의 인스턴스들끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스이다. - 이처럼 대상 타입의 주된 기능에 선택적 기능을 혼합한다고 해서 믹스인이라고 부른다.
인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
public interface SingerSongwriter extends Singer, Songwriter {
AudioClip strum();
void actSensitive();
}
- 위와 같은 구조를 클래스로 만들려면 가능한 조합의 전부를 각각의 클래스로 정의한 고도비만 계층구조가 만들어질 것이다.
- 속성이 n개라면 지원해야 할 조합의 수는 2^n개나 된다. 흔히 조합 폭발(combinatorial explosion)이라 부르는 현상이다.
추상 골격 구현(skeletal implementation)
- 인터페이스와 추상 클래스의 장점을 모두 취하는 방법이다.
- 단순히 골격 구현을 확장하는 것만으로 이 인터페이스를 구현하는데 필요한 일이 대부분 완료된다. 바로 템플릿 메소드 패턴이다.
- 관례상 인터페이스 이름이 Interface라면 그 골격 구현 클래스의 이름은
AbstractInterface
로 짓는다.
static List<Integer> intArrayAsList(int[] a) {
Objects.requireNonNull(a);
return new AbstractList<>() {
@Override
public Integer get(int i) {
return a[i];
}
@Override
public int size() {
return a.length;
}
}
}
주의사항
equals
,hashCode
와 같은 Object의 메소드는 디폴트 메소드로 제공하면 안된다.
ITEM21: 인터페이스는 구현하는 쪽을 생각해 설계하라
디폴트 메소드(default method)
- 자바8 이전에는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메소드를 추가할 방법이 없었다.
- 디폴트 메소드를 선언하면 그 인터페이스를 구현한 후 디폴트 메소드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다.
- 하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메소드를 작성하기란 어려운 법이다.
- 다중 상속이 가능하므로 AbstractClass와는 차별성이 있다.
주의사항
- 기존 인터페이스에 디폴트 메소드로 새 메소드를 추가하는 일은 꼭 필요한 경우가 아니면 피해야 한다. 추가하려는 디폴트 메소드가 기존 구현체들과 충돌하지는 않을지 심사숙고해야한다.
- 새로운 인터페이스를 만드는 경우라면 표준적인 메소드 구현을 제공하는데 매우 유용한 수단이며, 그 인터페이스를 더 쉽게 구현해 활용할 수 있게끔 해준다.
ITEM22: 인터페이스는 타입을 정의하는 용도로만 사용하라
- 인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는
타입 역할
을 한다. - 달리 말해 클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에 얘기해주는 것이다.
- 인터페이스는 오직 이 용도로만 사용해야 한다.
- 상수 인터페이스 안티패턴은 인터페이스를 잘못 사용한 예이다. enum을 사용하자.
ITEM23: 태그 달린 클래스보다는 클래스 계층구조를 활용하라
- 태그달린 클래스에는 단점이 한가득이다. 우선 열거 타입 선언, 태그 필드, switch 문 등 쓸데없는 코드가 많다.
- 태그달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류일 뿐이다.
ITEM24: 멤버 클래스는 되도록 static으로 만들어라
중첩 클래스(nested class)
란 다른 클래스 안에 정의된 클래스를 말한다.- 중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외에 쓰임새가 있다면 탑레벨 클래스로 만들어야 한다.
- 중첩 클래스의 종류는
정적 멤버 클래스
,비정적 멤버 클래스
,익명 클래스
,지역 클래스
이렇게 네 가지이다.
정적 멤버 클래스
- 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하고는 일반 클래스와 똑같다.
- 흔히 바깥 클래스와 함께 쓰일 때만 유용한
public 도우미 클래스
로 쓰인다. - Operation 열거 타입은 Calculator 클래스의 public 정적 멤버 클래스가 되어야 한다. (Calculator.Operation.MINUS)
- 개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다. 비정적 멤버 클래스는 바깥 인스턴스없이는 생성할 수 없기 때문이다.
- 반대로 말하면 바깥 인스턴스를 참조해야한다면 비정적 멤버 클래스를 사용해야 한다.
비정적 멤버 클래스
- 바깥 클래스의 인스턴스 메소드에서 비정적 멤버 클래스의 생성자를 호출할 때 자동으로 만들어지는게 보통이다.
- 직접 수동으로 만들때는 직접 바깥 인스턴스의 클래스.new MemberClass(args)를 호출한다.
User user = new User();
Address address = user.new Address();
- 반복자(Iterator)의 구현은 비정적 멤버 클래스의 흔한 쓰임이다.
public class MySet<E> extends AbstractSet<E> {
@Override
public Iterator<E> iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator<E> {
…
}
}
- 멤버 클래스가에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들어야 한다.
- 자원낭비 이외에도 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 문제가 발생할 수 있다!
익명 클래스
- 익명 클래스는 표현식 중간에 등장하므로 10줄 이하로 짧지 않으면 가독성이 떨어진다.
- 자바8 이후로는 람다가 익명 클래스의 역할을 대체하게 되었다.
Interface Name {
public void call();
}
Name name = new Name() {
private int age;
@Override
public void call() {
…
}
}
ITEM25: 탑레벨 클래스는 한 파일에 하나만 담아라
- 소스 파일 하나에 탑레벨 클래스(inner class가 아닌)를 여러개 선언하더라도 자바 컴파일러는 불평하지 않는다.
- 탑레벨 클래스는 한 파일에 하나만 담아라.
- 굳이 여러 탑레벨 클래스를 한 파일에 담고 싶다면 정적 멤버 클래스를 사용하는 방법을 고민해볼 수 있다.