Language/Java

[Java] 불변 객체 (Immutable Object)란?

苦盡甘來 2024. 4. 8. 23:25

 


불변 객체 (Immutable Object)란?

💡 자바(Java)를 이용한 OOP에서 불변 객체는 생성된 후에,
상태를 변경할 수 없는 객체를 의미한다.
읽기 전용(read-only) 메서드만을 제공하며, 인스턴스화 되면 내부 상태를 변경할 수 없다.
사전적으로 사물의 모양이나 성질이 달라질 수 없음을 의미한다.

 

👌 참고 - 가변 객체 (Mutable Object) vs 불변 객체 (Immutable Object)

 

➡️ 가변은 이름 그대로 처음 만든 이후 상태가 변할 수 있다는 의미이다.

 

➡️ 불변은 이름 그대로 처음 만든 이후 상태가 변하지 않는다는 의미이다.


특징

  • 캐시 안정성 (Caching)
    상태가 고정되어 있으므로, 캐시할 수 있다. 동일한 상태의 객체가 재사용되는 상황에서
    성능 향상으로도 이어질 수 있다.

  • 멀티 쓰레드 안정성 (Multi-Thread safety)
    여러 스레드가 동시에 수정할 위험 없이 접근할 수 있기때문에, 본질적으로 안전하다.
    즉, 멀티 쓰레드에 유용하며, 동기화를 고려하지 않아도 된다.

  • GC (Garbage Collection) 성능 향상
    생성되면 수정되지 않기에, 기존 개체를 재사용하며 객체의 생성 및 GC의 수집 빈도를 줄여준다.
    상태가 고정되머 있으므로, GC가 내부 객체 참조를 추적할 필요가 없다.
  • 보안 (Security)
    보안이 중요한 상황에서 자주 사용되는데, 문자열 조작에 불변 객체를 사용하면
    특정 유형의 보안 취약점을 방지할 수 있다.

  • 데이터 불변성 (Data Immutability)
    상태를 변경할 수 없으므로, 데이터 무결성을 유지하는데 도움이 된다.
    예상치 못한 수정에 대해 걱정 할 필요가없기에, 코드의 신뢰성을 높여준다.

코드 예시

예시를 보기전에 선언 방법 3가지에 대해 알아보려한다.

 

➡️ setter를 사용하지 않는다. (객체의 상태를 변경하는 메서드 미제공)

 

➡️ private으로 선언한다.

 

➡️ final을 선언한다.

 

 

⬇️ 하단 간단한 예시를 통해 자세히 알아보려한다.

 

Address.java

public class Address {

    private String value;

    public Address(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Address{" +
                "value='" + value + '\'' +
                '}';
    }
}

 

ImmutableAddress.java

public class ImmutableAddress {
    
    // final 불변 객체
    private final String value;

    public ImmutableAddress(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "Address{" +
                "value='" + value + '\'' +
                '}';
    }
}

 

불변 객체 실행 코드

public class RefMain2 {

    public static void main(String[] args) {

        // 참조형 변수는 하나의 인스턴스를 공유할 수 있다.
        ImmutableAddress a = new ImmutableAddress("서울"); // x001
        ImmutableAddress b = a; // x001

        // toString()을 재정의 했기에, 변수 값 출력
        System.out.println("a = " + a); // 실행 결과 ➡️ Address{value='서울'}
        System.out.println("b = " + b); // 실행 결과 ➡️ Address{value='서울'}

        /**
         * b의 값을 부산으로 변경 : final 객체이기때문에
         * setter가 존재하지 않음.
         * 즉, 새로운 인스턴스를 생성해서 할당해야한다.
         * b.setValue("부산");
         * */
        b = new ImmutableAddress("부산");

        System.out.println("부산 -> b");
        System.out.println("a = " + a); // 실행 결과 ➡️ Address{value='서울'}
        System.out.println("b = " + b); // 실행 결과 ➡️ Address{value='부산'}
    }
}

 

실행 결과

a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}

 

➡️ ImmutableAddress의 경우 값을 변경할 수 있는 b.setValue() 메서드 자체가 제거되었다.

 

➡️ setter() 메서드가 없음에 따라, 값이 변경 불가능하다는 사실을 알게되고, 불변 객체인 사실까지 깨달을 수 있다.

       예를 들어 b.setValue("부산")을 호출하려고 했는데, 해당 메서드가 없기에 컴파일 오류가 발생한다.

 

