책 읽고 정리하기/2022

Effective Java 3/E - 4장 클래스와 인터페이스 - 1

TwinParadox 2022. 1. 16. 13:21
728x90

15. 클래스와 멤버의 접근 권한을 최소화하라

아주 기본적인 내용이지만, 처음 입문했을 때 모든 내부 구현 정보를 외부에 제공하던 설계를 만들었던 시기가 생각나는 아이템이었다. 결론적으로, 잘 설계된 컴포넌트는 모든 내부 구현을 완벽히 숨겨서 구현과 API를 깔끔히 분리하고, 오직 API를 통해서만 다른 컴포넌트와 소통하고 내부의 동작에는 상호 개의치 않는 설계가 필요하다.

우리가 흔히 이야기하는 이 개념은 정보 은닉, 캡슐화로, 설계의 근간이다.

 

장점

시스템 개발 속도를 높인다.

여러 컴포넌트가 병렬로 개발할 수 있기 때문에

 

시스템 관리 비용을 낮춘다.

컴포넌트를 더 빨리 파악해, 디버깅하고 다른 컴포넌트로의 교체도 부담 적음

 

성능 최적화 단계에서 도움이 될 수 있다.

완성된 시스템을 프로파일링해 최적화할 컴포넌트를 정하고, 이를 다른 컴포넌트에 영향 없이 최적화 가능

 

재사용성을 높인다.

외부에 의존하지 않아서 독자적으로 동작할 수 있는 컴포넌트는 다른 환경에서 유용

 

큰 시스템의 제작 난이도를 낮춘다.

큰 시스템을 개별 컴포넌트 단위로 검증하며 개발 가능

 

자바에서는 이 캡슐화를 위해서 클래스, 인터페이스, 멤버의 접근 제한자(Access Modifier)를 제공하여 추구한다.

모든 클래스와 멤버의 접근성은 가능한 최대한 제한하는 것에서 출발한다.

 

Top-Level 클래스와 인터페이스에 부여할 수 있는 접근 수준

  • public으로 하면 공개 API
  • package-private은 해당 패키지에서만 사용 가능

외부에서 사용될 대상이 아니면, package-private으로 선언해서 내부 구현으로 가져가야 한다. 한 클래스에서만 사용하는 package-private Top-Level 클래스/인터페이스는 사용하는 클래스 안에 private static으로 중첩시켜 하나의 클래스를 통해서만 접근할 수 있게끔 처리 가능하다. 

 

필드, 메서드, 중첩 클래스/인터페이스에 부여 가능한 접근 수준

private

  • 멤버를 선언한 클래스에서만 접근 가능

package-private

  • 멤버가 소속된 패키지 안 모든 클래스에서 접근 가능
  • 명시하지 않은 경우 접근 수준(단, 인터페이스는 public이 기본)

protected

  • package-private 범위를 포함하고, 멤버를 선언한 클래스의 하위 클래스에서도 접근 가능

public

  • 모든 곳에서 접근 가능

 

클래스의 공개 API를 신중하게 설계하고, 공개 대상이 아닌 멤버들은 모두 private으로 만든다.

같은 패키지의 다른 클래스가 접근해야 하는 멤버에 한해서만, package-private(private 제거한 상태)로 만들어서, 권한을 풀어준다. 이런 작업이 반복되는 경우 클래스 분리를 고려해야 한다. public 클래스에서는 멤버의 접근 수준을 protected 단계로 올리는 순간, 그 멤버에 접근 가능한 대상 범위가 늘어난다. 공개 API가 되면서, 이에 대한 지원이 필요하다.

 

public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.

필드가 가변 객체를 참조하거나 final이 아닌 인스턴스 필드를 public으로 선언했을 때 값을 제한할 수 있는 힘이 사라지면서, 그 필드와 관련된 모든 것이 불변식임을 보장할 수 없게 된다. 필드가 수정되는 상황에서 작업이 불가능해져서, public 가변 필드를 갖게 되면 일반적으로 스레드 안전하지 않다. 우회하기 위해서 final을 붙이고, 불변 객체를 참조해도, 내부 구현을 바꾸고 싶어도 public 필드를 없애는 방식으로 리팩토링이 불가능하다.

 

public static final로 정의하는 상수에 대해서는 허용된다.

이들 필드는 기본 타입 값이나 불변 객체를 참조하도록 해야하며 배열 같은 형태로 두거나 이를 반환하는 메서드를 두지 않도록 해야 한다. 배열 형태의 값이 필요하다면, 다음 방법으로 대응한다.

  • 불변 리스트로 전환
  • 배열을 private으로 제한하고, 그 복사본을 반환하는 public 메서드를 제공

 

