Intro::
이펙티브 자바 정리본입니다.
결론
- 클래스는 꼭 필요한 경우가 아닌 이상 불변이어야 한다.
- 성능 때문에 어쩔 수 없다면 가변 동반 클래스를 public 클래스로 제공하도록 하자.
- 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.
- 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
- 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 합니다.
불변 클래스란?
- 인스턴스의 내부 값을 수정할 수 없는 클래스를 의미합니다.
- 객체가 파괴되는 순간까지 절대 달라지지 않습니다.
- 자바 라이브러리에는 대표적으로 String, 기본 타입의 박싱된 클래스들, BigInteger, BigDeciaml등이 있습니다.
장점
- 스레드 안전하여 따로 동기화할 필요가 없습니다. 즉, 안심하고 공유할 수 있습니다.
- 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩토리를 제공할 수 있습니다.
- 기본 타입의 박싱된 클래스들과 BigInteger가 여기에 속합니다.
- 불변 객체를 자유롭게 공유할 수 있다는 점은 방어적 복사도 필요없다는 것을 의미합니다.
- 그러니 불변 객체는 복사 생서자나 clone 메서드를 제공하지 않는 것이 좋습니다.
- 불변 객체끼리는 내부 데이터를 공유할 수 있습니다.
- 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많습니다.
- 불변 객체는 그 자체로 실패 원자성을 제공합니다.
- 상태가 절대 변하지 않으니 잠깐이라도 불일치 상태에 빠질 가능성이 없습니다.
단점
- 값이 다르면 반드시 독립된 객체로 만들어야 합니다.
- 값의 가짓수가 많다면 이들을 모두 만드는 데 큰 비용을 치러야 합니다.
- 해결방법
- 다단계 연산들을 예측하여 기본 기능으로 제공하는 방법
- 가변 동반 클래스를 제공
- StringBuilder 와 같이
클래스를 불변으로 만들기 위해서는
- 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않아야 합니다.
- 클래스를 확장할 수 없도록 해야합니다.
- 하위 클래스에서 부주의하게 혹은 나쁜 의도로 객체의 상태를 변하게 만드는 사태를 막아줍니다.
- 대표적으로, 클래스를 final로 선언하여 막을 수 있습니다.
- 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩터리를 제공하는 방법도 있습니다.
public static Complex valueOf(double re, double im) { return new Complex(re, im); }
public 이나 protected 생성자가 없으니 다른 패키지에서는 이 클래스를 확장하는 것이 불가능 하기 때문입니다.
- 모든 필드를 final로 선언합니다.
- 모든 필드를 private으로 선언합니다.
- 필드를 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아줍니다.
- public final로만으로도 불변 객체가 되지만, 다음 릴리지에서 내부 표현을 바꾸지 못할 수 있습니다.
- 자신 외에는 내부 가변 컴포넌트에 접근할 수 없도록 합니다.
- 생성자, 접근자, 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...