➡️ 따라서 새로운 ImmutableAddress("부산") 인스턴스를 생성해서 b에 대입한다.

 

➡️ 결과적으로 a, b는 서로 다른 인스턴스를 참조하고, a가 참조하던 ImmutableAddress는 그대로 유지된다.

 

 

⬇️ 하단 조금 더 복잡하고 의미있는 예시를 통해 자세히 알아보려한다.

 

불변 객체를 사용하지만 값을 변경해야하는 상황이 발생하면 어떻게 해야할까?

예를 들어서 기존 값에 새로운 값을 더하는 add()와 같은 메서드가 있다.

 

public class ImmutableObj {

    private final int value;

    public ImmutableObj(int value) {
        this.value = value;
    }

    public ImmutableObj add(int addValue) {
        int result = value + addValue;
        return new ImmutableObj(result);
    }

    public int getValue() {
        return value;
    }
}

 

➡️ 여기서 핵심은 add() 메서드이다.

 

➡️ 불변 객체는 값을 변경하면 안된다. 그러면 이미 불변 객체가 아닌걸까?

 

➡️ 하지만 위 예제 소스에서는 기존 값에 새로운 값을 더해야 한다.

 

➡️ 기존 값은 변경하지 않고, 대신 계산 결과를 바탕으로 새로운 객체를 만들어서 반환한다.

 

➡️ 이렇게하면 불변도 유지하면서 새로운 결과도 만들 수 있다.

 

public class ImmutableMain1 {

    public static void main(String[] args) {
        ImmutableObj obj1 = new ImmutableObj(10);
        ImmutableObj obj2 = obj1.add(20);

        // 계산 이후에도 기존값과 신규값 모두 확인 가능
        System.out.println("obj1 = " + obj1.getValue());
        System.out.println("obj2 = " + obj2.getValue());
    }
}

 

실행 결과

obj1 = 10
obj2 = 30

 

 

 

➡️ add(20)을 호출한다.

 

➡️ 기존 객체에 있는 10과 인수로 입력한 20을 더한다.

       이때 기존 객체의 값을 변경하면 안되므로 계산 결과를 기반으로

       새로운 객체를 만들어서 반환한다. 

 

➡️ 새로운 객체는 x002 참조를 가진다. 새로운 객체의 참조값을 obj2에 대입한다.

 

 

⚠️ 만일 새로 생성된 반환 값을 사용하지 않으면 어떻게 될까?

public class ImmutableMain2 {

    public static void main(String[] args) {
        ImmutableObj obj1 = new ImmutableObj(10);
        obj1.add(20);

        /**
         * 불변 객체에서 변경과 관련된 메서드들은 보통 새로운 객체를 반환하기때문에,
         * 꼭 반환 값을 받아야 한다.
         * */
        System.out.println("obj1 = " + obj1.getValue());
    }
}

 

실행 결과

obj1 = 10

 

실행 결과처럼 아무것도 처리되지 않은 것 처럼 보인다.

불변 객체에서 변경과 관련된 모든 메서드들은 보통 객체를 새로 만들어서 반환하기 때문에

반드시 반환 값을 받아야한다.


OUTRO

오늘은 불변 객체에 대해서 알아봤습니다.

 

저번 포스팅 참조형 타입에서 객체 간 공유를 막을 방법이 없기에 사이드 이펙트가 발생했다는 내용이 있었는데,

 

[Java] 기본형과 참조형(Primitive Type & Reference Type)

데이터 타입 https://bit.ly/4cMZkYh [Java] 변수와 데이터 타입이란? 변수(variable)란? 💡말 그대로 변하는 수 즉, 고정되지 않은 수를 변수라고 의미한다. 프로그래밍 언어에선 변수란 값을 저장할 수 있

hyun-dev-com.tistory.com

 

불변객체와 이어지는 내용이므로 한번 참고해보시길 바랍니다.

 

그러면 다음 포스팅에서 뵙겠습니다.

 

위 포스팅 글은 김영한님의 실전 자바 - 중급 1편을 참고했습니다.

 

김영한의 실전 자바 - 중급 1편 | 김영한 - 인프런

김영한 | 실무에 필요한 자바의 다양한 중급 기능을 예제 코드로 깊이있게 학습합니다., [사진]국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바

www.inflearn.com