Java 9의 모듈 시스템

모듈에 대해서 자신이 속하는 패키지 중 공개할 것들을 선언한다. protected나 public 멤버라도 해당 패키지를 공개하지 않으면 모듈 외부에서는 접근이 불가능하고 오로지 모듈 내부에서만 가능하다. 모듈 시스템을 통해서 클래스를 외부에 공개하지 않고도 같은 모듈 내에서 패키지 사이의 공유가 가능해진다.

 

모듈에서는 public클래스의 public과 protected의 수준에 대해 신중히 접근해야 한다.

모듈의 jar 파일을 모듈 경로가 아닌 애플리케이션의 classpath에 두면 그 모듈 안의 모든 패키지는 모듈이 없는 것처럼 행동해서, 모듈 공개 여부와 무관하게 public 클래스에 있는 public, protected에 대해 모듈 밖에서도 접근이 가능해진다. JDK가 이를 적극 활용한 대표적인 예로, 자바 라이브러리에서 공개하지 않은 패키지들은 해당 모듈 밖에서 절대 접근이 불가능하다.

 

적용에는 신중해야 한다.

패키지를 모듈 단위로 묶고, 모듈 선언에 패키지들의 모든 의존성을 명시하는 등의 조치들이 필요하고, 필요한 경우가 아니라면 자제하는 것이 좋다.

 

 

16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라.

절대 가변 필드를 public으로 제공하지 않아야 한다. getter를 제공하여, 클래스 내부 표현 방식을 어떻게든 변경할 수 있는 유연성을 가져가며 이점을 유지해야 한다.

 

public 클래스와 달리, package-private 클래스나, private 중첩 클래스인 경우에는 필드를 노출해도 상관 없다. package-private의 경우, 이 클래스를 포함하는 패키지 안에서만 코드가 동작하고 있고, 패키지 바깥 코드의 수정 없이 데이터의 표현 방식을 변경할 수 있다. private이라면 더 제한적으로 들어가, 해당 클래스를 포함하는 외부 클래스까지로 제한하게 된다.

 

public 클래스의 불변 필드는 불변식 보장이 가능하지만, 표현 방식 변경이 곧 API 변경이며, 필드를 읽는 과정에서의 추가 작업이 불가능하다.

 

 

17. 변경 가능성을 최소화하라

불변 클래스는 내부 값을 수정할 수 없는 클래스로, 정보가 고정되어 객체가 소멸되는 순간까지 절대 달라지지 않아야 한다. String이나 기본 타입의 박싱 클래스들, BigInteger/BigDecimal이 이에 속한다.

 

불변 클래스를 위한 다섯 가지 규칙

객체의 상태를 변경하는 메서드를 제공하지 않는다.

클래스를 확장할 수 없게 한다.

  • 하위 클래스에서 변경할 수 있는 여지 제거

모든 필드를 final로 선언한다.

  • 시스템이 강제하는 수단으로 의도를 드러내는 명확한 방법
  • 새로 생성된 인스턴스를 동기화를 고려하지 않아도 됨

모든 필드를 private으로 선언한다.

  • 필드가 참조하는 가변 객체를 직접 수정하는 일을 방지
  • public final로 선언해도 불변 객체가 될 순 있지만, 내부 표현을 바꾸지 못함

자신 외에는 내부의 가변 컴포넌트에 접근할 수 없게 한다.

  • 가변 객체를 하나라도 참조하면, 그 객체의 참조를 획득 불가 처리
  • 해당 필드가 클라이언트가 제공한 객체를 참조하거나, getter가 그 필드를 그대로 반환할 수 없도록
  • 생성자나, getter, 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 realPart() { return re; }
	public double imaginaryPart() {	return im; }

	public Complex plus(Complex c) { return new Complex(re + c.re, im + c.im); }
	public Complex minus(Complex c) { return new Complex(re - c.re, im - c.im); }
	public Complex times(Complex c) {
		return new Complex(re * c.re - im * c.im,
			re * c.im + im * c.re);
	}
	public Complex dividedBy(Complex c) {
		double tmp = c.re * c.re + c.im * c.im;
		return new Complex((re * c.re + im * c.im) / tmp,
			(im * c.re - re * c.im) / tmp);
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == this) { return true; }
		if (!(obj instanceof  Complex)) { return false; }

		Complex c = (Complex) obj;

		return Double.compare(c.re, re) == 0 && Double.compare(c.im , im) == 0;
	}

	@Override
	public int hashCode() { return 31 * Double.hashCode(re) + Double.hashCode(im); }

	@Override
	public String toString() { return "(" + re + " + " + im + "i)"; }
}

