Notice
Recent Posts
Recent Comments
Link
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

horizonyeong

불변 객체 본문

김영한님 강의 정리/실전 자바 - 중급 1편

불변 객체

horizonyeong 2025. 5. 3. 01:32

기본형과 참조형의 공유

자바의 데이터 타입을 갖아 크게 보면 기본형(Primitive Type)과 참조형(Reference Type)으로 나눌 수 있다.

  • 기본형: 하나의 값을 여러 변수에서 절대로 공유하지 않는다.
  • 참조형: 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있다.

기본형

기본형은 하나의 값을 여러 변수에서 절대로 공유하지 않는다.

public class PrimitiveMain {

    public static void main(String[] args) {

        //기본형은 절대로 같은 값을 공유하지 않는다.
        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

 

  • 기본형 변수 a와 b는 절대로 하나의 값을 공유하지 않는다.
  • b = a 라고 하면 자바는 항상 값을 복사해서 대입한다. (이 경우 a에 있는 값 10을 복사해서 b에 전달한다.)
  • 결과적으로 a 와 b 모두 10 이라는 똑같은 값을 가진다. 하지만 a의 10과 b의 10은 복사된 완전히 다른 10이다.
  • 메모리 상에 a에 속하는 10, b에 속하는 10이 각각 별도로 존재한다.

참조형

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 + '\'' +
                '}';
    }
}

//단순히 주소를 보관하는 객체이다.
//객체의 값을 편하게 확인하기 위해 soString() 오버라이딩
public class AddressMain {

