17. 변경 가능성을 최소화하라
📝 아이템 17: 변경 가능성을 최소화하라
🔹 요약
✅ 불변 클래스는 인스턴스의 내부 값을 수정할 수 없는 클래스 ✅ 불변 클래스는 가변 클래스보다 설계, 구현, 사용이 쉬우며 오류가 생길 여지가 적음 ✅ 불변 객체는 스레드 안전하여 동기화할 필요가 없음 ✅ 불변 클래스를 만드는 5가지 핵심 규칙:
객체의 상태를 변경하는 메소드(변경자)를 제공하지 않기
클래스를 확장할 수 없도록 하기 (final 클래스로 선언)
모든 필드를 final로 선언하기
모든 필드를 private으로 선언하기
자신 외에는 내부 가변 컴포넌트에 접근할 수 없도록 하기
🔹 주의사항
📌 불변 객체는 값이 다르면 반드시 독립된 객체로 생성해야 함 📌 성능을 위해 다단계 연산을 제공하는 가변 동반 클래스 고려 (예: StringBuilder) 📌 모든 클래스를 불변으로 만들 수는 없으나, 변경할 수 있는 부분을 최소화하는 것이 좋음
📚 추가 개념
💡 불변 객체의 장점
단순함: 생성된 시점의 상태를 파괴될 때까지 그대로 간직합니다.
스레드 안전: 내부 상태가 절대 변하지 않기 때문에 여러 스레드가 동시에 사용해도 훼손되지 않습니다.
안전한 공유: 불변 객체는 어떤 스레드든 신경 쓰지 않고 공유할 수 있습니다.
방어적 복사 불필요: 복사해도 원본과 같으므로 방어적 복사가 필요 없습니다.
실패 원자성 제공: 상태가 절대 변하지 않으므로 잠깐이라도 불일치 상태에 빠질 일이 없습니다.
🔑 불변 클래스 설계 방법
클래스를
final
로 선언하여 상속을 막습니다.모든 필드를
private final
로 선언합니다.생성자나 정적 팩터리 메소드를 통해서만 객체를 생성할 수 있게 합니다.
어떤 메서드도 객체의 상태를 변경할 수 없습니다.
💡 불변 객체와 정적 팩터리
불변 클래스는 정적 팩터리 메소드를 통해 객체 캐싱 기능을 제공할 수 있습니다.
예를 들어,
Integer.valueOf(int)
메소드는 객체를 캐싱하여 메모리 사용량을 줄이고, 동일한 값에 대해 새로운 객체를 생성하지 않고 캐시된 객체를 반환합니다.public class Example { public static void main(String[] args) { // 128은 캐싱 범위 외의 값 Integer a = Integer.valueOf(128); Integer b = Integer.valueOf(128); // 새로운 객체가 아닌, 기존 객체를 반환 System.out.println(a == b); // true, 이미 캐시된 값이기 때문 } }
💡 함수형 프로그래밍
불변 객체를 활용한 프로그래밍 패러다임으로, 피연산자에 함수를 적용해 그 결과를 반환하지만 피연산자 자체는 변경하지 않습니다.
이러한 방식은 코드의 예측 가능성을 높이고 버그를 줄일 수 있습니다.
💡 불변 객체와 방어적 복사
불변 객체는 내부 데이터가 변하지 않으므로 방어적 복사가 필요 없습니다.
그러나 가변 객체를 포함하는 경우에는 생성자에서 받은 가변 객체를 방어적으로 복사해야 합니다.
🎯 중요한 점
🔹 불변 클래스는 값이 다르면 반드시 독립된 객체로 만들어야 함 🔹 불변 클래스에서는 모든 필드가 final이어야 함 (일부 JVM 최적화에도 도움) 🔹 getter가 있다고 해서 setter도 반드시 있어야 하는 것은 아님 🔹 성능이 중요한 경우 불변 클래스와 함께 가변 동반 클래스를 제공하는 것이 좋음
💡 코드 예제 및 설명
flowchart LR
A["Original Book('Effective Java')"] --> B{{"withTitle('Effective Java 3rd Edition')"}}
B --> C["New Book('Effective Java 3rd Edition')"]
A -.-> D["Original Book is Unchanged"]
style A fill:#FFEFC8,stroke:#FFD95F,stroke-width:2px
style B fill:#E9F3FE,stroke:#4A90E2,stroke-width:2px
style C fill:#FFEFC8,stroke:#FFD95F,stroke-width:2px
style D fill:#FFEFC8,stroke:#FFD95F,stroke-width:2px
✅ 불변 클래스 예제
// 불변 클래스로 구현한 Book (즉, 이 클래스를 확장하는 서브클래스를 만들 수 없음)
public final class Book {
private final String title; // 제목
private final String author; // 저자
private final String bookContent; // 내용
private final int publicationYear; // 출판 연도
private final String publisher; // 출판사
// 생성자를 통해 모든 필드를 초기화
public Book(String title, String author, String bookContent, int publicationYear, String publisher) {
this.title = title;
this.author = author;
this.bookContent = content;
this.publicationYear = publicationYear;
this.publisher = publisher;
}
// 접근자 메서드(getter)만 제공, 변경자 메서드(setter) 없음
public String getTitle() { return title; }
public String getAuthor() { return author; }
public String getBookContent() { return bookContent; }
public int getPublicationYear() { return publicationYear; }
public String getPublisher() { return publisher; }
// 제목을 변경하려면 새로운 Book 객체를 생성해 반환
public Book withTitle(String newTitle) {
return new Book(newTitle, this.author, this.bookContent, this.publicationYear, this.publisher);
}
// 출판 연도를 변경하려면 새로운 Book 객체를 생성해 반환
public Book withPublicationYear(int newYear) {
return new Book(this.title, this.author, this.bookContent, newYear, this.publisher);
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof Book)) return false;
Book book = (Book) o;
return publicationYear == book.publicationYear &&
title.equals(book.title) &&
author.equals(book.author) &&
publisher.equals(book.publisher);
}
@Override
public int hashCode() {
return Objects.hash(title, author, publicationYear, publisher);
}
@Override
public String toString() {
return "Book{" +
"title='" + title + '\'' +
", author='" + author + '\'' +
", publicationYear=" + publicationYear +
", publisher='" + publisher + '\'' +
'}';
}
public static void main(String[] args) {
// 책 생성
Book book = new Book("Effective Java", "Joshua Bloch", 2018, "Addison-Wesley");
// 제목 변경 예시
Book updatedBook = book.withTitle("Effective Java 3rd Edition");
System.out.println(updatedBook);
}
}
❌ 가변 클래스의 문제점
public class MutablePoint {
private double x;
private double y;
public MutablePoint(double x, double y) {
this.x = x;
this.y = y;
}
// 변경자 메서드(setter)
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
// 접근자 메소드(getter)
public double getX() { return x; }
public double getY() { return y; }
// 다른 객체에서 이 객체의 상태를 변경할 수 있으므로
// 멀티스레드 환경에서 안전하지 않음 (동시성 문제)
public void moveTo(double newX, double newY) {
this.x = newX;
this.y = newY;
}
}
✅ 불변 클래스 - private 생성자 + 정적 팩터리 메서드
public final class ImmutablePoint {
private final double x;
private final double y;
// 생성자는 private로 외부에서 호출할 수 없게 제한
// 객체 생성을 정적 팩토리 메서드를 통해 제어할 수 있습니다.
private ImmutablePoint(double x, double y) {
this.x = x;
this.y = y;
}
// 정적 팩토리 메서드를 사용하여 객체를 생성
// 필요에 따라 객체의 재사용이나 캐싱 등의 작업을 수행할 수 있습니다.
public static ImmutablePoint of(double x, double y) {
// 이 메서드를 통해 반환된 객체는 불변 객체이므로, 상태를 변경 불가
// 객체 상태가 변하지 않도록 보장됩니다.
return new ImmutablePoint(x, y);
}
// 객체의 상태는 불변이므로, 해당 필드들의 getter 메서드를 통해서만 접근
public double getX() { return x; }
public double getY() { return y; }
// 객체 상태를 변경하려면 새로운 객체를 반환
// 기존 객체를 변경할 수 없기 때문에, 상태 변경을 요구하는 작업은 새로운 객체를 반환하는 방식으로 처리
public ImmutablePoint moveTo(double newX, double newY) {
// 새로운 ImmutablePoint 객체를 반환하여 상태를 변경
return new ImmutablePoint(newX, newY);
}
}
❗ 어려웠던 점
⚠️ 불변 객체가 포함하는 필드가 가변 객체인 경우 방어적 복사를 통해 보호해야 하는데 이 개념이 처음에는 이해하기 어려웠음
➡️ 불변 클래스에 가변 컴포넌트가 있다면, 해당 컴포넌트를 외부에서 수정할 수 없도록 방어적 복사(직접 반환하는 것이 아니라 복사본을 반환)
를 해야 함을 이해함
⚠️ 불변 객체의 경우 함수형 프로그래밍 패러다임(불변성, 순수 함수)
을 따름으로 필드 값마다 새 객체를 만들어야 해서 성능 저하의 우려가 있었음
➡️ 매번 새로운 객체를 생성하는 대신 내부적으로 캐싱 메커니즘을 사용하거나, 가변 동반 클래스(예: String
과 StringBuilder
)를 제공하는 방식으로 개선할 수 있음을 배움
💭 느낀 점
💡 처음에는 객체를 불변으로 만드는 것이 제약이 많다고 생각했는데, 실제로는 코드의 안정성과 신뢰성을 크게 높여준다는 것을 알게 되었다.
💡 멀티스레드 환경에서 불변 객체를 사용하면 동기화에 대해 걱정할 필요가 없다는 점이 큰 장점이라고 느꼈다.
💡 객체 지향 설계에서 "가변성은 필요한 경우에만 제공하라" 는 원칙의 중요성을 깨달았다.
Last updated