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

불변 클래스란?

  • 인스턴스 내부 값을 수정할 수 없는 클래스

  • 불변 클래스 인스턴스의 정보는 객체가 파괴되는 순간까지 절대 변하지 않는다

  • ex) String, 기본 타입의 박싱된 클래스(Integer, ...), BigInteger, BigDecimal

  • 가변 클래스보다 설계, 구현, 사용이 쉽다.

  • 오류 생길 여지가 적고 안전하다.

  • 꼭 필요한 경우가 아니라면 불변 클래스로 만들고, 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소화할 것.

  • 생성자는 불변식 설정이 모두 완료되어 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.

불변 클래스의 5가지 규칙

  • 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다

  • 클래스를 확장할 수 없도록 한다

    • 하위 클래스에서 부주의하게 혹은 나쁜 의도로 객체의 상태를 변하게 만들지 못하도록 함

    • 상속을 막는 대표적인 방법은 클래스를 final로 선언하는 것

  • 모든 필드를 final로 선언

    • 시스템이 강제하는 수단을 이용해 설계자의 의도를 명확히 드러내는 방법

    • 새로 생성된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작하게끔 보장하는 데 필요

    • 하지만 final로 선언된 변수에 변경 가능한 객체가 지정되어 있다면, 해당 변수에 들어있는 객체의 값을 사용하는 부분을 모두 동기화 시켜야 한다.

    • 성능을 위해 "객체의 상태 중 외부에 보이는 값을 변경하는 메서드는 있으면 안된다" 고 완화할 수 있다.

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

    • 필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아준다.(item15,16)

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

    • 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면, 클라이언트에서 해당 가변 객체의 참조를 얻을 수 없도록 해야 한다.

    • 생성자, 접근자, readObject 메서드(item88)에서 필드를 그대로 반환하면 안되며 방어적 복사를 수행해야 한다.

함수형 프로그래밍

  • 피연산자에 함수를 적용해 결과를 반환하지만 피연산자 자체는 그대로인 프로그래밍 패턴. (절차적/명령형 프로그래밍에서는 메서드에서 피연산자인 자기 자신을 수정해 상태가 변화된다.)

  • 불변이 되는 영역이 넓어진다.

  • 아래 예시는 Complex 객체끼리 더할 때 기존 피연산자를 수정하는 것이 아닌 새로운 인스턴스를 반환하는 메서드이다.

public Complex plus(Complex c) {
	return new Complex(re + c.re, im + c.im);
}

불변 객체의 장점

  • 불변 객체는 단순하다

    • 가변 객체는 임의의 복잡한 상태에 놓일 수 있기 때문에 가변 객체는 믿고 사용하기 어렵다.

    • 불변 객체는 생성된 시점의 상태를 파괴될 때까지 그대로 가지므로 믿고 사용할 수 있다.

  • 근본적으로 스레드 안전하여 동기화 할 필요가 없다

    • 여러 스레드가 동시에 사용해도 절대 훼손되지 않는다.

    • 클래스를 thread safe하게 만드는 가장 쉬운 방법이기도 하다.

  • 불변객체는 안심하고 공유 가능

    • 스레드 간 영향을 주고받을 수 없기 때문이다.

    • 아무리 복사해봐야 원본과 똑같아 방어적 복사가 필요 없으므로, clone 메서드나 복사 생성자를 제공하지 않는 게 좋다.

    • 같은 맥락에서 String 클래스는 불변 객체이므로 복사 생성자는 되도록 사용하지 말 것

  • 한번 만든 인스턴스 최대한 재활용

    • 자주 사용되는 인스턴스를 캐싱해 메모리 사용량과 가비지 컬렉션 비용을 줄인다.

    • 자주 쓰이는 값들을 상수로 제공하여 캐싱할 수 있다. (ex. BigInteger, Wrapper class 같이 박싱된 기본 타입 클래스 전부)

    public static final Complex ZERO = new Complex(0,0);
    
    //사용 방식
    Object o = Complex.ZERO;
    • public 생성자 대신 정적 팩토리를 제공하여 캐싱하면 클라이언트 수정이 없이 필요에 따라 캐시 기능을 덧붙일 수 있다.

  • 불변 객체끼리 내부 데이터 공유 가능

    • BigInteger 클래스는 부호(sign, int변수)와 크기(magnitude, int배열)를 각각의 필드로 표현한다.

    • 크기는 같고 부호만 반대로 표현하는 negate 메서드는 새로운 BigInteger를 생성하는데, 가변인 배열을 복사하는 대신 원본 인스턴스와 공유한다. 따라서 새로운 인스턴스는 원본 인스턴스가 가리키는 내부 배열을 그대로 가리킨다.

public BigInteger negate() {
        return new BigInteger(this.mag, -this.signum);
}

Map과 Set은 값이 바뀌지 않는 구성요소들(keySet, elements)로 이루어져 있다. 이러한 상황에서 불변 객체를 사용하면 안에 담긴 값이 바뀔 일이 없어 적합하다.

불변 객체의 단점

  • 값이 다르면 독립된 객체로 만들어야 하므로 비용이 클 수 있다.

    • 특정 상황에서 잠재적 성능 저하가 발생할 수 있다. 예를 들면 백만 비트짜리 인스턴스에서 단 하나의 비트만 변경할 때 새로운 인스턴스를 생성하면 시간과 공간을 잡아먹는다.

    • 원하는 객체를 완성하기까지의 단계가 많고, 그 중간 단계에서 만들어진 객체들이 모두 버려져야 한다면 성능 문제가 극대화된다.

    • 다단계 연산을 제공하거나, 다단계 연산 속도를 높여주는 package-private 가변 동반 클래스를 두어 해결 가능하다. ex) String 클래스의 가변 동반 클래스인 StringBuilder(+StringBuffer)

불변 클래스 만드는 방법

  • final 클래스로 선언해 상속하지 못하도록 불변 클래스를 만들 수 있다.

  • 모든 생성자를 private 혹은 package-private로 만들고 public 정적 팩토리를 제공하는 방식을 사용하면 더욱 유연하다.

  • 패키지 바깥의 클라이언트에서 볼 때 확장이 불가능해 사실상 final 클래스와 같다.

  • 다수의 구현 클래스를 활용하여 유연성을 제공하고, 객체 캐싱 기능을 추가해 성능을 향상시킬 수도 있다.

private Complex(double re, double im) {
	this.re = re;
	this.im = im;
}

public static Complex valueOf(doulble re, double im) {
	return new Complex(re, im);
}
  • 생성자와 정적 팩토리 외에 public으로 초기화 메서드를 제공하면 복잡성만 커지고 성능 이점이 없으므로 사용하지 말 것.

그 외

  • 신뢰할 수 없는 클라이언트로부터 final이 아닌 불변 객체가 있다면, 인수로 받은 객체가 '진짜' 해당 객체인지 확인해야 한다. 신뢰할 수 없는 하위 클래스의 인스턴스는 가변 객체로 구현되었을 수 있기 때문에, 일단 가변으로 가정하고 방어적으로 복사해서 사용해야한다. (item50)

public static BigInteger safeInstance(BigInteger val) {
	return val.getClass() == BigInteger.class ?
		val : new BigInteger(val.toByteArray());
}
  • 직렬화할 때 Serializable을 구현하는 불변 클래스 내부에 가변 객체를 참조하는 필드가 있다면, 공격자가 이 클래스로부터 가변 인스턴스를 만들어낼 수 있다. 따라서 readObject/readResolve 메서드를 반드시 제공하거나, writeUnshared, readUnshared 메서드를 사용해야 한다.(item 88)

Last updated