📖 book/❤️ Effective Java

이펙티브 자바 item 17) 변경가능성을 최소화 하라

인피니 2022. 3. 19. 11:25

🤔 불변 클래스란?

  •  그 인스턴스의 내부 값을 수정할 수 없는 클래스
  • 즉, 불변 클래스에 간직된 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다.

🧐 불변 클래스의 장점

  • 가변 클래스보다 설계하고 구현하고 사용하기 쉽고 오류가 생길 여지가 적고 훨씬 안전하다. 
    ->라고 책에서는 말하는데, 사용하기 쉽고 오류가 생길 여지가 적고 훨씬 안전 한건 객체 내부 값이 변경되지 않기 때문에 생기는 장점인 거 같은데 왜??? 가변 클래스보다 설계 및 구현이 쉬운 걸까? 이것도 불변이기 때문에 생기는 장점인데, 객체의 값이 변동되지 않도록 사용하는 방법 중 하나인 방어적 복사처럼 에러 상황에 대한 처리를 많이 고려하지 않아도 되기 때문 아닐까? 추측해본다. 

🤔 클래스를 불변으로 만들기 위한 방법

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

        • 클래스를 확장할 수 없도록 한다.
          대표적인 방법으로는 클래스를 final로 처리하는 것 
          -> final로 클래스를 선언하면 확장이 불가능한가? yes 왜냐 final로 하면 다른 클래스에서 상속받지 못하는 클래스가 되기 때문에 확장 자체가 불가능하다 

        • 모든 필드를 final로 선언한다.
          시스템이 강제하는 수단을 이용해 설계자의 의도를 명확히 드러내는 방법
          -> 필드를 final로 선언하면 값이 불가능하게 되는 즉 문법적으로 방지+ final 키워드를 사용한 것 자체가 해당 객체를 설계한 설계자의 입장에서 사용자에게 해당 필드는 변경 불가능한 것임을 알려주는 것 

        • 모든 필드를 private으로 선언한다.
          기술적으로는 기본 타입 필드나 불변 객체를 참조하는 필드를 public final로만 선언해도 불변 객체가 되지만 이렇게 하면 다음 릴리스에서 내부 표현을 바꾸지 못하므로 권하지 않음

          ex)
           -> testValue가 private 일 경우 직접 접근해 수정할 수 x 
          -> private으로 선언하면 클라이언트에서 임의로 접근해 필드를 변경할 수 있는 가능성을 지워준다.
          -> public final로 선언하면 다음 릴리스에서 내부 표현을 바꾸지 못하기 때문에 권하지 않는다??? 이것은 public final로 선언해도 불변을 보장하지만, 다음 릴리즈 때 public -> private으로 변경하고 싶어도 변경점이 많아 내부 표현을 바꾸지 못하기 때문에 권하지 않는다는 뜻 같다...
          더보기

          컴포넌트(component)란 여러 개의 프로그램 함수들을 모아 하나의 특정한 기능을 수행할 수 있도록 구성한 작은 기능적 단위

          -> testValue를 접근할 수 있는 메서드 getTestValue를 통해 참조를 획득하면 testValue를 마음대로 변경할 수 있다.
          -> 즉 설계자의 의도대로 동작하는 것이 아니다.
          -> 이럴 때는 어떻게 해결할까? 위에서 언급한 방어적 복사를 통해 해결하자 
          -> getTestValue를 통해 참조값을 얻으면 Test 생성 시 만들어진 참조값 testValue와 다르다.
          -> test 클래스의 testValue 참조값과 getTestValue 메서드를 통해 반환된 객체 참조값이 다른 것을 확인할 수 있다.
          위의 규칙을 만족하는 ex)
public final class 
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 * 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 (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Complex complex = (Complex) o;
        return Double.compare(complex.re, re) == 0 && Double.compare(complex.im, im) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(re, im);
    }
}

-> 해당 클래스는 복소수(실수부와 허수부로 구성된 수)를 표현한다.

 

-> 각 사칙연산 메서드(plus, minus, times, divideBy)는 자신은 수정하지 않고 새로운 Complex인스턴스를 만들어 반환
해당 클래스 같은 패턴을 함수형 프로그래밍이라 한다.

 

-> 함수형 프로그래밍이란? 피연산자에 함수를 적용해 그 결과를 반환하지만 피연산자 자체는
그대로인 프로그래밍 패턴을 함수형 프로그래밍이라 한다. 

 

-> 절차적 or 명령형 프로그래밍이란? 메서드에서 피연산자를 수정해 피연산자 상태가 변하게 된다.


-> 메서드 네이밍에 주목하자 

     -> (add 같은) 동사 대신 (plus 같은) 전치사를 사용한 점에 주목

 -> 왜 전치사를 사용했을까? 해당 메서드가 객체의 값을 변경하지 않는다는 사실을 강조하기 위해서

 

🤔  불변 객체 장점

1. 스레드 안전이기 때문에 따로 동기화할 필요가 없다.

-> 여러 스레드가 동시에 사용해도 절대 훼손되지 않음 

-> 즉 스레드 safe 하게 만드는 가장 쉬운 방법

-> 따라서 불변 객체는 안심하고 공유할 수 있기 때문에 인스턴스를 최대한 재활용하는 것을 권장

-> 어떻게 재활용을 할까? 

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)

-> 가장 쉬운 재활용 방법: 자주 쓰이는 값을 상수로 제공하는 것 

 

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

 

3. 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이정이 많다
-> 값이 바뀌지 않는 구성요소들로 이뤄진 객체라면 구조가 복잡하더라도 불변식을 유지하기 훨씬 수월하다.

 

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

-> 실패 원자성? 메서드에서 예외가 발생한 후에도 그 객체는 여전히 유효한 즉 메서드 호출 전과 동일한 상태여야 한다는 것 

-> 예외가 무엇일까? 생각해 보았을 때, 배열이 있고 해당 배열을 출력하기 위해 방어적 복사를 사용해 해당 배열 참조값을 넘겨주는 getxxx라는 메서드가 있다 가정해보자. 이때 방어적 복사가 된 객체의 참조값을 얻어 add라는 메서드로 해당 객체의 값을 변경할 수 있다. 

이렇게 불변 객체의 값을 변경하려는 시도가 ''예외'이며 add메서드를 호출해 객체의 값을 변경하려 시도해도, 원래 배열 값은 변경되지 않는다. 왜? 방어적 복사를 통해 객체의 참조값을 넘겨주었기 때문이다.

 

🤔불변 객체 단점

1. 값이 다르면 반드시 독립된 객체로 만들어야 한다는 것