[Effective Java] item17 - 변경 가능성을 최소화하라

신민철·2023년 4월 24일
1

Effective Java

목록 보기
17/23
post-thumbnail

불변 클래스라 함은 인스턴스의 내부 값을 수정할 수 없는 클래스를 의미한다. 이는 생성부터 객체가 파괴될 때까지 변함없다.

다음은 클래스를 불변으로 만들기 위한 다섯 가지 규칙이다.

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

  • 클래스를 확장할 수 없도록 한다: 상속을 막는 대표적 예는 final 키워드이다.

  • 모든 필드를 final로 선언한다: 설계자의 의도를 명확히 파악할 수 있다. 새로 생성된 인스턴스를 동기화 없이 스레드로 건네도 문제가 없다.

  • 모든 필드를 private으로 선언한다: 필드가 참조하는 가변 객체에 클라이언트가 직접 접근해 수정하는 일을 막아준다. public final만 해도 불변 객체가 되지만 다음 릴리즈에서 표현을 바꾸지 못하는 단점이 있다.

  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다: 필드 자체에 접근하지 못하도록 하는 대신 방어적 복사를 수행하자.


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 divideBy(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 o) {
		if (o == this)
			return true;
		if (!(o instanceof Complex))
			return false;
		Complex c = (Complex) o;
		return Double.compare(re, c.re) == 0 && Double.compare(im, c.im) == 0;
	}

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

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

해당 클래스는 복소수(실수 + 허수)를 표현한다. 사칙연산 메소드가 존재하는데 여기서 인스턴스 자신이 아니라 새로운 객체를 반환하는 것에 주목하자.

이렇게 피연산자 자체는 그대로이고 함수를 적용해 반환하는 패턴을 함수형 프로그래밍이라 한다.

그리고 메소드명이 **add와 같은 동사가 아니라 plus와 같은 전치사**로 쓰게 되면 해당 메소드가 객체의 값을 변경하지 않는다는 것을 강조할 수 있다!

불변 객체는 기본적으로 Thread-safe해서 공유를 해도 문제가 없다.


public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);

이런 식으로 자주 사용되는 인스턴스는 상수(static final)로 선언하여 재활용을 하는 것도 좋은 방법이다.

이 외에도 자주 사용되는 인스턴스를 캐싱하여 중복 생성하지 않게 해주는 정적 팩토리를 사용할 수 있다.

또한 불변 객체는 자유롭게 공유할 수 있다는 뜻은 방어적 복사가 필요 없다는 뜻이다. String 클래스에는 clone 메소드가 있는데 이러한 사실이 잘 이해하지 못했을 때 개발이 되었기에 사용할 필요가 없다!

또한 객체를 만들 때 다른 불변 객체를 구성요소로 사용하면 불변식을 유지하기 굉장히 편하다! 또한 그 자체로 실패 원자성을 제공한다.

불변 클래스에는 단점도 있는데, 값이 달라지면 객체를 새로 생성해야 한다는 점이다!


BigInteger 클래스의 극단적인 예를 보자.

BigInteger num = ...;
num = num.flipBit(0);

만약 백만 비트짜리 num의 bit를 뒤집는다면 어떻게 될까? 단지 한 비트만 차이가 나는데도 다른 백만 비트짜리 인스턴스를 생성하게 되는 것이다.. BitSet을 이용하면 해결되긴 하는데 이마저도 중간 과정에서 비효율이 발생하게 된다.


다음은 불변 클래스의 설계 방법이다.

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 static Complex valueOf(double re, double im) {
		return new Complex(re, im);
	}

	...
} 

이 방식이 일반적으로 최선일 때가 많다. 다른 클래스에서 확장도 불가능하다. 정적 팩토리 방식은 다수의 구현 클래스를 활용한 유연성을 제공하고, 다음 릴리즈에서 캐싱 기능을 추가하여 제공할 수도 있다.

BigInteger나 BigDecimal은 사실 불변 객체이지만 설계 당시에 final이어야 한다는 개념이 정립되지 않았어서 설계상의 오류가 있다. 그래서 방어적 복사를 통해 사용해야 할 것이다.


그런데 모든 불변 객체는 모든 필드가 final이고 어떤 메소드도 객체를 수정할 수 없다는건 너무 과한 느낌이지 않을까?

“그래서 외부에 노출되는 값을 변경할 수 없다.”라고 완화할 수 있다.

직렬화할 때 불변 클래스 내의 가변 객체를 참조하는 필드가 있으면 readObject나 readResolve메소드를 제공하거나, ObjectOutputStream.writeUnshared와 ObjectInputStream.readUnshared 메소드를 사용해야 한다. 그렇지 않으면 클래스 내에 가변 인스턴스를 만들어 공격할 수도 있다.

클래스는 꼭 필요한 경우가 아니면 setter를 만들지는 말자.

그리고 마지막으로 모든 클래스를 불변 클래스로 만들 수는 없다. 그래서 최대한 변경할 수 있는 부분을 최소로 줄여보자. 객체의 상태의 수를 줄이면 객체를 예측하기 쉬워지고 오류 가능성이 줄어든다.

따라서 합당한 이유가 없으면 필드는 private final이어야 한다.

또한 생성자는 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다. 이유가 없다면 초기화 메소드도 public으로 주어지면 안된다. 복잡성은 높아지고 성능상 이점은 없다!

0개의 댓글