Java/Effective Java

[Effective Java] 4장. 클래스와 인터페이스

SeungbeomKim 2024. 8. 8. 23:50

[item-15] 클래스와 멤버 접근 권한을 최소화하라

 

객체지향의 핵심: 캡슐화, 정보은닉

 

캡슐화의 장점

  1. 시스템 개발 속도 향상
  2. 시스템 관리비용 절감
  3. 성능 최적화
  4. 코드 재사용성
  5. 큰 시스템을 제작하는 난이도를 낮춰줍니다
  • 잘 설계된 객체는 모든 내부 구현을 완벽히 숨겨, 실제로 구현한 코드와 외부의 사용자가 사용하는 코드를 깔끔하게 분리합니다.
  • 외부에 공개한 메서드를 통해서만 다른 객체들과 소통하며 서로의 내부 동작방식에는 전혀 개의치 않습니다

 

[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 필드는 데이터 필드에 직접 변경할 수 있어 캡슐화의 이점을 얻지 못합니다.
  1. 필드에 대한 불변식이 깨지게 됩니다.
  2. API를 변경해야만 표현 방식을 바꿀 수 있습니다.
  3. 필드를 읽을 때 부수작업을 수행할 수 없습니다.
  • 접근자와 제어자를 활용하면 클래스 내부 표현을 자유롭게 바꿀 수 있는 유연성을 얻을 수 있습니다. public 클래스가 필드를 공개하면, 이를 사용하는 클라이언트가 생겨날 것이므로 내부 표현 방식을 바꿀 수 없게 됩니다.

 

예외

  • package-private, private 중첩 클래스
  • getter, setter > public 필드

[item-17] 변경 가능성을 최소화하라

 

불변 클래스 : 인스턴스의 내부 값을 수정할 수 없는 클래스

장점

  • 가변 클래스보다 설계하고 쉽고, 구현하고 사용하기 쉽습니다.
  • 불변 객체는 근본적으로 스레드에 안전하기 때문에 따로 동기화할 필요가 없습니다.
  • 불변 클래스는 재활용할 수 있고, 캐시를 활용해 성능을 개선할 수 있습니다.
  • 방어적 복사도 필요 없어집니다.

단점

  • 값이 다르면 반드시 독립된 객체로 만들어야 한다는 것이 단점으로 작용할 수 있습니다.
  • 이들을 모두 만드는데 큰 비용이 듭니다.

불변 클래스를 만드는 방법

  1. 객체의 상태를 변경하는 메서드를 제공 X(변경자 X)
  2. 클래스를 확장할 수 없도록 합니다.
  3. 모든 필드를 final로 선언합니다.
  4. 모든 필드를 private으로 선언합니다.
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 합니다.

확장하지 못하도록 하는 방법 (상속 X)

  1. final class로 만들고, 더 유연한 방법으로는 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩터리를 제공하는 방법
  2. 바깥에서 볼 수 없는 package-private 구현 클래스를 원하는 만큼 만들어 활용할 수 있습니다

불변과 관련된 주의사항

  1. 클래스는 꼭 필요한 경우가 아니라면 불변이여야 합니다
  2. 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄여야 합니다
  3. 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 합니다

 

[item-18] 상속보다는 컴포지션을 사용하라

 

상속의 장점

  1. 코드를 재사용함으로써 중복을 줄일 수 있습니다
  2. 변화에 대한 유연성 및 확장성이 증가합니다
  3. 개발 시간이 단축됩니다

상속의 단점

  • 캡슐화를 깨뜨립니다
  • 캡슐화: 만일의 상황(타인이 외부에서 조작)에 대비해 외부에서 특정 속성이나 메서드를 사용할 수 없도록 숨겨놓는 것
  • 상위 클래스의 구현이 하위 클래스에게 노출되는 상속은 캡슐화를 깨뜨립니다. 캡슐화가 깨짐으로써 하위 클래스가 상위 클래스에 강하게 결합, 의존하게 됩니다.
  • 상위 클래스의 요구사항이 변경되면, 하위 클래스에서 모두 수정해야 합니다.

조합 (Composition)

  • 조합: 기존 클래스가 새로운 클래스의 구성요소로 쓰이는 경우
  • 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조

조합을 사용하면?

  1. 메서드를 호출하는 방식으로 동작하기 때문에 캡슐화를 깨뜨리지 않습니다
  2. Lotto 클래스와 같은 기존 클래스의 변화에 영향이 적어지며, 안전합니다.
  3. 상속의 문제점에서 벗어날 수 있습니다.

결론

캡슐화를 깨뜨리고, 상위 클래스에 의존하게 돼서 변화에 유연하지 못한 상속을 사용하기보다는 조합을 사용

상속을 사용해야 하는 경우

  1. 확장을 고려하고 설계한 확실한 is-a 관계
  2. API에 아무런 결함이 없는 경우, 결함이 있다면 하위 클래스까지 전파돼도 괜찮은 경우

[item-19] 상속을 고려해 설계하고 문서화해라. 그러지 않았다면 상속을 금지해라

 

상속시 주의점

  1. 상속용 클래스의 생성자는 재정의 가능한 메서드(non-private, non-final, non-static)를 호출하면 안 됩니다.
  2. 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로, 하위 클래스에서 재정의해버린 메서드가 하위 클래스의 생성자보다 먼저 호출됩니다. 이때 하위 생성자에서 초기화하는 값에 의존한다면 의도대로 동작 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를 제공화기 때문에, 두 방식 모두 인스턴스 메서드를 제공할 수 있습니다.

추상클래스 특징

  • 추상 클래스는 좀 더 상세한 구현과 필드를 가질 수 있습니다.
  • 인터페이스와 달리 다중상속은 불가능합니다.

인터페이스의 장점

  1. 기존 클래스에 손쉽게 새로운 인터페이스를 구현해 넣을 수 있습니다. (ex) Comparable, Iterable, AutoCloseable ..)
  2. 계층 구조가 없는 타입 프레임워크를 만들 수 있습니다.
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. 해당 인터페이스를 활용하면 내부 구현이 해당 인터페이스에 종속적이게 됩니다. 이후 릴리즈에서 이 상수를 사용하지 않더라도, 호환성을 위해 계속 상수 인터페이스를 구현해야 합니다.

대안

  • 클래스나 인터페이스 자체에 필요한 상수를 추가
// 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);
		}
	}
}

 

태그달린 클래스의 단점

  1. enum 타입 선언, 태그 필드, 스위치문 등 쓸데없는 코드가 많아집니다.
  2. 여러 구현이 한 클래스에 몰려 있으니 가독성이 나쁘고 메모리를 많이 사용합니다.
  3. 필드들을 final로 사용하려면 쓰이지 않는 필드까지 생성자에서 초기화해야 합니다.
  4. 또 다른 의미를 추가하려면 코드 전체를 수정해야 합니다.
  5. 인스턴스의 타입만으로는 현재 어떤 의미를 가지는지 알 길이 없습니다.

 

// 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)

  • 지역 클래스는 지역 변수와 마찬가지로 일회성으로 클래스를 선언할 수 있는 문법