Course/Java

[java-mid1] 2. 불변 객체

Lpromotion 2025. 3. 4. 01:43
"김영한의 실전 자바 - 중급편" 내용을 참고하여 정리함.

목차
 

1. 기본형과 참조형의 공유

자바의 데이터 타입은 크게 기본형 (Primitive Type)과 참조형 (Reference Type)으로 나뉜다.

  • 기본형: 하나의 값을 여러 변수에서 공유하지 않음. → 값을 복사하여 대입하기 때문에 공유되지 X
  • 참조형: 하나의 객체를 참조값을 통해 여러 변수에서 공유 가능. → 참조값을 공유하기 때문에 같은 인스턴스를 바라봄.

 

기본형 예제 (값 복사)

int a = 10;
int b = a; // a -> b, 값 복사 후 대입
System.out.println("a = " + a);
System.out.println("b = " + b);

b = 20;
System.out.println("20 -> b");
System.out.println("a = " + a);
System.out.println("b = " + b);
a = 10
b = 10
20 -> b
a = 10
b = 20
  • `b = a` 실행 시 값을 복사하여 대입하므로 `b`의 변경이 `a`에 영향을 미치지 않는다.
  • 메모리 상에서도 `a`에 속하는 `10`과 `b`에 속하는 `10`이 각각 별도록 존재한다.
  • 기본형 변수는 하나의 값을 절대로 공유하지 않는다.

 

참조형 예제 (객체 공유)

Address a = new Address("서울");
Address b = a;
System.out.println("a = " + a);
System.out.println("b = " + b);

b.setValue("부산"); // b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='부산'}
b = Address{value='부산'}
  • `a`의 참조값을 복사하여 전달하므로 `a`와 `b`는 같은 인스턴스를 참조한다.
  • `a`와 `b`가 같은 객체를 참조하기 때문에 `b`의 값 변경이 `a`에도 영향을 준다.
  • 참조형 변수는 참조값을 통해 같은 객체(인스턴스)를 공유할 수 있다.

 

 

2. 공유 참조와 사이드 이펙트

  • 사이드 이펙트(Side Effect):
    • 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것.
    • 한 부분에서 발생한 변경이 의도치 않게 다른 부분에도 영향을 미치는 현상.
    • 객체를 공유할 때 발생하는 대표적인 문제.

 

사이드 이펙트 발생 예제

Address a = new Address("서울");
Address b = a; // 같은 객체를 공유

b.setValue("부산"); // b의 값 변경

System.out.println("a = " + a); // 부산 (a도 영향을 받음) -> 사이드 이펙트 발생
System.out.println("b = " + b); // 부산
  • `a`, `b`는 같은 객체(인스턴스)를 참조한다.
  • `b`의 값만 변경할 의도였지만 `a`도 영향을 받는다. → 사이드 이펙트 발생
  • 사이드 이펙트가 발생하면 디버깅이 어려워지고 코드의 안정성이 저하될 수 있다.

 

해결 방안

처음부터 서로 다른 인스턴스를 참조한다.

Address a = new Address("서울");
Address b = new Address("서울"); // 새로운 객체를 생성
  • `a`와 `b`는 서로 다른 인스턴스를 참조하므로, 한 객체를 변경해도 다른 객체에 영향을 주지 않는다.

 

Address a = new Address("서울"); // x001
Address b = new Address("서울"); // x002
System.out.println("a = " + a);
System.out.println("b = " + b);

b.setValue("부산");
System.out.println("부산 -> b");
System.out.println("a = " + a);
System.out.println("b = " + b);
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
  • 서로 다른 객체를 참조하므로 `b`가 참조하는 인스턴스의 값을 변경해도 `a`에는 영향이 없다.

 

여러 변수가 하나의 객체를 공유하는 것을 막을 방법은 없다

