업데이트:

좋은 코드를 작성하기 위한 공부를 하다보면 불변객체에 대해 듣게 되는데, 오늘은 이 불변객체란 무엇인지에 대해 알아보도록 하겠습니다.

불변객체


먼저 위키에 있는 내용을 보면 다음과 같습니다.

객체 지향 프로그래밍에 있어서 불변객체(immutable object)는 생성 후 그 상태를 바꿀 수 없는 객체를 말한다.
...
또, 경우에 따라서는 내부에서 사용하는 속성이 변화해도 외부에서 그 객체의 상태가 변하지 않은 것처럼 보인다면 불변 객체로 보기도 한다.

제일 중요한 개념은 “생성 한 뒤 내부 상태가 변경되지 않는다.”입니다.

String

Java의 대표적인 불변 객체로는 String이 있습니다.

보통 우리는 String을 선언할 때 다음과 같이 선언합니다.

String str = "ab";

그럼 다음과 같이 문자열을 추가하게 된다면 어떻게 될까요?

str = str + "cd";

str이 변경됐을꺼라 생각할 수 있지만, 사실 “ab”라는 String 객체는 그대로 남아있고, “abcd”라는 새로운 객체가 str 변수에 할당됩니다.

💡Java에서 String은 특이하게 Heap 영역에서 String Constant Pool이 존재하는데, StringLiteral로 생성하면 이 영역에 저장되어 재사용됩니다. 하지만, new 생성자로 생성하면 Constant Pool을 사용하지 않고, 일반적인 Heap 영역에 생성하여 재사용하지 않게 됩니다.

불변객체의 장점?


불변객체를 어떻게 만드는지 알기 전에 어떤 장점이 있는지 먼저 알아보겠습니다.

Thread-Safe 하다.

멀티 쓰레드 환경에서 동기화 이슈가 발생하는 이유는 공유 자원에 대한 덮어쓰기가 발생하기 때문인데요.

이를 불변 객체를 이용하여 로직을 구성하게 되면, 동기화에 대한 이슈를 해결할 수 있습니다.

쓰레드마다 새로운 객체를 생성하여 접근하기 때문에 덮어쓰기가 발생하지 않기 때문이죠.

또한 synchronized를 사용하여 동기화하지 않아도 되기 때문에 성능도 좋아집니다.

객체에 대한 신뢰도가 높아진다. (=Side Effect를 최소화할 수 있다.)

객체가 어떤 상태값으로 생성된 뒤에 변화가 없을 것이기 때문에 상태에 대한 보장을 받을 수 있고, 이후에 사용 시에도 믿고 사용할 수 있습니다.

만약 가변 객체(Setter가 있는)라면 시시때때로 변화하는 객체라면 특정 시점에 객체의 상태를 예측하기 힘들어집니다.

상태값을 일일히 확인하기 위해 메소드를 일일히 살펴봐야 하고 이는 유지보수에 큰 시간을 할애하게 만듭니다.

불변 객체는 상태의 변경이 불가능하게 하기 위해 객체의 생성과 사용이 상당히 제한됩니다.

이는 메소드를 순수함수로 만들어줄 것이며, 유지보수성이 높은 코드가 되도록 유도할 것입니다.

💡순수함수란? 동일한 인자가 들어갈 경우 반드시 같은 값을 return하는 함수를 말합니다.

방어적 복사를 만들 필요가 없어진다.

먼저, 방어적 복사란 무엇일까요? 객체의 특정 상태를 getter 메소드 등으로 확인하거나 접근할 때

객체 내부적으로 복사본을 새로 만들어 반환하는 코드를 말합니다.

객체의 특정 상태 값을 변경할 때 안전하게 원본 객체를 보존하기 위한 방법으로 사용합니다.

하지만 불변객체에서는 이런 방어적 복사 코드를 사용할 수고를 덜어주게 합니다.

방어적 복사만으로도 하나의 포스트를 작성해야 할 것 같아 다음에 관련된 포스트를 작성해보겠습니다.

GC의 성능을 높일 수 있다.

사실 굉장히 의아스러운 점이었습니다.

“새로운 객체가 많이 만들어질 수록 GC의 성능은 떨어지지 않나?”라고 생각했습니다.

먼저 오라클 공식 문서에 따르면 다음과 같이 적혀 있다고 합니다.

“불변 객체는 당신이 고민하면서 만든 가변 객체보다 메모리를 더 효율적으로 적게 사용한다.”

위 문구는 원문을 확인하고 싶었지만 제가 직접 확인하진 못했습니다.

그리고 다음과 같은 문구도 적혀 있습니다.

Programmers are often reluctant to employ immutable objects, 
because they worry about the cost of creating a new object as opposed to updating an object in place. 
The impact of object creation is often overestimated, 
and can be offset by some of the efficiencies associated with immutable objects. 
These include decreased overhead due to garbage collection, 
and the elimination of code needed to protect mutable objects from corruption.

