클래스와 멤버의 접근 권한을 최소화하자
잘 설계된 컴포넌트는 클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 숨겨, 구현과 API를 깔끔히 분리한다.
오직 API를 통해서만 다른 컴포넌트와 소통한다. [정보은닉, 캡슐화]라고 하는 이 개념은 소프트웨어 설계의 근간이 되는 원리다.
정보은닉 장점
- 시스템 개발 속도를 높인다. 여러 컴포넌트를 병렬로 개발할 수 있기 때문이다.
- 시스템 관리 비용을 나춘다. 컴포넌트가 모듈화되어 있어 더욱 쉽게 파악할 수 있고 교체하는 부담도 줄어든다.
- 소프트웨어 재사용성을 높인다.
- 큰 시스템을 제작하는 난이도를 낮춰준다.
- 성능 그 자체를 높여주지는 않지만, 성능 최적화에 도움 된다.
→ 컴포넌트들을 독립시켜 개발, 테스트, 최적화, 분석, 수정, 교체를 개별적으로 할 수 있게 해준다.
정보은닉 사용 핵심: 접근 제한자 활용
모든 클래스와 멤버의 접근성을 가능한 좁혀야 한다. : 최소한의 public API를 설계하자.
- public : 모든 곳에서 접근할 수 있다.
- protected : package-private의 접근 범위를 포함하여, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근할 수 있다.
- package-private : 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다. 접근 제한자를 명시하지 않았을 때 적용되는 패키지 접근 수준이다.
- private : 멤버를 선언한 톱레벨 클래스에서만 접근할 수 있다.
테스트를 위한 목적으로 접근 범위를 넓히는 것은 고려하자
public 클래스를 테스트하려 했지만 private 멤버여서 테스트하지 못한 경험은 있을 것이다.
이때 private 멤버를 package-private 까지 풀어주는 것은 고려할 수 있지만 그 넘어서는 절대 안된다. 오로지 테스트만을 위해 클래스, 인터페이스, 멤버를 공개 API로 만드는 것을 옳지 않다.
테스트 코드를 테스트 대상과 같은 패키지에 두면 package-private 요소에 접근할 수 있으므로 제한된 범위 안에서 사용하는 것이 바람직하다.
public 클래스의 인스턴스 필드는 되도록 public 이면 안된다.
- 필드가 가변 객체를 참조하거나, final이 아닌 인스턴스 필드를 public으로 선언하면 그 필드와 관련된 모든 것은 불변식을 보장할 수 없게 된다. 이는 스레드 안전하지 않다.
- 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안된다. 또한 public static final 필드가 참조하는 객체가 불변인지 확인하자.
변경 가능성을 최소화하자.
변경 가능성을 최소화하도록 클래스를 불변으로 설계하는 것은 오류가 생길 여지도 적고 더 안전하다.
클래스를 불변으로 만들기
- 객체의 상태를 변경하는 메서드를 제공하지 않는다.
- 클래스를 확장할 수 없도록 한다.
- 모든 필드를 final로 선언한다. (사용자의 의도를 명확히 드러낸다)
- 접근권한을 최소화하도록 모든 필드를 private로 선언한다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
불변 객체 장점
- 스레드 안전하여 따로 동기화할 필요가 없다. 다른 스레드에 영향을 줄 수 없기 때문에 불변 객체는 안심하고 공유할 수 있다.
- 한번 만든 인스턴스를 재활용할 수 있다.
- 불변 객체는 자유롭게 공유할 수 있고, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
불변으로 만들기
- 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
Getter가 있다고해서 무조건 Setter를 만들지는 말자 - 불면으로 만들 수 없는 클래스라도 변경할 수 있는 부분은 최소한으로 줄여야 한다. 객체가 가질 수 있는 상태의 수를 줄이면 오류가 생길 가능성도 줄어들고 의도를 드러내기 쉽다. 그렇기 때문에 합당한 이유가 없다면 모든 필드는 private final로 선언하는 것이 좋다.
- 생성자는 부련식 설정이 모두 완료된, 초기화가 끝난 상태의 객체를 생성해야 한다.
- 합당한 이유가 없다면 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도 public으로 제공하면 안 된다.
상속을 고려해 설계하자
- 상속용 클래스는 재정의할 수 있는 메서드들은 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
- 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.
- 상속용 클래스의 생성자는 재정의 가능 메서드를 호출해서는 안 된다.
- clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.
추상 클래스보다 인터페이스를 우선하자
추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다.
또한 자바는 단일 상속만 지원하기 때문에 추상 클래스 방식은 새로운 타입을 정의하는데 제약을 안게 된다.
인터페이스는 선언한 메서드를 모두 정의하고 그 규약을 지킨 클래스라면 다른 어떤 클래스를 상속했던 간에 같은 타입으로 취급이 된다. 또한 인터페이스를 이용함으로 자바의 다중 상속을 실현할 수 있다.
인터페이스의 장점
- 기존 클래스에도 손쉬게 새로운 인터페이스를 구현해 넣을 수 있다.
인터페이스는 implements 구문을 추가하고, 요구되는 메서드만 추가해주면 된다.
추상클래스는 계층구조상 두 클래스의 공통 조상이어야 하기 때문에 클래스 계층 구조에 혼란을 일으킨다. - mixin 정의에 안성맞춤이다.
대상 타입의 주된 기능에 선택적 기능(행위)를 제공하여 '혼합(mixed in)'한다 하여 mixin이라 한다.
추상 클래스같은 경우 mixin을 정의할 수 없다. 추상 클래스는 기존 클래스에 덧씌울 없기 때문이고, 단일 상속이기 때문에 두 부모를 섬길 수 없기에 mixin 정의가 불가능하다. - 인터페이스는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
- 추상 클래스로 타입을 정의한 경우, 기능 추가할 방법은 상속 한 번 뿐이기에 활용도가 떨어진다.
인터페이스는 타입을 정의하는 용도로 사용하자
인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다.
즉 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트 이야기해주는 것이다.
클래스는 계층 구조로, 톱 레벨 클래스는 한 파일에 하나만
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문 등 쓸데없는 코드가 많아 가독성이 매우 떨어진다.
- 사용하려는 코드 외에 다른 코드들도 많이 담겨 메모리도 부담된다.
위 같은 방식으로 클래스를 작성하기 보다는 아래처럼 계층구조로 클래스 작성하자
abstract class Figure {
abstract double area();
}
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; }
}
class Circle extends Figure {
final double radius;
Circle(double radius) { this.radius = radius; }
@Override
double area() { return Math.PI * (radius * radius); }
}
class Square extends Rectangle {
Square(double side) {
super(side, side);
}
}
또한 같은 맥락으로, 소스 파일 하나에는 반드시 톱 레벨 클래스(혹은 인터페이스)를 하나만 담자.
객체는 객체답게 작성하는 것이 유지보수, 성능 최적화, 코드의 가독성 등 모든 면에서 이롭다.
'Reading Book > 이펙티브 자바' 카테고리의 다른 글
[이펙티브 자바] 메서드 설계 주의점 (0) | 2022.12.12 |
---|---|
[이펙티브 자바] Enum과 EnumMap (0) | 2022.12.12 |
[이펙티브 자바] Object 메소드 관련 규약 + Comparable (0) | 2022.12.12 |
[이펙티브 자바] try-finally 보다 try-with-resources 사용하자 (0) | 2022.12.12 |
[이펙티브 자바] 불필요한 객체 생성을 피하고, 다 쓴 객체 참조를 해제하자 (0) | 2022.12.11 |