앞서 본 것처럼, 객체를 공유하지 않도록 설계하면 사이드 이펙트 문제를 해결할 수 있다.
하지만 현실적으로 객체 공유를 막을 수 있는 완벽한 방법은 없다.

 

객체 공유를 막을 수 없는 이유

Address a = new Address("서울");
Address b = a; // 참조값을 공유 (막을 방법이 없음, 문법상 문제가 없음)
  • 참조값을 대입하는 것을 문법적으로 막을 방법이 없다.
  • 개발자가 실수로 `b = a`;라고 해도 자바는 이를 허용한다.
  • 즉, 객체를 공유하지 않도록 강제할 방법이 없다.

 

 

3. 불변 객체 - 도입

불변 객체(Immutable Object)

  • 한 번 생성되면 내부 상태가 절대 변경되지 않는 객체
  • 값을 변경할 수 없으므로 객체를 공유하더라도 안전함
  • 기존 객체의 값을 변경하는 대신 새로운 객체를 만들어서 반환

 

불변 객체가 필요한 이유

  • 공유 참조로 인한 문제 (사이드 이펙트)
    • 객체를 여러 변수가 공유하면 값을 변경할 때 예기치 않은 사이드 이펙트가 발생할 수 있음.
    • 공유 참조로 인해 값이 예측하지 못한 방식으로 변경될 위험이 있음.
  • 객체 공유를 완전히 막을 방법이 없음
    • 참조값을 대입하는 것을 문법적으로 막을 방법이 없음.
    • 개발자가 실수로 b = a;라고 해도 자바는 이를 허용.
    • 즉, 객체를 공유하지 않도록 강제할 방법이 없다.

=> 결론: 객체 공유를 완벽히 막을 수 없다면, 차라리 값 변경을 불가능하게 만드는 것이 더 효과적이다.
=> 해결책: 불변 객체 (Immutable Object)

 

불변 객체의 원리

  • 불변 객체는 객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체
  • 내부 필드를 final로 선언하여 변경할 수 없도록 함.
  • 값을 변경하는 setter 메서드를 제공하지 않음.
  • 새로운 값을 적용하려면 새로운 객체를 생성해서 반환함.

 

불변 객체 설계 예제

public class ImmutableAddress {
    private final String value; // 필드를 final로 선언

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

    public String getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "Address{" + "value='" + value + '\'' + '}';
    }
}
  • final 키워드 사용 → 한 번 설정된 `value` 값은 변경할 수 없음.
  • Setter 메서드 제거 → `setValue()` 가 없으므로 값 변경이 불가능.
  • 객체 생성 이후 상태 변경 불가 → ImmutableAddress 객체는 한 번 생성되면 절대 변경되지 않음.

=> 불변 클래스를 만드는 방법은, 객체 안에 있는 상태가 바뀌지 않도록 설게하면 된다.

 

불변 객체 사용 예제

// 참조형 변수는 하나의 인스턴스를 공유할 수 있다.
ImmutableAddress a = new ImmutableAddress("서울"); // x001
ImmutableAddress b = a; // 참조값 대입을 막을 수 있는 방법이 없다. // x001
System.out.println("a = " + a);
System.out.println("b = " + b);

// b.setValue("부산"); // 컴파일 오류 발생
b = new ImmutableAddress("부산"); // 새로운 객체를 생성하여 할당
System.out.println("부산 -> b");
System.out.println("a = " + a);
System.out.println("b = " + b);

 

실행 결과

a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
  • `ImmutableAddress` 은 불변 객체이므로 `b` 가 참조하는 인스턴스의 값을 서울에서 부산으로 변경하려면
    새로운 인스턴스를 생성해서 할당해야 한다.

 

불변 객체의 특징

특징 설명
값 변경 불가능 내부 상태가 한 번 설정되면 변경 불가
객체 공유 안전 공유되어도 값이 바뀌지 않으므로 안전
멀티쓰레드 안전 동기화 없이도 여러 쓰레드에서 안전하게 사용 가능
변경 시 새 객체 반환 기존 객체를 변경하지 않고 새로운 객체를 생성하여 반환

 

 

