[JAVA] 불변 객체

종미·2024년 3월 3일
6

☕️ Java

목록 보기
1/5
post-thumbnail

들어가며

평소 자바로 프로그래밍하며 불변 객체를 자주 사용하는가? 아래의 Line은 불변객체라 할 수 있을까?

class Line {
	private final List<Point> points;
    
    public Position(final List<Point> points) {
    	this.points = points;
    }
    
    public getPoints() {
    	return points;
    }
}

불변 객체는 생성 후 상태를 바꿀 수 없는, 즉 인스턴스 내부의 값을 변경할 수 없는 객체이다. 설계와 구현이 쉬우며 안전하다는(출처: 조슈아 블로크의 'Effective Java') 불변 객체를 더 공부해보자.

불변 객체 만들기

가장 엄격한 기준의 불변 객체를 만들어보자.

1. 모든 필드를 final로 선언한다.

  • 값을 수정할 수 없게 된다.
  • 다만 final로 선언된 변수가 참조형 객체라면 그 내부의 값은 변경될 수 있다. (그러니까 불변 객체에서는 이를 막아야 한다.)

2. 모든 필드를 private으로 선언한다.

  • 클라이언트에서 필드를 직접 접근해 수정할 수 없다.
  • primitive type이나 불변 객체를 참조하는 필드를 public final로 선언해도 불변 객체지만, public 인 필드는 하위 호환을 위해 관리되어야 하고 따라서 추후 변경하기 어렵다.**

3. 객체의 상태를 변경하는 메서드를 제공하지 않는다.

4. 클래스를 확장할 수 없도록 한다.

  • 하위 클래스에서 객체의 상태를 바꿀 수 없게 하고 가변 상태를 가지지 못하게 한다.
  • 만드는 방법은 2개이다.
  1. final 클래스로 생성하기

  2. private 혹은 default 생성자를 만들고 public 정적 팩토리 메서드를 제공하기

    2번이 더 유연하다. 정적 팩토리 메서드의 장점*** 을 챙기면서 default 생성자로 같은 패키지 안에서 객체를 자유롭게 생성할 수 있다. 패키지 밖의 클라이언트에게는 확장 불가능한 불변 객체일 뿐이지만!

5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

  • 클래스에서 가변 객체를 참조하는 필드가 있다면 클라이언트에서 참조를 그 객체의 참조를 얻을 수 없어야 한다.
  • 클라이언트가 제공한 객체 참조를 필드가 가리키거나 접근자 메서드(getter)가 그 필드를 그대로 반환해서는 안된다.
  • 아래 코드를 보자. case1case2 이후에 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를 사용하고 있다.

*** 정적 팩토리 메서드는 생성자와 달리

  1. 이름을 가질 수 있다.
  2. 캐싱할 수 있다. (최적화)
    Wrapping class의 일부(Integer, Character, Boolean)들을 대표적인 예시로 들 수 있다.
  3. 반환 타입의 하위 객체 타입을 반환할 수 있다.
  4. 입력 매개변수에 따라 다른 클래스를 반환할 수 있다.
  5. 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

불변 객체 사용하기

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);
    }
}

함수형 프로그래밍

  • 피연산자에 함수를 적용해 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴
  • 절차적(명령형) 프로그래밍은 메서드에서 피연산자를 수정해 상태가 변한다.
  • 명명규칙으로, 동사를 사용하지 않고 전치사를 사용한다.

불변 객체의 장점

1. 단순하다.

  • 불변 객체를 사용하는 프로그래머가 별다른 노력을 기울이지 않아도 영원히 불변으로 남는다. 가변 객체는 임의의 복잡한 상태를 가질 수 있다. 아래가 문제의 코드이다.
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의 요소로 불변 객체를 사용하면 불변식을 유지하기 편하다.

2. 같은 인스턴스를 공유할 수 있다.

  • 스레드 간 안심하고 '공유'할 수 있다.
  • 따라서 불변 객체는 한 번 만든 인스턴스를 최대한 재활용하는 것이 좋다.
  • 자주 사용되는 인스턴스를 캐싱하는 정적 팩토리 메서드를 제공하면 된다. 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.
  • 불변 객체 자체는 방어적 복사가 필요하지 않다.
  • 불변 객체끼리 내부 데이터를 공유할 수 있다.
    ex. BigInteger의 경우 내부에서 값의 부호와 크기(int 배열)를 따로 저장한다. negate는 부호가 반대인 BigInteger를 반환하는데, 이때 크기(int 배열)를 공유한다.

3. 실패 원자성을 제공한다.

  • 실패 원자성이란 메서드에서 예외가 발생하더라도 그 객체는 여전히 호출 전의 유효한 상태와 같아야 한다는 성질이다.

불변 객체의 단점

값이 다르면 매번 인스턴스를 생성해야 한다.

  • 원하는 객체를 생성하기까지 생성해야 하는 객체가 많고, 중간에 만들어진 객체들을 모두 버려야 한다면 가장 좋지 않은 상황이다.

  • 대책은 가변 동반(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 - 조슈아 블로크

profile
BE 🪐

3개의 댓글

comment-user-thumbnail
2024년 3월 4일

불변 객체의 장점뿐만 아니라 단점과 그에 대한 대책으로 String와 BigInteger의 예시를 들어줘서 유익했어요 🥔

답글 달기
comment-user-thumbnail
2024년 3월 4일

나름 불변 객체를 만든 줄 알았더니 .. 4, 5번에서 와장창이네요 🥹 열심히 참고해서 진짜 불변 객체를 만들어보겠습니다 !~!!

답글 달기
comment-user-thumbnail
2024년 3월 6일

불변객체를 잘 정리해주셔서 개념을 짚는데 도움이 됐어요👍
방어적 복사 + 최적화를 위해 생성자에서 List.copyOf()를 쓰는건 어떨까요? new 와 동시에 불변객체를 만들어줘서 코드를 줄일 수 있을 것 같아요!

답글 달기