[이펙티브 자바] 클래스와 인터페이스 Item17 - 변경 가능성을 최소화하라

이성훈·2022년 4월 18일
0

이펙티브 자바

목록 보기
13/17
post-thumbnail

객체 지향 방법론(Object Oriented Programming : OOP)의 기본 개념은
공통적으로 사용되는 부분을 미리 추상화 해놓는 것이다.

그것을 필요에 따라 구체화시켜 사용하는 것은 코드의 재사용성을 높이며,
그것은 곧 OOP의 지향점이 된다.


자바에서는 추상화의 기본 단위로 클래스(Class)와 인터페이스(Interface) 를 정의하고 있고, 이는 곧 자바의 심장과도 같다.


"4장 - 클래스와 인터페이스" 에서는,
쓰기 편하고, 견고하며, 유연한 클래스와 인터페이스를 만드는 방법에 대해 내용을 서술한다.



  • Item17. 클래스와 멤버의 접근 권한을 최소화하라.
  • Item16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라.
  • Item17. 변경 가능성을 최소화하라.
  • Item18. 상속보다는 컴포지션을 사용하라.
  • Item19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라.
  • Item20. 추상 클래스보다는 인터페이스를 우선하라.
  • Item21. 인터페이스는 구현하는 쪽을 생각해 설계하라.
  • Item22. 인터페이스는 타입을 정의하는 용도로만 사용하라.
  • Item23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라.
  • Item24. 멤버 클래스는 되도록 static으로 만들라.
  • Item25. 톱레벨 클래스는 한 파일에 하나만 담으라.




<"변경 가능성을 최소화하라">





#   불변 클래스



<불변 클래스란?>

  • 인스턴스의 내부 값을 수정할 수 없는 클래스.
  • 불변 인스턴스에 간직된 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않음.
  • 자바 플랫폼 라이브러리의 String, Boxing Class, BigInteger, BigDecimal등이 속함.

앞선 장에서 계속 등장하던 키워드, 블변 클래스에 대한 이야기가 드디어 나왔다.
(필자는 정말 읽는 사람의 사고 순서를 배려하지 않나보다.)

이렇게 정의를 먼저보니 생각보다 이론은 단순하다.


불변 클래스는 왜 쓸까?
일반적으로 불변 클래스는 가변 클래스에 비해 설계하고 구현하고 사용하기가 쉽고,
오류가 생길 여지도 적으며 훨씬 안전하다고 한다.


그럼 이제 클래스를 불변으로 만드는 규칙에 대해 알아보자.


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

    Setter가 대표적인 예시가 될 수 있겠다.
    그 외에, 인스턴스의 값을 변경할 수 있는 로직이 들어가는 메서드는 안된다.


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

    쉽게 말해 상속(Extends)이 불가능하게 하라는 말이다.
    상속 받아 사용하는 하위 클래스가, 의도했던 안했던 객체의 상태를 변하게 만들 수가 있다.

    대표적인 방법으로 final로 선언하여 상속을 막는다.


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

    final로 선언된 필드는 초기화 된 이후 상수처럼 취급받아 값이 변경되지 않는다.

    시스템이 강제하는 수단이기에 설계자의 의도를 더욱 명확하게 드러낼 수 있다.
    (사용자 입장에서 "아~ 이건 바꾸지 말라는 말이구나~")

    새로 생성된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작한다.


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

    필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하지 못하게 한다.
    (쉽게 말해, 작성자 외에는 인스턴스의 상태를 변하게 할 수 없다는 것이다.)

    기술적으로 기본 타입 필드나 불변 객체를 참조하는 필드를 public final로만 선언해도 불변은 된다.
    하지만 이렇게 되면 내부 표현을 바꾸지 못한다.


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

    클래스에 가변 객체를 참조하는 필드가 하나라도 있으면,
    클라이언트에서 절대 그것에 대한 참조를 얻지 못하게 해야한다.
    (참조를 얻는다는 것은, 다시 말해 어떻게든 접근이 가능하다는 것이다.)

    반대로 필드가 클라이언트에서 제공한 객체 참조를 바라봐서도 안되고,
    접근자 메서드가 그 필드를 그대로 반환해서도 안된다.
    (클라이언트가 그 객체를 바꿔버리면, 클래스도 상태가 바뀐다.)

    생성자, 접근자, readObject 메서드 모두에서 방어적 복사를 수행해야 한다.




여지껏 아이템들에서 봤던 예시들은 웬만하면 모두 불변이였다.

다음의 매우?조금? 복잡한 예시를 보자.

public final class Complex {
    private final double re;
    private final double im;

    public static final Complex ZERO = new Complex(0, 0);
    public static final Complex ONE  = new Complex(1, 0);
    public static final Complex I    = new Complex(0, 1);

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