정리

  • 객체를 공유하지 않더라도, 값 변경이 가능하면 사이드 이펙트가 발생할 수 있다.
  • 객체를 공유하는 것을 막을 방법은 없기 때문에, 차라리 값 변경을 불가능하게 만드는 것이 효과적이다.
  • 불변 객체(Immutable Object)를 사용하면 공유 참조로 인한 값 변경 문제를 근본적으로 해결할 수 있다.
  • 불변 객체는 값이 변경되지 않으며, 값 변경이 필요하면 새로운 객체를 생성하여 반환한다.

 

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

  • 가변: 처음 만든 이후 상태가 변할 수 있다는 뜻. (사전적으로 사물의 모양이나 성질이 달라질 수 있다는 뜻)
  • 불변: 처음 만든 이후 상태가 변하지 않는다는 뜻. (사전적으로 사물의 모양이나 성질이 달라질 수 없 다는 뜻)

 

 

4. 불변 객체 - 예제

불변 객체는 객체가 생성된 후 내부 값을 변경할 수 없도록 설계된 객체다.

1. 모든 필드를 final로 선언하여 변경을 방지한다.
2. Setter 메서드를 제공하지 않는다.
3. 값이 변경되면 새로운 객체를 반환하도록 구현한다.

 

불변 클래스 예제: MemberV2

public class MemverV2 {

    private String name;
    private ImmutableAddress address; // 불변 객체 사용

    public MemverV2(String name, ImmutableAddress address) {
        this.name = name;
        this.address = address;
    }

    public ImmutableAddress getAddress() {
        return address;
    }

    public void setAddress(ImmutableAddress address) {
        this.address = address;
    }