Complex를 반환하는 모든 메서드들이 자기 자신을 수정해서 반환하는 것이 아닌, 새로운 인스턴스를 생성해 반환하고 있다. 이런 식으로 피연산자 자체는 그대인 상태로 반환되는 방식을 함수형 프로그래밍이라고 하며(반대는 절차형/명령형 프로그래밍), 이런 방식이 코드에서 불변인 영역의 비율을 높여준다.

 

불변 객체는 단순하다.

생성된 시점의 상태를 소멸되는 시점까지 가져간다. 모든 생성자가 클래스 불변식을 보장할 때, 그 클래스를 사용하는 입장에서는 불변을 위한 다른 작업이 필요 없다. 

 

불변 객체는 근본적으로 스레드 안전하고 동기화가 필요 없다.

스레드가 동시에 사용해도 훼손되지 않는다. 이것이 결국 클래스를 스레드 안전하게 만드는 가장 쉬운 방법임을 의미하며, 스레드 안전하기 때문에 공유하여 사용하는 것도 가능하다. 불변 객체를 자유롭게 공유하므로, 방어적 복사도 필요 없고, clone 메서드나 복사 생성자의 제공도 필요 없다. 훼손이나 공유 관련 문제가 없기 때문에, 한 번 만든 불변 클래스의 인스턴스는 계속 재활용하는 것이 좋다. public static final을 고려해보는 것이 가장 간단한 방법이다. 값이 좀 더 있다고 하면, 사용되는 인스턴스를 캐싱해서 정적 팩토리 메서드로 제공이 가능하다. 

 

불변 객체끼리 내부 데이터 공유가 가능하다.

BigInteger 클래스는 내부에서 값의 부호와 크기를 따로 표현하는데, 부호에 int 크기에 int 배열을 사용한다. negate 메서드에서는 크기가 같지만, 부호만 반전시켜 BigInteger를 생성하는데, 배열은 가변이어도, 복사하지 않고 원본 인스턴스와 공유하는 방식으로 구현되어 있다. 이렇게 객체 생성 과정에서 다른 불변 객체를 구성요소로 사용하게 되면, 불변식 유지가 쉽다.

public BigInteger negate() {
    return new BigInteger(this.mag, -this.signum);
}
BigInteger(int[] magnitude, int signum) {
    this.signum = magnitude.length == 0 ? 0 : signum;
    this.mag = magnitude;
    if (this.mag.length >= 67108864) {
        this.checkRange();
    }

}

 

 

불변 객체 그 자체가 실패 원자성을 제공한다.

상태가 불변이기 때문에 불일치 상태에 있을 일이 없다.

 

불변 클래스는 하나라도 다르다면, 서로 다른 독립된 객체여야 한다.

값이 1개라도 다른 만큼, 그 만큼의 객체가 필요하다. 이러한 문제를 극복하기 위해서는 크게 두 가지 방법을 사용한다.

 

다단계 연산을 예측하여, 기본 기능으로 제공한다.

각 단계마다 값이 달라진다고 객체를 생성하지 않고, pakcage-private의 가변 동반 클래스를 사용하여 대응한다. 물론 가변 동반 클래스를 직접 사용하면 복잡하겠지만, BigInteger 같은 클래스들은 알아서 잘 처리해준다.

 

연산 예측이 불가능하다면, 클래스를 public으로 제공한다.

String 클래스가 대표적인 예로, StringBuilder와 StringBuffer가 이에 해당한다.

 

 

정적 팩토리로 제공하는 불변 객체

public static Complex valueOf(double re, double im) {
	return new Complex(re, im);
}

클래스의 불변을 보장하기 위해 상속하지 못하게 하려면, final 클래스로 선언하는 방법보다 유연한 방법이 있다. 모든 생성자를 private, package-private으로 제한하고 public인 정적 팩토리를 제공하는 방법으로 대응이 가능하다. 대부분의 경우 이 방식이 최선이다.

 

BigInteger와 BigDecimal을 사용할 때 주의하라

이들 클래스는 설계 당시 불변 객체가 사실상 final이어야 된다는 개념이 널리 퍼지지 않아서, 모두 재정의 가능하게 설계되어 있다. 이들을 파라미터로 받는 경우에, 이들이 확장한 클래스가 아닌 그 원본 클래스인지 확인해야 한다. 만약 확인이 불가능하거나, 변형된 것이라면 가변 클래스로 간주해야 한다.

 

 

요약하면...

  • Getter가 있다고 Setter가 꼭 있어야 하는 건 아니다.
  • 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소화해야 한다.
  • 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
728x90
728x90