    // 코드 17-2 정적 팩터리(private 생성자와 함께 사용해야 한다.) (110-111쪽)
    public static Complex valueOf(double re, double im) {
        return new Complex(re, 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 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;

        // == 대신 compare를 사용하는 이유는 63쪽을 확인하라.
        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 라는 전치사를 사용하고 있다.
이는 해당 메서드가 객체의 값을 변경하지 않는다는 사실을 강조하려는 의도이다.


<Comment.>

  • 실제로 위와 같은 명명 규칙을 따르지 않은 BigInteger와 BigDecimal 클래스는 사람들에게 자주 오용되어 오류가 발생한다.

어쨋든 이렇게 함수형 프로그래밍으로 코드를 짜게되면 불변이 되는 영역이 비율이 높아진다는 장점이 있다.





#   불변 객체의 장점


클래스를 불변하게 만드는 방법에 대해 알아보았으니, 이제 그 특성에 대해서 알아보자.


  • 불변 객체는 단순하다.

    불변 객체는 생성된 시점의 상태를 파괴될 때까지 간직한다.

    모든 생성자(Constructor)가 클래스 불변식을 보장한다면 이후의 별도의 노력없이도 영원히 불변으로 남는다.


  • 불변 객체는 근본적으로 스레드가 안전하여 따로 동기화할 필요가 없다.

    여러 스레드가 동시에 사용해도 절대 훼손되지 않는다.
    결국 클래스를 가장 안전하게 만드는 방법이 바로 불변 클래스로 만드는 것이다.


  • 불변 객체는 안심하고 공유할 수 있다.

    불변 객체에 대해서는 Thread가 서로 영향을 줄 수 없으니 안심이다.
    따라서 불변 클래스는 최대한 재활용하는 것을 권장한다.


    이 방법에는 정적 팩토리 메서드가 사용될 수 있다.
    자주 사용되는 인스턴스를 미리 만들어놓고 그것을 반환함으로써, 불필요한 중복 생성을 방지한다.

    이렇게 하면, 여러 클라이언트가 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.


    인스턴스를 자유롭게 공유할 수 있게 되면,앞서 말한 방어적 복사도 필요가 없게된다.
    백날 복사해봐야 원본과 똑같다면 굳이 복사할 이유가 없다.

    그래서 불변 클래스는 clone 메서드나 복사 생성자를 제공하지 않는게 좋다.


    아래는 앞선 복소수 예시 코드에서 불변 인스턴스를 생성하여 재활용 할 수 있는 예시이다.


public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);

  • 불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.

    예시를 들어보자.

    BigInteger 클래스의 경우 내부에서 값의 부호(sign), 크기(magnitude)를 따로 표현한다.
    부호에서는 int 변수를, 크기에서는 int 배열을 사용하게 되어있다.

    그 내부의 negate 메서드는 크기만 같고 부호만 반대인 새로운 BigInteger를 생성하는 역할을 하는데,
    이때 배열은 비록 가변이지만, 복사하지 않고 원본 인스턴스와 공유해도 된다.

    그 결과, 새로 만든 BigInteger 인스턴스도 원본 인스턴스가 가르키는 내부 배열을 그대로 가리킨다.


  • 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.

    값이 바뀌지 않는 구성요소들로 이뤄진 객체라면, 불변식을 유지하기 훨씬 수월하다.

    좋은 예로, 맵의 키(Key)와 집합(Set)의 원소로 쓰기에 매우 좋다.

    보통 맵이나 집합은 안에 담긴 값이 바뀌면 불변식이 허물어지지만,
    불변 객체가 들어간다면 계속해서 불변식을 유지할 수 있다.


  • 불변 객체는 그 자체로 실패 원자성을 제공한다.

    상태가 절대 변하지 않으므로, 불일치에 빠지는 순간도 없다.

<실패 원자성 (Failure Atomicity)>

  • 메서드에서 예외가 발생한 후에도 여전히 호출 전과 똑같은 유효한 상태여야 한다는 것.




#   불변 객체의 단점


불변 클래스에도 단점은 존재한다.

단순한 얘기지만, 불변인 만큼 값이 달라야 한다면 다 다른 독립된 객체로 만들어야 한다는 것이다.

만약 값이 다른 가짓수가 많다면 모든 경우의 객체를 생성하는데 비용은 만만치가 않다.


예를 들어보자.

BigInteger moby = ...;
moby = moby.flipBit(0);

BigInteger는 백만 비트 크기의 2진수로 이루어진다.

그런데 여기서 단 하나의 비트만 바꾸고 싶다면 어떨까?

실제로 flipBit는 새로운 BitInteger를 생성하는데,
원본과 비트 하나가 달라야 한다는 이유만으로 매번 백만비트 짜리 객체를 생성하는건 너무 낭비다.


이번엔 BitSet의 예시를 보자.

BigSet moby = ...;
moby = moby.flipBit(0);

BitSet은 BigInteger와 달리 가변 객체이다.

여기서의 mody는 원하는 비트 하나를 상수 시간 안에 바꿔준다.


즉, 위와 같은 상황에서는 BigInteger (불변 객체)보다 BigSet(가변 객체)이 더 성능이 좋다고 말하고 싶은거다.



만약 원하는 값 상태로 세팅된 불변 객체를 얻기까지의 과정에서,
단계가 많고 그 중간 과정에서 만들어진 객체들이 모두 버려진다면 성능 문제는 더욱 커진다.


이런 문제를 해결할 수 있는 방법에는 두 가지가 있다.


  • 다단계 연산(multistep operation)들을 예측하여 기본 기능으로 제공한다.

    다단계 연산을 예측하여 기본으로 제공하면, 각 단계마다 객체를 생성하지 않아도 된다.







#   불변 객체를 만드는 또 다른 설계 방법


앞서서 상속에 대한 얘기를 한 적이 있다.

클래스를 불변하게 만들려면 자신을 상속하지 못하게 만들어야 하고,
그것에 대한 제일 쉬운 방법이 바로 final 클래스로 선언하는 것이였다.


하지만 더 유연한 방법이 있다.
바로 생성자를 private이나 package-private으로 만들고 public 정적 팩토리 메서드를 제공하는 것이다.
(익숙하지 않은가? 사실 Item1에서 이미 했던 이야기이다)


다음의 예시를 보자.

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

  	...
}

기본적으로 상속을 받으려면, 하위 클래스는 상위 클래스의 생성자를 호출해야 한다.

위의 예시처럼 생성자를 private으로 만들게 되면,
하위 클래스에서 상위 클래스의 생성자를 호출할 수 없게되어 상속 자체를 무마시킨다.


사실 위 방법이 최선인 경우가 많다.