    @Override
    public String
    toString() {
        return "MemverV1{" +
                "name='" + name + '\'' +
                ", address=" + address +
                '}';
    }
}
  • 주소를 변경할 수 없는, 불변인 `ImmutableAddress 를 사용한다.

 

불변 객체 사용 예제

ImmutableAddress address = new ImmutableAddress("서울");
MemverV2 memberA = new MemverV2("회원A", address);
MemverV2 memberB = new MemverV2("회원B", address);

// 회원A, 회원B의 처음 주소는 모두 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);

// 회원B의 주소를 부산으로 변경해야함
// memberB.getAddress().setValue("부산"); // 컴파일 오류
memberB.setAddress(new ImmutableAddress("부산")); // 새로운 객체를 생성하여 변경
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);

 

실행 결과

memberA = MemverV1{name='회원A', address=Address{value='서울'}}
memberB = MemverV1{name='회원B', address=Address{value='서울'}}
부산 -> memberB.address
memberA = MemverV1{name='회원A', address=Address{value='서울'}}
memberB = MemverV1{name='회원B', address=Address{value='부산'}}
  • `ImmutableAddress` 에는 값을 변경할 수 있는 메서드가 없다.
  • 새로운 주소 객체를 생성하여 `setAddress` 로 전달한다.
  • 결과적으로 사이드 이펙트가 발생하지 않고, 회원A는 기존 주소를 유지한다.

 

 

5. 불변 객체 - 값 변경

불변 객체는 값 변경이 불가능하지만, 새로운 값을 적용할 방법이 필요하다.
이를 위해, 기존 객체를 변경하는 것이 아니라 "새로운 객체를 만들어서 반환"해야 한다.

 

가변 객체(Mutable Object) 방식

public class MutableObj {

    private int value;

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

    public void add(int addValue) { // 기존 값에 새로운 값을 더하는 메서드
        value = value + addValue; // 기존 값 변경 (불변 객체 아님)
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}
public class MutableMain {

    public static void main(String[] args) {
        MutableObj obj = new MutableObj(10);
        obj.add(20);
        // 계산 이후의 기존 값은 사라짐
        System.out.println("obj = " + obj.getValue());
    }
}

 

실행 결과

obj = 30

 

문제점

  • `add()` 호출 시 기존 객체의 값이 변경된다.
  • 공유된 객체라면 예상치 못한 값 변경이 발생할 위험이 있다.
  • 사이드 이펙트 발생 가능히다.

 

불변 객체에서 값 변경하기 (새로운 객체 반환)

public class ImmutableObj {

    private final int value; // final 필드 (변경 불가)

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

    public ImmutableObj add(int addValue) {
        return new ImmutableObj(value + addValue); // 기존 객체 변경 없이 새로운 객체 반환
    }

    public int getValue() {
        return value;
    }
}
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

기존 객체 obj1은 변경되지 않는다.
obj2는 obj1의 값을 기반으로 새로운 객체로 생성된다.
불변 객체 방식으로 값 변경을 안전하게 처리할 수 있다.

 

 

불볍 객체 설계 시 기존 값을 변경해야 하는 메서드가 필요한 경우

  • 기존 객체의 값을 그대로 두고, 변경된 결과를 새로운 객체에 담아서 반환한다.
  • 기존 객체는 변경되지 않는다.
  • 객체 공유 문제가 없다. (사이드 이펙즈 방지)

 

 

참고 - withXxx()

불변 객체에서 값을 변경하는 경우, 메서드 이름이 withXxx() 형태로 자주 사용된다.

 

"with"의 의미

  • "coffee with sugar" → 커피에 설탕이 추가되어 원래의 상태를 변경하여 새로운 변형을 만든다는 것을 의미
  • 프로그래밍에서도 "with"는 원본 객체를 유지한 채 일부 변경사항을 반영한 새 객체(인스턴스)를 반환하는 의미

 

불변 객체에서 "with" 사용 예시

LocalDate date = LocalDate.of(2024, 3, 4);
LocalDate newDate = date.withYear(2025); // 새로운 객체 반환

System.out.println(date); // 2024-03-04
System.out.println(newDate); // 2025-03-04
  • `date` 객체는 변경되지 않고, `newDate` 라는 새로운 객체가 반환됨
  • `withYear(2025)` 는 "연도를 2025로 변경한 새로운 LocalDate 객체를 반환"

"with"는 불변 객체에서 값을 변경할 때 새로운 객체를 반환하는 메서드 명명 규칙으로 자주 사용된다.
원본 객체는 변경되지 않는다는 점을 강조하면서, 새로운 객체를 생성하는 과정을 직관적으로 표현한다.

 

 

6. 정리

불변 객체를 학습한 이유

  • 자바에서 가장 많이 사용되는 String 클래스가 대표적인 불변 객체(Immutable Object)
  • 뿐만 아니라 Integer, LocalDate 등 자바 기본 제공 클래스 중 다수가 불변 객체로 설계됨
  • 따라서 불변 객체의 필요성과 원리를 이해해야 자바 기본 클래스를 더 깊이 있게 이해할 수 있음

 

모든 클래스를 불변 객체로 만들지는 않는다

  • 우리가 개발하는 대부분의 클래스는 값을 변경할 수 있는 가변 객체(Mutable Object)로 설계됨
    • 예: 회원 정보 관리 클래스 → 회원의 속성(이름, 주소 등)은 변경될 수 있어야 함
  • 불변 객체는 "값이 변경되면 안 되는 특별한 경우"에 만들어서 사용
  • 때로는 같은 기능을 하는 클래스를 하나는 불변, 하나는 가변으로 각각 만드는 경우도 있음

 

불변 객체의 주요 장점

  • 캐시(Cache) 안정성
  • 멀티 쓰레드(Thread) 환경에서 안전하게 사용 가능
  • 엔티티의 값 타입을 안정적으로 유지할 수 있음
반응형