“프로그래머는 객체 생성에 대한 비용 때문에 불변을 꺼려하지만 불변을 이용한 효율로 상쇄될 수 있다”

라고 적혀있습니다.

그러면 어떻게 이게 가능한지 보겠습니다.

불변객체

public class ImmutableWrapper {
    private final Object obj;
    public ImmutableWrapper(Object obj) {
        this.obj = obj;
    }
    public Object getObj() {
        return this.obj;
    }
}

위와 같은 불변객체를 생성해내는 ImmutableWrapper 클래스가 있습니다.

ImmutableWrapper 내부에 obj에 할당된 객체는 ImmutableWrapperGC에 의해 제거되기 전까지 반드시 참조되어 살아있을 것입니다.

다시 말하면 ImmutableWrapper가 살아있는 이상 내부에 있는 objGC 스캔 대상에서 제외됩니다.

즉, 불변객체를 사용하면 GC의 스캔 범위 및 스캔 빈도수가 줄어들게 되어 GC의 성능을 높이는 결과를 가져옵니다.

만약 위의 ImmutableWrapper를 가변객체로 사용한다면 어떻게 될까요?

가변객체

public class MutableWrapper {
    private Object obj;
    public void setObj(Object obj) {
        this.obj = obj;
    }
    public Object getObj() {
        return this.obj;
    }
}

setter를 통해 A라는 객체를 세팅하고, 후에 B라는 객체를 세팅하게 되면 A 객체는 더 이상 참조하지 않기 때문에 GC 스캔 대상이 됩니다.

이렇게 스캔범위 및 스캔 빈도수가 늘어나게 되고 결국 GC의 성능이 떨어지게 됩니다.

그리고 중요한 사실이 있는데, GC는 새롭게 생성된 객체는 금방 죽는다는 Weak Generational Hypothesis 가설에 맞춰 설계되었습니다.

가변객체는 보통 하나의 객체를 가지고 상태를 변화하는 형태로 코드가 작성되기 때문에 오래 살아 있고 불변객체는 보통 생명주기가 짧기 때문에 GC가 처리하는데 더 적합합니다.

불변 객체 규칙

그럼 불변객체는 어떻게 만들까요?

Java에선 다음과 같이 4가지 규칙을 얘기하고 있습니다.

  1. 클래스를 final로 선언하라.
  2. 모든 클래스 변수를 privatefinal로 선언하라.
  3. 객체를 생성하기 위한 생성자나 팩토리 메소드를 추가하라.
  4. 참조에 의해 변경 가능성이 있는 경우 방어적 복사를 이용하여 전달하라.
public final class Team {

    private final String name;
    private final int memberCount;
    private final List<User> members;

    private Team(String name, int memberCount, List<User> members) {
        this.name = name;
        this.memberCount = memberCount;
        this.members = members;
    }

    public static Team of(String name, int memberCount, List members) {
        return new Team(name, memberCount, members);
    }

    public List getMembers() {
        return Collections.unmodifiableList(this.members);
    }

    // 그외 getter
}

위의 예시는 Team이라는 클래스이며 멤버 변수는 final로 선언되어 초기화 이후 변경할 수 없게 했습니다

초기화는 static factory method를 통해 객체를 초기화 하도록 했습니다.

그리고 getter 메소드를 통해 멤버 변수를 읽을 수만 있습니다.

특이한 점은 getMembers에서 Collections.unmodifiableList를 사용하여 방어적 복사를 진행한 것인데요.

그대로 members를 반환할 경우 members.add 등을 사용하여 members의 상태를 변경할 수 있기 때문에 방어적 복사를 사용했습니다.

마무리


이펙티브 자바에선 다음과 같은 문구가 있습니다.

클래스들은 가변적이여야 하는 매우 타당한 이유가 있지 않는  반드시 불변으로 만들어야 한다.
만약 클래스를 불변으로 만드는 것이 불가능하다면, 가능한 변경 가능성을 최소화하라.

사실 이번 포스팅을 작성하기 전까지 불변객체에 대해 코드 방면에선 분명히 좋지만 메모리 성능을 생각하면 이것저것 따질것이 많겠다라고 생각했습니다. 또한, 불변객체를 사용하게 하기 위해 다른 개발자를 설득함에 있어서도 어려울 것이라 생각했습니다.

하지만 GC의 성능을 오히려 높여준다는 얘기와 더불어 오라클에서도 권장하고 있으니 이유는 충분하겠지요.

이상 오늘은 불변객체에 대한 포스트를 작성해봤습니다.

오늘도 긴 제 글을 읽어주셔서 감사합니다.😀

📌참고


[Java] 불변 객체(Immutable Object) 및 final을 사용해야 하는 이유

[Java] Immutable Object(불변객체)

댓글남기기