이펙티브 자바:: 아이템 17 <변경 가능성을 최소화하라>
🙉

이펙티브 자바:: 아이템 17 <변경 가능성을 최소화하라>

Created
Aug 21, 2024 06:05 AM
Last edited time
Last updated August 22, 2024
Tags
Language
Language
Java
URL

Intro::

이펙티브 자바 정리본입니다.
 

결론

  • 클래스는 꼭 필요한 경우가 아닌 이상 불변이어야 한다.
  • 성능 때문에 어쩔 수 없다면 가변 동반 클래스를 public 클래스로 제공하도록 하자.
  • 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.
  • 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 합니다.

불변 클래스란?

  • 인스턴스의 내부 값을 수정할 수 없는 클래스를 의미합니다.
  • 객체가 파괴되는 순간까지 절대 달라지지 않습니다.
  • 자바 라이브러리에는 대표적으로 String, 기본 타입의 박싱된 클래스들, BigInteger, BigDeciaml등이 있습니다.
 

장점

  • 스레드 안전하여 따로 동기화할 필요가 없습니다. 즉, 안심하고 공유할 수 있습니다.
  • 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩토리를 제공할 수 있습니다.
    • 기본 타입의 박싱된 클래스들과 BigInteger가 여기에 속합니다.
  • 불변 객체를 자유롭게 공유할 수 있다는 점은 방어적 복사도 필요없다는 것을 의미합니다.
    • 그러니 불변 객체는 복사 생서자나 clone 메서드를 제공하지 않는 것이 좋습니다.
  • 불변 객체끼리는 내부 데이터를 공유할 수 있습니다.
  • 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많습니다.
  • 불변 객체는 그 자체로 실패 원자성을 제공합니다.
    • 상태가 절대 변하지 않으니 잠깐이라도 불일치 상태에 빠질 가능성이 없습니다.

단점

  • 값이 다르면 반드시 독립된 객체로 만들어야 합니다.
    • 값의 가짓수가 많다면 이들을 모두 만드는 데 큰 비용을 치러야 합니다.
    • 해결방법
        1. 다단계 연산들을 예측하여 기본 기능으로 제공하는 방법
        1. 가변 동반 클래스를 제공
          1. StringBuilder 와 같이
 

클래스를 불변으로 만들기 위해서는

  1. 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않아야 합니다.
  1. 클래스를 확장할 수 없도록 해야합니다.
    1. 하위 클래스에서 부주의하게 혹은 나쁜 의도로 객체의 상태를 변하게 만드는 사태를 막아줍니다.
    2. 대표적으로, 클래스를 final로 선언하여 막을 수 있습니다.
    3. 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩터리를 제공하는 방법도 있습니다.
      1. public static Complex valueOf(double re, double im) { return new Complex(re, im); }
        public 이나 protected 생성자가 없으니 다른 패키지에서는 이 클래스를 확장하는 것이 불가능 하기 때문입니다.
  1. 모든 필드를 final로 선언합니다.
  1. 모든 필드를 private으로 선언합니다.
    1. 필드를 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아줍니다.
    2. public final로만으로도 불변 객체가 되지만, 다음 릴리지에서 내부 표현을 바꾸지 못할 수 있습니다.
  1. 자신 외에는 내부 가변 컴포넌트에 접근할 수 없도록 합니다.
    1. 생성자, 접근자, readObject 메서드 두에서 방어적 복사를 수행해야 합니다.
 
// 코드 17-1 불변 복소수 클래스 (106-107쪽) 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); } ... }
위의 예제에서 Complex 객체는 사칙연산 메서드에서 자기 자신을 수정하지 않고 새로운 인스턴스를 만들어 반환합니다. 이처럼 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라고 합니다.
 

규칙 완화

