📌 4장 : 클래스와 인터페이스
아이템 15. 클래스와 멤버의 접근 권한을 최소화하라
- 잘 설계된 컴포넌트 : 모든 내부 구현을 완벽히 숨겨, 구현과 API를 깔끔하게 분리한다.
- 클래스, 인터페이스, 멤버가 의도치 않게 API로 공개되는 일이 없도록 해야 한다.
- public 클래스에서 public static final 이외에 어떤 public 필드가 있어서는 안된다.
- public static final 필드가 참조하는 객체가 불변인지 확인한다.
- 정보 은닉의 장점
- 시스템을 구성하는 컴포넌트들을 서로 독립시켜서 개발, 테스트, 최적화, 적용, 분석, 수정을 개별적으로 할 수 있게 해주는 것과 연관되어 있다.
- 시스템의 개발 속도를 높인다. (여러 컴포넌트를 병렬로 개발)
- 시스템 관리 비용을 낮춘다. (각 컴포넌트 교체 부담이 적다.)
- 성능 최적화에 도움을 준다.
- 소프트웨어 재사용성을 높인다. 외부에 거의 의존하지 않고, 독자적으로 동작 가능한 컴포넌트는 낯선 환경에서도 유용하게 쓰일 수 있다.
- 큰 시스템을 제작하는 난이도를 낮춰준다.
- 접근 제어 메커니즘
- 클래스, 인터페이스, 멤버의 접근성(접근 허용 범위)을 명시한다.
- 각 요소의 접근성은 선언된 위치와 접근 제한자로 정해진다.
- 접근 제한자를 제대로 활용하는 것이 정보 은닉의 핵심
- 기본 원칙
- 모든 클래스와 멤버의 접근성을 가능한 좁혀야 한다.
- private : 멤버를 선언한 톱레벨 클래스에서만 접근 가능
- package-private : 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다.
- 클라이언트에 피해 없이 다음 릴리스에서 수정, 교체, 제거 가능
- protected : package-private의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근 가능
- public : 모든 곳에서 접근 가능. 공개 API, 하위 호환을 위해 계속 관리 필요
- 상위 클래스의 메서드를 재정의할 때는 접근 수준을 상위 클래스에서보다 좁게 설정할 수 없다.
- 상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체해 사용할 수 있어야 한다.(리스코프 치환 원칙)
- public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.
- 필드가 가변 객체를 참조하거나 final이 아닌 인스턴스 필드를 public으로 선언하면 그 필드에 담을 수 있는 값을 제한할 힘을 잃는다.
- 꼭 필요한 구성요소의 상수일 경우 public static final 필드로 공개해도 된다.
- 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공하면 안된다.
- 길이가 0이 아닌 배열은 모두 변경이 가능하다.
- 클라이언트에서 이런 접근자를 제공하면 그 배열의 내용을 수정할 수 있게 되기 떄문이다.
- 첫 번째 방법 : public 배열을 private으로 만들고 public 불변 리스트를 추가한다.
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
- 두 번째 방법 : 배열을 private 으로 만들고 복사본을 반환하는 public 메서드를 추가(방어적 복사)
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
return PRIVATE_VALUES.clone(); }
아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
- 데이터 필드에 직접 접근할 수 있지만, 캡슐화의 이점을 제공할 수 없는 상황
class Point {
public double x;
public double y;
}
필드들을 모두 private으로 바꾸고 public 접근자(getter) 추가
class Point {
private double x;
private double y;
public Point(double xf double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
- 접근자와 변경자(mutator) 메서드를 활용해 데이터를 캡슐화한다.
- package-private 클래스 혹은 private 중첩 클래스에서는 데이터 필드를 노출해도 문제가 없다.
- 패키지 바깥 코드는 변경하지 않아도 데이터 표현 방식을 바꿀 수 있다.
아이템 17. 변경 가능성을 최소화하라
- 불변 클래스 : 인스턴스 내부 값을 수정할 수 없는 클래스
- 가변 클래스보다 설계, 구현, 사용이 간단하고 오류가 생길 여지가 적다.
- 자바 플랫폼 라이브러리의 불변 클래스 : String, 기본 타입의 박싱된 클래스, BigInteger, BigDecimal
- 불변 클래스 만드는 5가지 규칙
- 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
- 클래스 확장을 할 수 없게 한다. ex. final
- 모든 필드를 final으로 선언한다.
- 모든 필드를 private으로 선언한다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
- 클래스에 가변 객체를 참조하는 필드가 있으면, 클라이언트에서 객체를 참조할 수 없게 해야 한다.
- 생성자, 접근자, readObject 메서드에서 방어적 복사를 수행해라
- 함수형 프로그래밍
- 피연산자에 함수를 적용해서 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴
- 절차적 혹은 명령형 프로그래밍은 메서드에서 피연산자인 자신을 수정해 자신의 상태가 변한다.
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 double realPartf) { return re; }
public double imaginaryPart() { return im; }
public Complex plus(Complex c) {
return new Complex(re + c.re, im + c.im);
}
...
}
- 불변 객체는 근본적으로 스레드 안전하여 따로 동기화가 필요없다.
- 불변 클래스는 한 번 만든 인스턴스를 최대한 재활용한다.
- 가장 쉬운 재활용 방법 : 자주 쓰이는 값들을 상수로 제공한다. ex. public static final
- 불변 클래스는 자주 사용되는 인스턴스를 캐싱해서 같은 인스턴스를 중복 생성하지 않도록 정적 팩터리를 제공할 수 있다.
- 정적 패터리를 사용하면 여러 클라이언트가 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용을 줄어든다.
- public 생성자 대신에 정적 팩터리를 만들어두면, 클라이언트를 수정하지 않아도 캐시 기능을 덧붙일 수 있다.
- 방어적 복사도 필요 없기 때문에 clone 메서드나 복사 생성자를 제공하지 않는 것이 좋다.
- 불변 객체는 자유롭게 공유할 수 있고, 불변 객채꼐리 내부 데이터를 공유할 수 있다.
- 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.
- 불변 객체는 그 자체로 실패 원자성을 제공한다.
- 단점으로는 값이 다르면 반드시 독립된 객체로 만들어야 한다는 점이다.
- 중간 단계에서 만들어진 객체들이 모두 버려지는 경우 성능 문제
- 문제 대처 방법 : 다단계 연산(multistep operation) 들을 예측해서 기본 기능으로 제공하는 방법
- 클라이언트가 원하는 복잡한 연산들을 정확하게 예측하면 package-private의 가변 동반 클래스만으로도 충분하다.
- 문제 대처 방법 : 다단계 연산(multistep operation) 들을 예측해서 기본 기능으로 제공하는 방법
- 모든 클래스를 불변으로 만들 수 없다. 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄인다.
- 다른 합당한 이유가 없으면 모든 필드는 private final 이어야 한다.
- 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
아이템 18. 상속보다는 컴포지션을 사용하라
- 상속 : 클래스가 다른 클래스를 확장하는 구현 상속
- 메서드 호출과 다르게 상속은 캡슐화를 깨뜨린다.
- 상위 클래스에 따라 하위 클래스의 동작에 이상이 생길 수 있다.
- 잘못된 상속 예시 : addAll 메서드로 원소 3개를 더했을 때
InstrumentedHashSet<String> s = new InstrumentedHashSeto();
s.addAlKList.of("틱", "탁탁", "펑,,));
- getAddCount 메서드를 호출하면 3을 반환해야 하지만 6을 반환한다.
- HashSet의 addAll 메서드가 add 메서드를 사용해 구현되어서 addCount에 값이 중복해서 더해져서 최종 값이 6으로 늘어났다.
- 하위 클래스에서 addAll 메서드를 재정의하지 않으면 문제를 고칠 수 없다.
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
public InstrumentedHashSet() { }
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, LoadFactor);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.sizeO;
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
- 컴포지션(composition) : 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하는 방법
- 새 클래스의 인스턴스 메서드들은 기존 클래스의 대응하는 메서드를 호출해서 결과를 반환한다.
- 래퍼 클래스
- 다른 Set 인스턴스를 감싸고 있는 뜻에서 래퍼 클래스라고 한다.
- 다른 계측 기능을 덧씌운다는 뜻에서 데코레이터 패턴(decorator pattern) 이라고도 부른다.
- 단점이 거의 없지만, 래퍼 클래스가 콜백(callback) 프레임워크와는 어울리지 않는다는 점을 주의해야 한다.
public class InstrumentedSet<E> extends Fon시ardingSet<E> {
private int addCo나nt = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
- 재사용할 수 있는 전달(forwarding) 클래스
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
public void clear() { s.clearO; }
public boolean contains(Object o) { return s.contains(o); }
public boolean isEmpty() { return s.isEmpty(); }
public int size() { return s.size(); }
public Iterator<E> iterater() { return s.iterator(); }
public boolean add(E e) { return s.add(e); }
public boolean remove(Object o) { return s.remove(o); }
public boolean containsAll(Collection<?> c) { return s.containsAll(c); }
public boolean addAll(Collection<? extends E> c){ return s.addAH(c); }
public boolean removeAll(Collection<?> c){ return s. removeAH(c); }
public boolean retainAlKCollection<?> c){ return s. retainAlKc); }
public Object[] toArrayO { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override public boolean equals(Object o){ return s.equals(o); }
@Override public int hashCode() { return s.hashCodef); }
@Override public String toString() { return s.toString(); }
}
아이템 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
- 상속용 클래스는 재정의할 수 있는 메소드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
- 내부 메커니즘을 문서로 남기는 것이 상속을 위한 설계의 전부는 아니다.
- 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있따.
- 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 유일하다.
- clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.
- clone이 잘못되면 복제본말고도 원본 객체에도 피해를 줄 수 있다.
- 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것이 나을 수 있다.
- 상속을 금지하는 방법 2가지
- 클래스를 final으로 선언하는 방법
- 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어주는 방법
- 생성자 모두를 외부에서 접근할 수 없도록 만든다.
아이템 20. 추상 클래스보다는 인터페이스를 우선하라
- 자바가 제공하는 다중 구현 메커니즘 : 인터페이스, 추상클래스
- 두 메커니즘 공통점 : 인스턴스 메서드를 구현 형태로 제공 가능
- 차이점 : 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다.
- 추상 클래스 방식은 새로운 타입을 정의하는 데 커다란 제약이 생긴다.
- 인터페이스 방식은 선언한 메서드를 모두 정의하고 일반 규약을 잘 지킨 클래스라면 어떤 클래스를 상속해도 같은 타입으로 취급된다.
- 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다.
- 인터페이스가 요구하는 메서드를 추가하거나 클래스 선언에 implements 구문만 추가하면 된다.
- 추상 클래스 방식은 클래스 계층 구조에 혼란을 일으켜 새로 추가된 추상 클래스의 모든 자손이 상속을 하게 되는 문제가 생길 수 있다.
- 인터페이스는 믹스인(mixin) 정의에 안성맞춤이다.
- 믹스인 : 클래스가 구현할 수 있는 타입.
- 믹스인을 구현한 클래스에 원래의 '주된 타입' 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.
- 대상 타입의 주된 기능에 선택적 기능을 혼합한다고 해서 믹스인이라고 부른다.
- 인터페이스로 계층 구조가 없는 타입 프레임워크를 만들 수 있다.
- 타입을 계층적으로 정의하면 개념을 구조적으로 잘 표현 가능하지만, 표현이 어려운 개념도 있다.
public interface Singer {
AudioClip sing(Song s);
}
public interface Songwriter {
Song compose(int chartPosition);
}
- 작곡도 하는 가수가 있기 때문에 가수 클래스가 Singer와 Songwriter 모두를 구현해도 문제가 되지 않는다.
- 가능한 조합 전부를 각각의 클래스로 정의한 복잡한 계층 구조가 만들어질 수 있다.
- 래퍼 클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다.
- 인터페이스와 추상 골격 구현(skeletal implementation) 클래스를 함께 제공하는 방법으로 인터페이스와 추상 클래스 장점을 모두 취하는 방법도 있다.
- 템플릿 메서드 패턴 : 인터페이스로 타입 정의, 필요하면 디폴트 메서드도 제공, 골격 구현 클래스는 나머지 메서드까지 구현한다.
- 인터페이스 이름이 Interface 이면, 골격 구현 클래스 이름은 AbstractInterface
- 골격 구현을 사용해 완성한 구체 클래스 예시
static List<Integer> intArrayAsList(int[] a) {
Objects.requireNonNull(a);
// 다이아몬드 연산자를 이렇게 사용하는 건 자바 9부터 가능하다.
// 더 낮은 버전을 사용한다면 <Integer>로 수정하자.
return new AbstractListo() {
@Override public Integer get(int i) {
return a[i]; // 오토박싱(아이템 6)
}
@Override public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val; // 오토언박싱
return oldVal; // 오토박싱
}
@Override public int size() {
return a.length;
}
};
}
아이템 21. 인터페이스를 구현하는 쪽을 생각해 설계하라
- 자바 8 이전에는 추가된 메서드가 우연히 기존 구현체에 이미 존재할 가능성이 낮기 때문에, 기존 구현체를 깨뜨리지 않으면 인터페이스에 메소드를 추가할 방법이 없었다.
- 자바 8 이후부터는 기존 인터페이스에 메서드를 추가할 수 있도록 디폴트가 생겼지만, 위험이 아예 없어진 것은 아니다.
- 디폴트 메서드를 선언하면, 인터페이스 구현 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다.
- 모든 기존 구현체들이 매끄럽게 연동되는 보장이 없다.
- 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드 작성은 어렵다.
- 디폴트 메서드는 컴파일은 성공해도, 기존 구현체에 런타임 오류를 일으킬 수 있다.
- 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 것이 아니면 피해야 한다.
- 새로운 인터페이스를 만드는 경우에는 표준적인 메서드 구현을 제공하는 데 유용한 수단이 된다.
아이템 22. 인터페이스는 타입을 정의하는 용도로만 사용하라
- 인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할
- 자신의 인스턴스로 무엇을 할 수 있는지 클라이언트에게 알려줄 수 있다.
- 지침에 맞지 않는 예시 : static final 필드로만 가득찬 인터페이스
public interface PhysicalConstants {
// 아보가드로 수 (1/몰)
static final double AVOGADROS_NUMBER = 6.022_140_857e23;
// 볼츠만 상수 (J/K)
static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
// 전자 질량 (kg)
static final double ELECTRON_MASS = 9.109_383_56e-31;
}
아이템 23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라
- 태그가 달린 클래스
class Figure {
enum Shape { RECTANGLE, CIRCLE };
// 태그 필드 - 현재 모양을 나타낸다.
final Shape shape;
// 다음 필드들은 모양이 사각형(RECTANGLE)일 때만 쓰인다.
double length;
double width;
// 다음 필드는 모양이 원(CIRCLE)일 때만 쓰인다.
double radius;
// 원용 생성자
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// 사각형용 생성자
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() {
switch(shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError(shape);
}
}
}
- 태그가 있는 클래스 단점
- 열거 타입 선언, 태그 필드, switch 문 등등 쓸데없는 코드가 너무 많아 가독성이 나쁘다.
- 많은 메모리 사용
- 필드를 final로 선언하려면 쓰이지 않는 필드도 생성자에서 초기화해야 한다.
- 태그 달린 클래스는 장황하고, 오류가 생기기 쉽고, 비효율적이다.
- 자바와 같은 객체지향 언어에서는 타입 하나로 다양한 의미의 객체를 표현하는 나은 수단을 제공한다.
- 클래스 계층 구조를 활용하는 서브타이핑(subtyping)
- 태그 달린 클래스를 클래스 계층 구조로 변환
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
Circle(double radius) { this.radius = radius; }
@Override double area() { return Math.PI * (radius * radius); }
}
class Rectangle extends Figure {
final double length;
final double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override double area() { return length * width; }
}
아이템 24. 멤버 클래스는 되도록 static으로 만들라
- 중첩 클래스(nested class)
- 다른 클래스 안에 정의된 클래스
- 자신을 감싼 바깥 클래스에서만 사용해야 한다.
- 종류 : 정적 멤버 클래스. (비정적) 멤버 클래스, 익명 클래스, 지역 클래스
- 정적 멤버 클래스를 제외한 나머지는 내부 클래스(inner class)
- 정적 멤버 클래스
- 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에 접근할 수 있다.
- 바깥 클래스와 함께 쓰일 때 유용한 public 도우미 클래스로 쓰인다.
- 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있으면 정적 멤버 클래스로 만들어야 한다.
- 비정적 멤버 클래스
- 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.
- 정규화된 this : 클래스명.this
- 바깥 인스턴스 없이 생성할 수 없다.
- 비정적 멤버 클래스의 인스턴스 안에 관계 정보가 만들어져 메모리 공간을 차지하고, 생성 시간이 더 걸린다.
- 어댑터 정의에 주로 사용된다.
- 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없으면 정적 멤버 클래스로 만들어준다.
- 익명 클래스
- 쓰이는 시점에 선언과 동시에 인스턴스가 만들어진다.
- 선언한 지점에서만 인스턴스를 만들 수 있고, 클래스 이름이 필요한 작업은 수행할 수 없다.
- 여러 인터페이스를 구현할 수 없고, 인터페이스를 구현하는 동시에 다른 클래스를 상속할 수 없다.
- 주로 사용되는 경우는 팩터리 메서드 구현
- 지역 클래스
- 가장 드물게 사용되는 중첩 클래스
- 지역변수를 선언할 수 있는 곳이면 어디서든 선언 가능
- 유효 범위도 지역변수와 같다.
- 멤버 클래스처럼 이름이 있고 반복해서 사용 가능
- 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있고, 정적 멤버는 가질 수 없고, 가독성을 위해 짧게 작성해야 한다.
아이템 25. 톱레벨 클래스는 한 파일에 하나만 담으라
- 컴파일러가 한 클래스에 대한 정의를 여러 개 만들지 않도록, 소스 파일 하나에 톱레벨 클래스를 하나만 담는다.
'ETC > 이펙티브자바' 카테고리의 다른 글
[이펙티브 자바] 7장 : 람다와 스트림 (0) | 2023.03.20 |
---|---|
[이펙티브 자바] 6장 : 열거 타입과 애너테이션 (0) | 2023.03.20 |
[이펙티브 자바] 5장 : 제네릭 (0) | 2023.03.20 |
[이펙티브 자바] 3장 : 모든 객체의 공통 메서드 (0) | 2023.01.16 |
[이펙티브 자바] 2장 : 객체 생성과 파괴 (0) | 2023.01.02 |