본문 바로가기
Language/Effective java

클래스와 인터페이스

by y.j 2022. 6. 12.
728x90

변경 가능성을 최소화하라

클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다. 불변 클래스는 장점이 많고, 단점은 특정 상황에서의 잠재적 성능 저하뿐이다. 이것을 불변 클래스라고 부르는데 인스턴스의 내부 값을 수정 할 수 없는 클래스이다. 생성된 시점의 상태를 파괴될때까지 가져간다.

 

불변 클래스의 규칙

1. 객채의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
2. 클래스를 확장 할 수 없도록 한다.
  - 하위 클래스에서 부주의하게 혹은 나쁜 의도로 객체의 상태를 만드는 사태를 막아준다.
3. 모든 필드를 final로 선언한다. 
4. 모든 필드를 private으로 선언한다.
  - 외부에서 직접 접근하여 필드를 수정하는 것을 막아준다.
     public final로 사용한다면 다음 릴리즈때 내부 표현을 바꾸지 못하므로 권하지 않는다.
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);
    }

    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 int hashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }

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

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

 

사칙연산함수를 반환 할 때 새로운 Complex인스턴스를 만들어 반환한다(함수형 프로그래밍). add(동사) 대신에 plus(전치사)를 사용하는 명명규칙이 있다.

 


 

불변객체는 근본적으로 스레드 안전하여 따로 동기화할 필요 없다. 여러 스레드가 동시에 사용해도 절대 훼손되지 않기 때문에 안심하고 공유해서 사용 할 수 있다(public static final 사용). 더해서 캐싱을 하여 메모리 사용량과 가비지 컬렉션 비용을 줄이게 줄인다.

 

불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유 할 수 있다. BigInteger의 negate메서드는 크기가 같고 부호만 반대인 새로운 BigInteger를 생성하는데 이 때 배열은 원본인스턴스를 참조해도 상관없다.

 

불변 객체는 그 자체로 실패 원자성을 제공한다. 메서드에서 예외를 던지더라도 전과 같은 상태이다.

 

불변 클래스의 단점

값이 다르면 반드시 독립된 객체로 만들어야 한다. 독립된 객체를 만들기까지 단계가 많고, 그 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제가 발생한다. 이를 어떻게 해결해야 할까?

다단계 연산(multistep operation)들을 예측하여 기본 기능으로 제공하는 연산 속도를 높여주는 가변 동반 클래스를 제공하는 것이다. 


String의 replace() 메서드를 보면 내부에 StringBuilder를 사용하고 있다. StringBuilder를 생성해야 하기 때문에 다단계 연산을 기본으로 제공하는 것이 좋다. 반면에 StringBuilder자체는 기본적인 함수를 제공하고 있다. 

public String replace(CharSequence target, CharSequence replacement) {
    String tgtStr = target.toString();
    String replStr = replacement.toString();
    int j = indexOf(tgtStr);
    if (j < 0) {
        return this;
    }
    int tgtLen = tgtStr.length();
    int tgtLen1 = Math.max(tgtLen, 1);
    int thisLen = length();

    int newLenHint = thisLen - tgtLen + replStr.length();
    if (newLenHint < 0) {
        throw new OutOfMemoryError();
    }
    StringBuilder sb = new StringBuilder(newLenHint);
    int i = 0;
    do {
        sb.append(this, i, j).append(replStr);
        i = j + tgtLen;
    } while (j < thisLen && (j = indexOf(tgtStr, j + tgtLen1)) > 0);
    return sb.append(this, i, thisLen).toString();
}

불변 클래스의 설계

자신을 상속하지 못하게 하기 위해서 final을 사용해도 되지만 모든 생성자를 private로 만들고 public 정적 팩토리를 제공하는 방법이다.

public final 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);
    }
}

자바 설계 상 BigInteger와 BigDecimal은 불변 객체가 final이어야 한다는 생각이 널리 퍼지지 않았다. 그래서 재정의 할 수 있게 설계되었고 인수로 사용하기 위해서 '진짜' 객체인지 확인하고 방어적 복사를 사용하는 것이 좋다.

public static BigInteger safeInstance(BigInteger val) {
    return val.getClass() == BigInteger.class ?
            val : new BigInteger(val.toByteArray());
}

규칙을 좀 더 완화해서 성능을 향상 시킬 수 있다. 연산이 많이 필요한 필드(final이 아닌)에 처음쓰일 때만 캐시하도록 하는 것이다.

 

정리

1. 클래스는 꼭 필요한 경우가 아니라면 불벼니어야 한다.
2. 불변으로 만들 수 없는 클래스라도 최소한으로 줄인다.
  - 성능을 위해 가변 동반 클래스를 public클래스로 제공하도록 하자.
3. 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
4. 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
728x90

'Language > Effective java' 카테고리의 다른 글

클래스와 인터페이스  (0) 2022.06.14
클래스와 인터페이스  (0) 2022.06.14
클래스와 인터페이스  (0) 2022.06.12
모든 객체의 공통 메서드  (0) 2022.06.11
모든 객체의 공통 메서드  (0) 2022.06.07

댓글