[Java] Item 17 : 변경 가능성을 최소화하라
Effective Java를 읽고 정리한 정리본입니다.
📌 Item 17 : 변경 가능성을 최소화하라
🫧 불변 클래스
불변 클래스는 그 인스턴스의 내부 값을 수정할 수 없는 클래스이다. 쉽게 말해 한 번 만들어진 이상 값이 바뀌지 않는 클래스를 말한다.
String, 기본 타입의 박싱된 클래스들, BigInteger, BigDecimal이 이러한 불변 클래스에 속한다.
🫧 클래스를 불변으로 만드는 방법
- 객체의 상태를 변경하는 메서드 (변경자)를 제공하지 않는다.
-
클래스를 확장할 수 없도록 한다.
-> 상속을 막는 대표적인 방법은 클래스를 final로 선언하는 것이다. - 모든 필드를 final로 선언한다.
- 모든 필드를 private으로 선언한다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
-> 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야 한다. 또한, 접근자 메서드가 그 필드를 그대로 반환하게 해도 안 된다.
🫧 함수형 프로그래밍
함수형 프로그래밍이란 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 의미한다.
반면, 절처적 혹은 명령형 프로그래밍에서는 메서드에서 피연산자인 자신을 수정해 자신의 상태가 변하게 된다.
✨ 함수형 프로그래밍 예시 코드
public final class Complex {
private final double re;
private final double im;
public Conplex(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 Comliex 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 o) {
if (o == this)
return true;
if (!(o instanceof Complex))
return false;
Complex c = (Complex) o;
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)";
}
}
- 피연산자 자체는 그대로이다.
- 메서드 이름으로 add 같은 동사 대신 plus 같은 전치사를 사용한다.
-> 해당 메서드가 객체의 값을 변경하지 않는다는 사실을 강조함.
코드에서도 확인할 수 있듯, 함수형 프로그래밍은 함수의 순수성 (함수의 참조 불변성)과 변수의 불변성이라는 두 가지 특징을 갖고 있다.
따라, 불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요도 없으며, 안심하고 공유할 수 있게 된다.
🫧 불변 객체(클래스) 장점
- 스레드 안전하여 따로 동기화할 필요가 없다.
- 안심하고 공유할 수 있다.
-
정적 팩터리 제공이 가능하다
-> item 1, 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해준다. 이런 정적 팩터리를 사용하면 여러 클라이언트가 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용이 줄어든다. - 그 자체로 실패 원자성을 제공한다
-> 메서드에서 예외가 발생한 후에도 그 객체는 여전히 메서드 호출 전과 똑같은 유효한 상태여야 한다는 성질. 불변 객체는 내부 상태를 바꾸지 않으므로 이 성질은 만족한다.
🫧 불변 클래스 단점
- 값이 다르면 반드시 독립된 객체로 만들어야 한다.
이러한 문제는 성능 저하를 야기한다. 1번 문제를 해결하는 방법은 크게 두 가지가 있다.
- 흔히 쓰일 다단계 연산들을 예측하여 기본 기능으로 제공하는 방법.
-> 이를 통해 각 단계마다 객체를 생성하지 않아도 된다.
추가로, 불변 클래스는 아무리 복사해 봐야 원본과 똑같으므로 clone 메서드나 복사 생성자를 제공하지 않는 것이 좋다.
🫧 가변 동반 클래스
본래 불변인 객체의 관련값을 수정해 줄 수 있는 클래스를 동반해 제공할 수 있다.
예를 들어 불변 객체인 String의 단점을 보완해 주는 가변 동반 클래스인 StringBuilder, StringBuffer가 있다.
이러한 가변 동반 클래스는 불변 객체의 값이 다르다면 반드시 독립된 객체로 만들어야 한다.
의 문제를 해결하고자 생겨났다.
클라이언트들의 원하는 복잡한 연산을 정확히 예측할 수 있다면 package-private의 가변 동반 클래스로, 그렇지 않다면 public으로 클래스를 제공해야 한다.
🫧 불변 클래스 만들기 - 상속 금지
- final 클래스로 선언
- 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩터리를 제공
✨ 2번 코드 예시
public class Complex {
private final double re;
private final double im;
private Complex(double re, double im) {
this.re = re;
this.im = im;
}
public static Complex valueOf(double re, double im) {
return new Complex (re, im);
}
// ... 생략
}