    public static void main(String[] args) {

        Address a = new Address("서울");
        Address b = a;

        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 라고 하면 a 에 있는 참조값을 복사해서 b에 전달한다.
  • a 와 b 모두 서울이라는 주소를 가진다. (같은 인스턴스 참조)
  • 이후에 b의 주소를 변경하면 a 의 주소도 함께 부산으로 변경되어 버린다.

공유 참조와 사이드 이펙트

사이드 이펙트 (side effect)는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말한다.

  • b의 주소값(value)를 서울에서 부산으로 변경할 의도로 값 변경을 시도
  • 하지만 a,b는 같은 인스턴스를 참조 -> a 의 값도 부산으로 변경

사이드 이펙트는 프로그램의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생

디버깅이 어려워지고 코드의 안정성이 저하될 수 있다.

 

사이드 이펙트 해결방안

다음과 같이 a 와 b가 처음부터 서로 다른 인스턴스를 참조하면 된다.

Address a = new Address("서울");
Address b = new Address("부산");
a = Address{value='서울'}
b = Address{value='서울'}
b -> 부산
a = Address{value='서울'}
b = Address{value='부산'}

 

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

지금까지 발생한 모든 문제는 같은 객체(인스턴스를) 변수 a, b가 함께 공유하기 때문이다.

따라서 객체를 공유하지 않으면 문제가 해결된다.

여기서 변수 a, b가 서로 각각 다른 주소지로 변경할 수 있어야 한다. 이렇게 하러면 서로 다른 객체를 참조하면 된다.

 

즉 서로 다른 객체를 참조해서 같은 객체를 공유하지 않으면 문제가 해결된다.

여러 변수가 하나의 객체를 공유하지 않으면 된다.

하지만 여러 변수가 하나의 객체를 공유하지 않도록 막을 방법은 없다.

 Address a = new Address("서울");
 Address b = a;

 

공유 참조로 인해 발생하는 문제를 어떻게 해결할 수 있을까?

 

불변 객체 - 도입

사이드 이펙트의 근본적인 원인을 고려해보면, 객체를 공유하는 것 자체는 문제가 아니다. 

객체를 공유한다고 바로 사이드 이펙트가 발생하지 않는다.

문제의 직접적인 원인은 공유된 객체의 값을 변경한것에 있다.

 

a, b는 처음에는 둘다 "서울" 이라는 주소를 사용해야 한다. 이후에 b의 주소를 "부산"으로 변경해야 한다.

(공유된 객체의 값을 변경하고 있다.)

 

따라서 처음에는 "서울" 이라는 Address 인스턴스를 a, b 가 함께 사용하는것이, 다음과 같이 서로 다른 인스턴스를

사용하는 것보다 메모리와 성능상 더 효율적이다. 인스턴스가 하나이니 메모리가 절약되고, 인스턴스를 하나만 생성하여

생성 시간이 줄어 효율적이다.

 //"서울" Address 공유
 Address a = new Address("서울");
 Address b = a;
 
 //"서울" Address 객체 2개
 Address a = new Address("서울");
 Address b = new Address("서울");

 

공유 참조를 사용해도 아무런 문제가 없다. 오히려 효율적이다.

 

사이드 이펙트는 이후에 b가 공유 참조하는 Address객체 (인스턴스)의 값을 변경하기 때문에 발생한다.

공유될 수 있는 Addreess 객체의 값을 어디선가 변경했기 때문

 

Addreess 객체의 값을 변경하지 못하게 설계한다면 사이드 이펙트가 발생하지 않는다.

 

불변 객체 (Immutable Object)

객체의 상태(객체  내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체라 한다.

public class ImmutableAddress {

    private final String value;

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

    public String getValue() {
        return value;
    }
    
    @Override
    public String toString() {
        return "Address{" +
                "value='" + value + '\'' +
                '}';
    }
  • 내부 값이 변경되면 안된다. value의 필드를 final로 선언
  • 값을 변경하는 setValue() 제거. (final로 선언했기에 사용불가)
  • 불변 객체는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경할 수 없다.

이처럼 불변 클래스는 어떻게든 필드 값을 변경할 수 없게 클래스를 설계하면 된다.

public class AddressMain {

    public static void main(String[] args) {

        ImmutableAddress a = new Address("서울");
        ImmutableAddresss b = a; // 참조값 대입을 막을 수 없다

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

 

setValue() 메서드가 제거 되었으니

b.setValue() 로 a 와 공유하는 객체의 값을 변경 할 수 없다. 따라서 b에 "부산" 값을 갖는 객체를 생성해서 b에 대입해야 한다.

결과적으로 a, b 는 서로 다른 인스턴스를 참조한다. a의 값은 유지된다

 

정리

불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 막을 수 있다.

  • 객체의 공유 참조는 막을 수 없다. 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이펙트가 발생. 사이드 이펙트가 발생하면 안되는 상황이라면 불변 객체를 사용한다.
  • 불변 객체는 값을 변경 할 수 없다. 따라서 불변 객체의 값을 변경하고 싶다면 변경하고 싶은 값으로 새로운 불변객체를 생성해야 된다. 기존 변수들이 참조하는 값에 영향을 주지 않게 된다.

불변 객체 - 값 변경

불변 객체를 사용하지만 그래도 값을 변경해야 하는 메서드가 필요하면 어떻게 해야할까

public class ImmutableObj {

    private final int value;

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

    public int getValue() {
        return value;
    }

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

}
  • 여기서 핵심은 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을 더한다. 기존 객체의 값을 변경하면 안되므로 계산 결과를 기반으로 새로운 객체를 만들어서 반환한다.
  • 새로운 객체는 다른 참조를 가진다. 새로운 객체의 참조값을 obj2에 대입한다.
  • 생성된 반환 값을 변수에 대입하지 않으면 출력되지 않는다.
//ImmutableObj obj2 = obj1.add(20);
obj1.add(20); //출력안됨

 

참고 - withXxx()

불변 객체에서 값을 변경하는 경우 withYear() 처럼 "with"로 시작하는 경우가 많다.

coffe withsugar 라고 하면, 커피에 설탕이 추가되어 원래의 상태를 변경하여 새로운 변형을 만든다는것을 의미한다.

불변 객체의 메서드가 "with"로 이름 지어진 경우, 그 메서드가 지정된 수정사항을 포함하는 객체의 새 인스턴스를 반환한다는 사실이다. "with"는 관례처럼 사용되는데, 원본 객체의 상태가 그대로 유지됨을 강조하면서 변경사항을 새 복사본에 포함하는 과정을 간결하게 포함한다.

 

본 내용과 자료는 인프런의 김영한 님의 "김영한의 실전자바 중급 1편" 강의를 참고하고 있습니다.

(내돈내산 노트 정리)