[item-15] 클래스와 멤버 접근 권한을 최소화하라
객체지향의 핵심: 캡슐화, 정보은닉
캡슐화의 장점
- 시스템 개발 속도 향상
- 시스템 관리비용 절감
- 성능 최적화
- 코드 재사용성
- 큰 시스템을 제작하는 난이도를 낮춰줍니다
- 잘 설계된 객체는 모든 내부 구현을 완벽히 숨겨, 실제로 구현한 코드와 외부의 사용자가 사용하는 코드를 깔끔하게 분리합니다.
- 외부에 공개한 메서드를 통해서만 다른 객체들과 소통하며 서로의 내부 동작방식에는 전혀 개의치 않습니다
[item-16] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
// Degenerate classes like this should not be public!
class Point {
public double x;
public double y;
}
// Encapsulation of data by accessor methods and mutators
class Point {
private double x;
private double y;
public Point(double x, 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; }
}
- getter: 접근자, setter: 변경자
- public 필드는 데이터 필드에 직접 변경할 수 있어 캡슐화의 이점을 얻지 못합니다.
- 필드에 대한 불변식이 깨지게 됩니다.
- API를 변경해야만 표현 방식을 바꿀 수 있습니다.
- 필드를 읽을 때 부수작업을 수행할 수 없습니다.
- 접근자와 제어자를 활용하면 클래스 내부 표현을 자유롭게 바꿀 수 있는 유연성을 얻을 수 있습니다. public 클래스가 필드를 공개하면, 이를 사용하는 클라이언트가 생겨날 것이므로 내부 표현 방식을 바꿀 수 없게 됩니다.
예외
- package-private, private 중첩 클래스
- getter, setter > public 필드
[item-17] 변경 가능성을 최소화하라
불변 클래스 : 인스턴스의 내부 값을 수정할 수 없는 클래스
장점
- 가변 클래스보다 설계하고 쉽고, 구현하고 사용하기 쉽습니다.
- 불변 객체는 근본적으로 스레드에 안전하기 때문에 따로 동기화할 필요가 없습니다.
- 불변 클래스는 재활용할 수 있고, 캐시를 활용해 성능을 개선할 수 있습니다.
- 방어적 복사도 필요 없어집니다.
단점
- 값이 다르면 반드시 독립된 객체로 만들어야 한다는 것이 단점으로 작용할 수 있습니다.
- 이들을 모두 만드는데 큰 비용이 듭니다.
불변 클래스를 만드는 방법
- 객체의 상태를 변경하는 메서드를 제공 X(변경자 X)
- 클래스를 확장할 수 없도록 합니다.
- 모든 필드를 final로 선언합니다.
- 모든 필드를 private으로 선언합니다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 합니다.
확장하지 못하도록 하는 방법 (상속 X)
- final class로 만들고, 더 유연한 방법으로는 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩터리를 제공하는 방법
- 바깥에서 볼 수 없는 package-private 구현 클래스를 원하는 만큼 만들어 활용할 수 있습니다
불변과 관련된 주의사항
- 클래스는 꼭 필요한 경우가 아니라면 불변이여야 합니다
- 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄여야 합니다
- 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 합니다
[item-18] 상속보다는 컴포지션을 사용하라
상속의 장점
- 코드를 재사용함으로써 중복을 줄일 수 있습니다
- 변화에 대한 유연성 및 확장성이 증가합니다
- 개발 시간이 단축됩니다
상속의 단점
- 캡슐화를 깨뜨립니다
- 캡슐화: 만일의 상황(타인이 외부에서 조작)에 대비해 외부에서 특정 속성이나 메서드를 사용할 수 없도록 숨겨놓는 것
- 상위 클래스의 구현이 하위 클래스에게 노출되는 상속은 캡슐화를 깨뜨립니다. 캡슐화가 깨짐으로써 하위 클래스가 상위 클래스에 강하게 결합, 의존하게 됩니다.
- 상위 클래스의 요구사항이 변경되면, 하위 클래스에서 모두 수정해야 합니다.
조합 (Composition)
- 조합: 기존 클래스가 새로운 클래스의 구성요소로 쓰이는 경우
- 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조
조합을 사용하면?
- 메서드를 호출하는 방식으로 동작하기 때문에 캡슐화를 깨뜨리지 않습니다
- Lotto 클래스와 같은 기존 클래스의 변화에 영향이 적어지며, 안전합니다.
- 상속의 문제점에서 벗어날 수 있습니다.
결론
캡슐화를 깨뜨리고, 상위 클래스에 의존하게 돼서 변화에 유연하지 못한 상속을 사용하기보다는 조합을 사용
상속을 사용해야 하는 경우
- 확장을 고려하고 설계한 확실한 is-a 관계
- API에 아무런 결함이 없는 경우, 결함이 있다면 하위 클래스까지 전파돼도 괜찮은 경우
[item-19] 상속을 고려해 설계하고 문서화해라. 그러지 않았다면 상속을 금지해라
상속시 주의점
- 상속용 클래스의 생성자는 재정의 가능한 메서드(non-private, non-final, non-static)를 호출하면 안 됩니다.
- 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로, 하위 클래스에서 재정의해버린 메서드가 하위 클래스의 생성자보다 먼저 호출됩니다. 이때 하위 생성자에서 초기화하는 값에 의존한다면 의도대로 동작 X
public class Super {
// Broken - constructor invokes an overridable method
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
public final class Sub extends Super {
// Blank final, set by constructor
private final Instant instant;
Sub() {
instant = Instant.now();
}
// Overriding method invoked by superclass constructor
@Override public void overrideMe() {
System.out.println(instant);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
3. Cloneable과 Serializable 인터페이스는 상속용 설계를 어렵게 합니다.
4. 상속을 고려하지 않은 구체 클래스는 상속을 금지해야 합니다. final class를 만들거나 생성자를 private이나 package-private로 선언하고 정적 팩토리 메서드를 활용하는 것이 좋습니다.
[item-20] 추상 클래스보다는 인터페이스를 우선해라
자바 8부터는 인터페이스에 default method를 제공화기 때문에, 두 방식 모두 인스턴스 메서드를 제공할 수 있습니다.
추상클래스 특징
- 추상 클래스는 좀 더 상세한 구현과 필드를 가질 수 있습니다.
- 인터페이스와 달리 다중상속은 불가능합니다.
인터페이스의 장점
- 기존 클래스에 손쉽게 새로운 인터페이스를 구현해 넣을 수 있습니다. (ex) Comparable, Iterable, AutoCloseable ..)
- 계층 구조가 없는 타입 프레임워크를 만들 수 있습니다.
public interface Singer {
AudioClip sing(Song s);
}
public interface Songwriter {
Song compose(int chartPosition);
}
public interface SingerSongwriter extends Singer, Songwriter {
AudioClip strum();
void actSensitive();
}
3. 인터페이스의 메서드 중 구현 방법이 명백한 것이 있다면, 그 구현을 디폴트 메서드로 제공할 수 있습니다. 디폴트 메서드는 상속하려는 사람을 위해 @implSpec 자바독 태그를 활용
[item-21] 인터페이스는 구현하는 쪽을 생각해 설계하라
- default method가 있더라도 이를 추가하는 데는 많은 숙고가 필요합니다.
- 인터페이스를 구현한 후 나중에 디폴트 메서드를 추가하면, 기존 구현체들과 연동될 것이라는 보장이 없기 때문입니다.
- 일례로 Collections의 removeIf()가 있습니다. 아파치의 SynchronizedCollection 클래스는 모든 메서드를 동기화하여 호출하는 역할을 하는데, removeIf()를 재정의하고 있지 않습니다. 이 상황에서 removeIf()를 호출하면, 해당 구현체가 모든 메서드에서 동기화해 주던 역할이 보장되지 못합니다.
- 새로운 인터페이스를 만드는 경우에는 기존 인터페이스에 디폴트 메서드를 추가하는 경우와 달리, 새로운 인터페이스를 만들 땐 표준적인 메서드 구현을 제공하는 유용한 수단입니다.
- 새로운 인터페이스라면, 릴리스 전에 서로 다른 방식으로 최소한 세 가지는 구현하여 테스트를 해봐야 합니다. 또 각각 인터페이스의 인스턴스를 다양한 작업에 활용하는 클라이언트도 여러 개 만들어 봐야 합니다.
[item-22] 인터페이스는 타입을 정의하는 용도로만 사용하라
상수 인터페이스
- 인터페이스는 클래스가 해당 클래스로 무엇을 할 수 있는지 클라이언트에게 설명해 주는 역할을 담당합니다.
- 이외의 용도로는 사용하면 안 됩니다.
- 상수 인터페이스: 메서드 없이 static final 상수로만 차있는 인터페이스
- 안티패턴(anti-pattern) → 소프트웨어 개발과 관련된 맥락에서 일반적으로 비효율적이고 문제가 발생하기 쉬운 관행, 접근 방식 또는 설계를 의미합니다
// Constant interface antipattern - do not use!
public interface PhysicalConstants {
// Avogadro's number (1/mol)
static final double AVOGADROS_NUMBER = 6.022_140_857e23;
// Boltzmann constant (J/K)
static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
// Mass of the electron (kg)
static final double ELECTRON_MASS = 9.109_383_56e-31;
}
문제점
- 해당 인터페이스를 활용하면 내부 구현이 해당 인터페이스에 종속적이게 됩니다. 이후 릴리즈에서 이 상수를 사용하지 않더라도, 호환성을 위해 계속 상수 인터페이스를 구현해야 합니다.
대안
- 클래스나 인터페이스 자체에 필요한 상수를 추가
// 1
public class PhysicalConstants {
private PhysicalConstants() { } // Prevents instantiation
public static final double AVOGADROS_NUMBER = 6.022_140_857e23;
public static final double BOLTZMANN_CONST = 1.380_648_52e-23;
public static final double ELECTRON_MASS = 9.109_383_56e-31;
}
// 2
public enum Day{ MON, TUE, WED, THU, FRI, SAT, SUN};
public class ConstantsUser {
public static void main(String[] args) {
// import static을 사용하여 상수 사용
double avogadro = AVOGARDROS_NUMBER;
double boltzmann = BOLTZMANN_CONSTANT;
double electronMass = ELECTRON_MASS;
// 상수 값을 출력
System.out.println("Avogadro's Number: " + avogadro);
System.out.println("Boltzmann Constant: " + boltzmann);
System.out.println("Electron Mass: " + electronMass);
}
}
[item-23] 태그 달린 클래스보다는 클래스 계층구조를 활용하라
태그달린 클래스
// Tagged class - vastly inferior to a class hierarchy!
class Figure {
enum Shape { RECTANGLE, CIRCLE };
// Tag field - the shape of this figure
final Shape shape;
// These fields are used only if shape is RECTANGLE
double length;
double width;
// This field is used only if shape is CIRCLE
double radius;
// Constructor for circle
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// Constructor for rectangle
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);
}
}
}
태그달린 클래스의 단점
- enum 타입 선언, 태그 필드, 스위치문 등 쓸데없는 코드가 많아집니다.
- 여러 구현이 한 클래스에 몰려 있으니 가독성이 나쁘고 메모리를 많이 사용합니다.
- 필드들을 final로 사용하려면 쓰이지 않는 필드까지 생성자에서 초기화해야 합니다.
- 또 다른 의미를 추가하려면 코드 전체를 수정해야 합니다.
- 인스턴스의 타입만으로는 현재 어떤 의미를 가지는지 알 길이 없습니다.
// Class hierarchy replacement for a tagged class
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; }
}
[item-24] 멤버 클래스는 되도록 static으로 만들라
중첩 클래스 (nested class)
- 하나의 클래스 안에 정의된 다른 클래스를 의미합니다.
- 중첩 클래스는 자신을 감싼 바깥 객체에서만 쓰여야 하며, 그 외의 용도가 있다면, 톱레벨 클래스로 만들어야 합니다.
정적 멤버 클래스 (static member class)
- 부모 객체의 private 필드에 접근할 수 있다는 점, 내부 객체가 호출되는 시점에 클래스가 로드된다는 점만 제외하면 탑레벨 클래스와 같습니다.
- 컴패니언 객체로 자주 쓰이며, 다른 정적 멤버와 똑같은 규칙을 적용받습니다.
멤버 클래스 (instance member class)
- 바깥 인스턴스와 비정적 멤버클래스는 암묵적으로 연결됩니다. 그러므로 this. 를 활용할 수 있게 됩니다. 그 말은, 바깥 인스턴스가 있어야만 생성이 가능하다는 것입니다.
- 하지만, 바깥 인스턴스에 접근할 일이 없다면, 무조건 정적 메서드로 만드는 것이 좋습니다 (외부 객체를 향한 숨은 참조가 생기고, 메모리 누수의 원인이 되기 때문입니다)
익명 클래스 (anonymous class)
- 익명 클래스는 선언한 지점에서 만드는 구현체입니다. 인터페이스르 사용단에서 직접 구현하거나 단 한 번만 사용될 Override용 코드를 활용할 때 쓰입니다.
지역 클래스 (inner class)
- 지역 클래스는 지역 변수와 마찬가지로 일회성으로 클래스를 선언할 수 있는 문법
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] 3장. 모든 객체의 공통 메서드 (0) | 2024.08.08 |
---|---|
[Effective Java] 2장. 객체 생성과 파괴 (0) | 2024.08.08 |