본문 바로가기
Language/Java

이펙티브 자바 - 변경가능성 최소화

by ocwokocw 2021. 10. 30.

- 이 글은 Effective Java 를 기반으로 작성되었습니다.

- 변경 불가능(immutable) 클래스

변경 불가능 클래스란 수정할 수 없는 클래스를 말하며, 객체가 생성될 때의 정보가 살아있는 동안 그대로 보존된다. 우리가 자주 사용하는 String, 기본 자료형(primitive type), Big Integer, Big Decimal 등이 모두 변경 불가능 클래스에 해당한다.

 

변경 불가능 클래스는 설계와 구현이 쉬우며 Client 가 사용하기도 쉽다. 그리고 오류 발생가능성이 적어 훨씬 안전하다. 변경 불가능 클래스가 지켜야 할 5 가지 규칙은 아래와 같다.

  • 상태 변경 메소드(ex - setter, mutator) 를 제공하지 않는다.
  • 계승(extends) 를 막아야 한다. 보통은 final 을 이용하여 막는다.
  • 모든 field 를 final 로 선언한다. 이렇게 field 에 final 을 선언하면 작성자의 의도가 좀 더 명확해진다.
  • 모든 field 를 private 로 선언한다. 기본 자료형과 변경 불가능 객체 참조를 public 으로 선언해도 변경 불가능 클래스를 만들 수는 있으나 나중에 클래스의 내부 표현을 변경할 수 없게 된다.
  • 변경 가능 컴포넌트에 대한 독점적 접근권을 보장한다. 변경 불가능 클래스를 사용하는 Client 는 변경 가능 객체에 대해 참조획득이 불가능해야 한다. 부득이하게 변경 가능 객체의 정보를 제공해야 한다면 생성자, 접근자, readObject 메소드 안에서 방어적 본사본을 생성해야 한다.

 

public final class Complex {

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

	public double realPart() {
		return re;
	}

	public double imaginaryPart() {
		return im;
	}
	
	public Complex add(double re, double im) {
		return new Complex(this.re + re, this.im + im);
	}

	@Override
	public boolean equals(Object obj) {

		if(obj == this) {
			return true;
		}
		
		if (!(obj instanceof Complex)) {
			return false;
		}
		
		Complex other = (Complex) obj;
		return Double.compare(re, other.re) == 0 && 
				Double.compare(im, other.im) == 0;
	}
}

 

위의 코드는 복소수(Complex) 클래스를 간단하게 작성한것이다. add 메소드를 보면 자신의 객체 상태를 변경하는 대신 새로운 Complex 객체를 생성 후 반환하였다. 자신의 상태를 그냥 변경하면 되는데 왜 자원이 더 소모되는 신규 객체를 생성해서 반환했을까?


- 변경 불가능(immutable) 클래스의 장점

우선 변경 불가능 객체는 단순하다. 그래서 1 가지 상태만 갖는다. 만약 생성자가 불변식을 만족한다면 상태를 변경할 수 없으므로 해당 객체는 살아있는 동안 불변식을 어기지 않는다.

 

쓰레드 안전성(thread-safe)을 만족하며 동기화(synchronized) 를 신경쓸 필요가 없다. 쓰레드 안전성을 만족하므로 여러 쓰레드에 자유롭게 공유할 수 있다. 해당 클래스를 변경 불가능 클래스로 제공하기로 결정했다면 Client 에게 재사용을 유도할 필요가 있다.

 

public static final BigInteger ZERO = new BigInteger(new int[0], 0);

 

이를 더 개선하고 싶다면 정적 팩토리 메소드를 이용하여 cache 값을 제공하면 된다.

 

변경 불가능 객체는 그 내부의 속성도 공유할 수 있다. BigInteger 는 부호는 int 변수로, 크기는 int[] 배열로 구성되어 있다.

 

public class BigInteger extends Number implements Comparable<BigInteger> {
    /**
     * The signum of this BigInteger: -1 for negative, 0 for zero, or
     * 1 for positive.  Note that the BigInteger zero <i>must</i> have
     * a signum of 0.  This is necessary to ensures that there is exactly one
     * representation for each BigInteger value.
     *
     * @serial
     */
    final int signum;

    /**
     * The magnitude of this BigInteger, in <i>big-endian</i> order: the
     * zeroth element of this array is the most-significant int of the
     * magnitude.  The magnitude must be "minimal" in that the most-significant
     * int ({@code mag[0]}) must be non-zero.  This is necessary to
     * ensure that there is exactly one representation for each BigInteger
     * value.  Note that this implies that the BigInteger zero has a
     * zero-length mag array.
     */
    final int[] mag;

 

BitInteger 클래스의 negate 메소드는 크기는 같고 부호만 다른 BigInteger 를 반환한다. 이때 부호와 크기를 둘 다 복사하면 공간의 낭비가 된다.

 

/**
     * Returns a BigInteger whose value is {@code (-this)}.
     *
     * @return {@code -this}
     */
    public BigInteger negate() {
        return new BigInteger(this.mag, -this.signum);
    }

 

BitInteger 는 변경 불가능 클래스이므로 위와 같이 크기 배열을 같이 공유해도 문제가 되지 않는다.

 

또한 변경 불가능 클래스는 다른 객체의 구성요소로도 훌륭하다. Map의 key의 경우 불변 객체를 이용하면 값이 변경되어 불변식이 깨질일이 없다.


- 변경 불가능(immutable) 클래스의 단점

위의 설명까지 보면 변경 불가능 클래스는 단점이 없는 완벽한 클래스인 것 같지만 단점이 존재한다. 값마다 별도의 객체를 생성해야 한다는것이다. 간단한 클래스의 경우 그리 큰 단점이 되지 않지만 만약 복잡한 클래스거나 메모리를 많이 사용하는 클래스라면 비용이 상당히 높을 수 있다. 또한 단계별로 새로운 객체를 생성하다가 실질적으로 마지막 단계의 객체만 사용하는 경우 성능의 문제를 야기할 수 있다.


- 변경 불가능 클래스의 유연한 설계

변경 불가능 클래스 작성시 상황에 따라 유연한 설계법을 적용할 수 있다. private 혹은 package-private 생성자와 public 정적 팩토리 메소드를 제공하는것이다. 이렇게 설계하면 몇 가지 장점이 있는데 아래와 같다.

  • 여러 개의 package-private 구현 클래스를 활용할 수 있게 되어 유연성이 증대된다. 
  • 또한 protected 와 public 생성자가 없으므로 package 외부에서는 상속이 불가하다.
  • public 정적 패토리 메소드를 제공하므로 추가 동작(ex - cache)을 처리할 수 있게된다.

서두에서 어떤 메소드도 객체를 수정해서는 안되며 모든 field 는 final 로 선언한다고 하였는데 이는 다소 과한면이 있다. 이 규칙을 그대로 지켜야 한다면서 무조건 final 로 선언한다기 보다 '어떤 메소드도 외부에서 관측가능한 상태변화를 야기하지 않아야 한다.' 라는 측면으로 이해하는것이 좋다.


- 마치면서

위에서 언급한 세부적인 규칙들은 시간이 지나면 까먹기 마련이다. 가장 중요한 2 가지의 대원칙만 기억하면 변경가능성을 최소화 한다는 규칙을 지킬 수 잇을것이다.

  • 모든 get 메소드에 대해 set 를 무조건 제공하지 말라.
  • 변경 불가능 클래스로 제공하는것이 불가능하다면 변경 가능성을 최대한 제한하라.

댓글