모든 필드가 final이고 어떤 메서드도 그 객체를 수정할 수 없어야 한다는 규칙은 좀 과한감이 있어서, 성능을 위해 살짝 완화 가능하다.
  • 어떤 메서드도 객체의 상태 중 외부에 비치는 값을 변경할 수 없다.
  • 계산 비용이 큰 값을 나중에 계산하여 final이 아닌 필드에 캐시
 

궁금증

String의 경우 jvm의 string pool이라는 메커니즘을 통해 동일한 문자열 리터럴이 여러 번 사용되게 해 메모리 최적화를 하는데, 커스텀한 불변객체는 어떻게 메모리 최적화 해야하는 건가??

→ 불변 객체에 대한 메모리 관리가 궁금했음
Flyweigh 패턴이나 Interning 을 구현해 최적화를 할 수 있습니다. 또한 불변 객체의 값이 한정된 경우에는 Enum을 사용하는 것이 효율적입니다.
  • Flyweigh
    • 객체가 많을 때 메모리를 최적화하려는 목적에서 사용됩니다.
    • Flyweight 패턴은 객체의 상태를 분리하여 공유 가능한 부분(내부 상태)과 객체마다 다른 부분(외부 상태)을 관리하는 데 초점을 맞춥니다.
  • Interning
    • 동일한 값을 가진 불변 객체를 최적화하여 재사용하는 방식입니다.
    • Interning은 단순한 값의 중복을 피하기 위한 기법으로, 일반적으로 문자열과 같은 간단한 불변 객체에 적용됩니다. String.intern()이 대표적인 예입니다.
두 개념은 기본적으로 객체 공유를 통해 메모리 사용을 최적화하려는 같은 목적을 가지고 있지만, Flyweight 패턴은 더 복잡한 상태 관리와 관련이 있고, Interning은 간단한 값 재사용에 초점을 맞추고 있습니다. 따라서 엄밀히 말하면 Flyweight는 더 넓은 개념이며, Interning은 특정한 형태의 Flyweight 패턴으로 볼 수 있습니다.
public final class ImmutableDimension { private final int width; private final int height; // 객체 캐싱을 위한 Map private static final Map<String, ImmutableDimension> cache = new HashMap<>(); private ImmutableDimension(int width, int height) { this.width = width; this.height = height; } public static ImmutableDimension of(int width, int height) { String key = width + "," + height; if (!cache.containsKey(key)) { cache.put(key, new ImmutableDimension(width, height)); } return cache.get(key); } public int getWidth() { return width; } public int getHeight() { return height; } }
 

String 리터럴을 통한 Spring Pool의 동작

 
리터럴이라는 것은 코드에서 String s = "example";처럼 직접적으로 문자열 값을 지정하는 부분을 말합니다. 이러한 문자열 리터럴은 컴파일 시점String Pool에 자동으로 저장되며, 이후 동일한 리터럴이 사용될 때는 String Pool에 저장된 객체를 재사용하게 됩니다.
String s1 = new String("example");// 새로운 객체 생성 String s2 = s1.intern();// "example"을 Spring Pool에서 찾고 있다면 참조, 없다면 새로 등록 후 참조 String s3 = "example";// s2와 같은 객 System.out.println(s1 == s2);// false System.out.println(s2 == s3);// true System.out.println(s1 == s3);// false
 

가변 동반 클래스

한번 생성하면 불변인 String과는 달리 StringBuilder는 AbstractStringBuilder를 사용해 내부적으로 byte[]을 사용해 문자열을 가변적으로 관리하다가 toString 메서드를 통해 String으로 만들어준다.
간단하게 이해하자면 가변 적인 요소들이 확정되면 불변객체로 반환해주는 방식이라 생각하면 된다.
public static void main(String[] args) { StringBuffer sb = new StringBuffer(); sb.append("hello").append(" ").append("world"); String res = sb.toString(); System.out.println(res); }
 

References::

이펙티브 자바 / 조슈아 블로크 지음 (프로그래밍 인사이트)

Loading Comments...