  • BigInteger와 BigDecimal

BigIntegr와 BigDecimal을 처음 설계할 당시에는,
불변 객체가 final이어야 한다는 생각이 널리 퍼지지 않았다.

실제로 그 덕에 둘 모두 재정의 할 수 있게 설계되었는데 이는 지금까지도 많은 문제를 야기하고 있다.


만약 신뢰할 수 없는 클라이언트로부터 BigInteger나 BigDecimal의 인스턴스를 인수로 받는다면 주의해야 한다.

이 값들을 믿을 수 없다면, 아래처럼 방어적 복사를 사용하자.

pubic static BigInteger safeInstance(BigInteger val) {

  	return val.getClass() == BigInteger.class ? 
  			val : new BigInteger(val.toByteArray());
}



  • 어떤 메서드도 객체의 상태 중 외부에 비치는 값을 변경할 수 없다.

글 초입부에, 불변 클래스를 만드는 규칙 중 하나는
모든 필드는 final이고 어떤 메서드도 그 객체를 수정할 수 없어야 한다고 했다.

하지만 이는 너무 과한 감이 있어 제목과 같이 완화가 가능하다.


경우에 따라서 어떤 불변 클래스는
계산 비용이 큰 값을 나중에 계산하여 final이 아닌 필드에 캐시 해놓기도 하기 때문이다.

똑같은 값을 다시 요청하면 캐시해둔 값을 반환하여 계산 비용을 절감할 수 있으므로 이 방법은 좋은 방법이다.


<Comment.>

  • 사실 이러한 묘수도 불변 클래스이기 때문에 가능한 것이다.
  • 몇번을 계산해도 같은 값이 나올 것을 보장하기 때문에 미리 캐시하는게 가능하다.




지금까지 불변 객체란 무엇인지, 불변 객체를 만드는 규칙, 그의 장단점 등을 알아보았다.
다음과 같이 정리해보자.

<Item17 정리>

  • 게터(Getter)가 있다고 해서 무조건 세터(Setter)를 만들지는 말자.
  • 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
  • 불변 클래스는 장점이 주를 이루지만, 특정 상황에서 성능 저하라는 단점도 있긴 하다.
  • Complex 예제 같은 단순한 값 객체는 무조건 불변으로 만들자.
  • String과 BigInteger처럼 무거운 값 객체도 불변으로 만들수 있을지 고심하자.
  • 성능 대문에 어쩔 수 없다면 불변 클래스와 쌍을 이루는 가변 동반 클래스를 public으로 제공하자.
  • 모든 클래스를 불변으로 만드는 것은 불가하지만, 그래도 변경 가능성을 최대로 줄이자.
    (꼭 변경해야할 필드를 제외하고선 모두 final로 선언하자.)
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
  • 확실하 이유가 없다면, 생성자와 정적 팩토리 메서드 외의 그 어떤 초기화 메서드도 허용해선 안된다.

Item17과 Item15를 종합하면 한마디로 줄일 수 있다.

다른 합당한 이유가 없다면 모든 필드는 private final 이어야 한다.

profile
IT 지식 공간

0개의 댓글