평소 자바로 프로그래밍하며 불변 객체를 자주 사용하는가? 아래의 Line은 불변객체라 할 수 있을까?
class Line {
private final List<Point> points;
public Position(final List<Point> points) {
this.points = points;
}
public getPoints() {
return points;
}
}
불변 객체는 생성 후 상태를 바꿀 수 없는, 즉 인스턴스 내부의 값을 변경할 수 없는 객체이다. 설계와 구현이 쉬우며 안전하다는(출처: 조슈아 블로크의 'Effective Java') 불변 객체를 더 공부해보자.
가장 엄격한 기준의 불변 객체를 만들어보자.
final로 선언한다.final로 선언된 변수가 참조형 객체라면 그 내부의 값은 변경될 수 있다. (그러니까 불변 객체에서는 이를 막아야 한다.)private으로 선언한다.public final로 선언해도 불변 객체지만, public 인 필드는 하위 호환을 위해 관리되어야 하고 따라서 추후 변경하기 어렵다.**final 클래스로 생성하기
private 혹은 default 생성자를 만들고 public 정적 팩토리 메서드를 제공하기
2번이 더 유연하다. 정적 팩토리 메서드의 장점*** 을 챙기면서 default 생성자로 같은 패키지 안에서 객체를 자유롭게 생성할 수 있다. 패키지 밖의 클라이언트에게는 확장 불가능한 불변 객체일 뿐이지만!
getter)가 그 필드를 그대로 반환해서는 안된다.case1과 case2 이후에 line 인스턴스의 points에는 2개의 point가 존재한다. 불변성이 깨진 것이다.class Line {
private final List<Point> points;
public Position(final List<Point> points) {
this.points = points;
}
public getPoints() {
return points;
}
}
List<Point> points = new ArrayList<>(List.of(new Point(1)));
Line line = new Line(points);
// case 1
points.add(new Point(2));
// case 2
line.getPoints().add(new Point(2));
class Line {
private final List<Point> points;
public Position(final List<Point> points) {
this.points = new ArrayList<>(points);
}
public getPoints() {
/*
참고: 불변 컬렉션으로 최적화할 수 있다.
ex. unmodifiableList(points)
*/
return new ArrayList<>(points);
}
}
class Line {
private final List<Point> points;
public Position(final List<Point> points) {
this.points = unmodifiableList(points); // 불변이 아니다
}
public getPoints() {
return points;
}
}
List<Point> points = new ArrayList<>(List.of(new Point(1)));
Line line = new Line(points);
points.add(new Point(1)); // 원본 컬렉션의 참조로 Line을 조작한다.
** 아래 value 필드는 public이므로 외부에서 직접 사용하는 클라이언트 코드가 있을 것이다. (있을 수 있다.)
class Position {
public final int value;
public Position(final int value) {
this.value = value;
}
}
class ClientExample {
// (생략)
public void move(int condition) {
Position position = new Position(3);
if (position.value > 5 ) { // 여기!
// (생략)
}
}
}
이제 필드명을 point로 바꾸고 싶어도 바꿀 수 없다. 이미 누군가(타자)는 public final int value를 사용하고 있다.
*** 정적 팩토리 메서드는 생성자와 달리
Integer, Character, Boolean)들을 대표적인 예시로 들 수 있다.class Position {
private final int value;
public Position(final int value) {
this.value = value;
}
public Position plus(final int number) {
return new Position(value + number);
}
public Position minus(final int number) {
return new Position(value - number);
}
}
class Position {
private int value;
public Position(int value) {
this.value = value;
}
public Position plus(final int number) {
return value++;
}
}
class Player {
private Position position;
public Player(Position position) {
this.position = position;
}
public void moveForward() {
return position.plus();
}
}
Position position = new Position(0);
Player meoru = new Player(position);
Player gari = new Player(position);
meoru.moveForward()
meoru만 움직였다고 생각할테지만, 실은 두 플레이어 모두 움직였다. 불변 객체를 사용한다면 이같은 실수를 방지할 수 있다.Map의 key나 Set의 요소로 불변 객체를 사용하면 불변식을 유지하기 편하다.BigInteger의 경우 내부에서 값의 부호와 크기(int 배열)를 따로 저장한다. negate는 부호가 반대인 BigInteger를 반환하는데, 이때 크기(int 배열)를 공유한다.원하는 객체를 생성하기까지 생성해야 하는 객체가 많고, 중간에 만들어진 객체들을 모두 버려야 한다면 가장 좋지 않은 상황이다.
대책은 가변 동반(companion) 클래스를 생성하는 것이다.
예시1) BigInteger: 내부에서 default가 범위인 동반 클래스들(MutableBigInteger 등)을 사용한다.
public BigInteger gcd(BigInteger val) {
if (val.signum == 0)
return this.abs();
else if (this.signum == 0)
return val.abs();
MutableBigInteger a = new MutableBigInteger(this);
MutableBigInteger b = new MutableBigInteger(val);
MutableBigInteger result = a.hybridGCD(b);
return result.toBigInteger(1);
}
예시2) String: 복잡한 연산을 모두 내부에서 지원하기 어려워 동반 클래스인 StringBuilder를 제공한다.
익숙한 단어라는 것과 별개로, 왜 필요한지∙어떤 장점이 있는지∙어떻게 활용해야 하는지 분명히 설명하기 어려웠다. 불변객체를 공부하며 그동안 자바로 프로그래밍하며 들었던 의문들(객체를 어떻게 분리하고 필드를 어떻게 관리해야 하는가?)도 해소할 수 있었다. 값이 다르면 매번 객체를 생성해야 한다는 점이 성능 이슈로 부정적으로만 다가왔으나, 다양한 장점을 보며 적재적소에 활용하는 게 중요하다는 걸 깨달았다.
Effective Java - 조슈아 블로크
불변 객체의 장점뿐만 아니라 단점과 그에 대한 대책으로 String와 BigInteger의 예시를 들어줘서 유익했어요 🥔