본문 바로가기

STUDY/이펙티브자바

4-3) 변경 가능성을 최소화하라

불변 클래스

 

불변 클래스란 인스턴스 내부 값을 수정할 수 없는 클래스를 말한다. Java에서는 String, BigInteger, BigDecimal 등 여러 클래스가 불변 클래스로 설계되었다. 불변 클래스 방식은 설계 및 구현이 쉽고 사용하기에도 쉽다. 또 오류가 생길 여지도 적어서 훨씬 안전하다. 이러한 장점을 가진 불변 클래스를 만들기 위해서는 다음 다섯가지 규칙을 따르면 된다.

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

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

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

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

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

 

 


불변 복소수 클래스

 

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 divideBy(Complex c) {
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex((re * c.re - im * c.im) / tmp,
                (re * c.im + im * c.re) / tmp);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Complex)) return false;
        Complex complex = (Complex) o;
        return Double.compare(complex.re, re) == 0 &&
                Double.compare(complex.im, im) == 0;
    }

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

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

위 클래스를 실수와 허수를 가지는 복소수를 불변클래스로 만든것이다. 실수와 허수 필드에 대한 getter 메소드는 있지만 setter 메소드는 존재하지 않는다. 그리고 클래스를 final로 정의하여 확장할수 없도록 하였다. 또 클래스의 모든 필드를 final로 선언하여 초기화 이후 그 값이 바뀔 수 없도록 하였다. 그리고 모든 필드가 private로 선언되어있어서 클래스 외부에서는 직접 필드를 볼 수 없도록 하였다. 마지막으로 클래스에 가변 컴포넌트가 존재하지 않는다. 따라서 이 클래스는 불변 클래스의 다섯가지 규칙을 모두 지키고 있다.

 

 


Thread-safe한 불변객체

 

// Integer는 가변객체이기 때문에 멀티스레드 환경에서 사용하려면
// AtomicInteger로 변환하여 사용하여야한다.
Integer number = Integer.valueOf(100);
AtomicInteger atomicNumber = new AtomicInteger(number); 
multiThreadMethd(atomicNumber);

// BigDecimal는 불변객체이기 때문에 멀티스레드 환경에서 사용해도 문제가 없다.
BigDecimal number = BigDecimal.valueOf(100);
multiThreadMethd(number);

가변객체의 경우 멀티스레드 환경에서 동작할 경우, 동시에 값을 수정하려는 시도가 발생하면 의도치 않은 결과가 발생할 수 있다. 이때문에 멀티쓰레드 환경에서 가변객체를 사용할 경우, 멀티스레드에서 사용할 수 있도록 추가 로직이 들어간 객체로 만들어서 사용한다. 하지만 불변객체의 경우 해당 객체의 값은 불변이기 때문에 멀티스레드를 위한 별도의 처리가 필요없다.

 

 


불변객체의 비효율성

 

// 실행시간 486ms
StringBuilder strBuilder = new StringBuilder("");
for(int i = 0; i < 100000000; i++) {
	strBuilder.append("a");
}

// 동작이 끝나지 않는다.
String str = new String("");
for(int i = 0; i < 100000000; i++) {
	String newStr = str.concat("a");
}

불변객체라고 단점이 없는 것은 아니다. 불변이라는 특성때문에 연산의 결과를 기존 객체의 값을 수정해서 보여주는 것이 아니라, 새로운 객체에 결과 값을 담아서 보여주어야 한다. 위 예제코드에서 StringBuilder의 경우 기존 객체의 데이터에 계속 "a" 문자열을 붙여가면서 메모리와 성능의 낭비없이 동작을 수행하였다. 하지만 String은 불변객체이기 때문에 기존 객체 데이터에 "a" 문자열을 붙인 결과를 새로운 객체로 만들면서 동작을 수행하여야한다. 그 결과 메모리와 성능이 모두 낭비되어 실행 속도가 매우 느리다.

 

 


+ final으로 클래스를 선언하지 말고, 모든 생성자를 private로 선언하여 불변 클래스를 만들자. 정적 팩토리 메소드를 통해 더욱 유연한 구현이 가능하다.

+ 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소화 하자.

728x90