관리 메뉴

나구리의 개발공부기록

불변 객체, 기본형과 참조형의 공유, 공유 참조와 사이드 이펙트, 불변 객체(도입, 예제, 값 변경) 본문

인프런 - 실전 자바 로드맵/실전 자바 - 중급 1편

불변 객체, 기본형과 참조형의 공유, 공유 참조와 사이드 이펙트, 불변 객체(도입, 예제, 값 변경)

소소한나구리 2025. 1. 17. 14:41

출처 : 인프런 - 김영한의 실전 자바 - 중급1편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


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

1) 기본형과 참조형

(1) 설명

  • 자바의 데이터 타입을 가장 크게 보면 기본형(Primitive Type)과 참조형(Reference Type)으로 나눌 수 있음
  • 기본형: 하나의 값을 여러 변수에서 절대로 공유하지 않음
  • 참조형: 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있음
  • 자바 기본편에서 했던 내용을 복습, https://nagul2.tistory.com/388

2) 기본형 예제

(1) PrimitiveMain

  • 기본형 변수 a와 b는 절대로 하나의 값을 공유하지 않고, b = a 라고 하면 자바는 항상 값을 복사해서 대입하므로 a에 있는 값 10을 복사해서 b에 전달함
  • a와 b는 둘다 10이라는 똑같은 숫자의 값을 가지지만 a가 가지는 10과 b가 가지는 10은 복사된 완전히 다른 10이며 메모리 상에서도 별도로 존재함
  • b = 20 이라고하면 b의 값만 20으로 변경 a는 그대로 10이되어 프로그램을 실행해보면 예상했던 결과가 나오는 것을 확인할 수 있음
  • 기본형 변수는 하나의 값을 절대로 공유하지 않으므로 값을 변경해도 변수 하나의 값만 변경됨
package lang.immutable.address;

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
*/

3) 참조형 예제

(1) Address

  • 주소 필드 하나만 있고 생성자, 게터, 세터, 오버라이딩 된 toString을 가지고 있는 클래스
package lang.immutable.address;

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

 

(2) RefMain1_1

  • a, b 모두 "서울" 이라는 주소를 가지도록 작성 후 b.setValue()를 통해 b의 주소를 부산으로 변경하였음
  • 결과를 실행해보면 b뿐만 아니라 a의 주소도 함께 부산으로 변경되었음
package lang.immutable.address;

public class RefMain1_1 {
    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("부산"); // 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='부산'}
*/

 

(3) 그림으로 설명

  • 참조형 변수들은 같은 참조값을 통해 같은 인스턴스를 참조할 수 있으므로 b = a라고 하면 a에 있는 참조값을 복사해서 b에 전달함
  • 즉 자바에서 모든 값 대입은 변수가 가지고 있는 값을 복사해서 전달하기 때문에 int같은 숫자 값을 가지고 있다면 숫자값을 복사해서 전달하고 참조값을 가지고 있으면 참조값을 복사해서 전달함
  • a는 참조변수이기 때문에 참조값을 복사해서 전달하므로 결과적으로 a, b는 x001이라는 같은 인스턴스를 참조하게됨

(4) 정리

  • 기본형 변수는 절대로 같은 값을 공유하지 않음
  • 참조형 변수는 참조값을 통해 같은 객체(인스턴스)를 공유할 수 있음
  • 메모리 구조를 보면 바로 답을 알 수 있지만 개발을 하다보면 누구나 이런 실수를 할 수 있음

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

1) 공유 참조와 사이드 이펙트

(1) 사이드 이펙트(Side Effect)

  • 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말함
  • 예제에서 b의 값을 변경하는 의도로 b.setValue("부산")으로 변경을 시도하였는데 a까지 함께 변경된 상황 처럼 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 사이드 이펙트라함
  • 프로그래밍에서 사이드 이펙트는 보통 부정적인 의미로 사용되는데 프로그램의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생하며 디버깅이 어려워지고 코드의 안정성이 저하될 수 있음

(2) 사이드 이펙트 해결 방안 - RefMain1_2

  • a와 b가 처음부터 서로 다른 인스턴스를 참조하면 매우 단순하게 해결할 수 있음
  • a와 b는 완전 다른 인스턴스를 참조하기 때문에 b가 참조하는 인스턴스의 값을 변경해도 a에는 영향이 없음
package lang.immutable.address;

public class RefMain1_2 {
    public static void main(String[] args) {
        // 참조형 변수는 하나의 인스턴스를 공유할 수 있음
        Address a = new Address("서울");
        Address b = new Address("서울");
        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='부산'}
*/

 

(3) 여러 변수가 하나의 객체를 공유하는 것을 막을 방법이 없음 - 공유 참조

  • 지금까지 발생한 모든 문제는 객체(인스턴스)를 변수 a, b가 함께 공유하기 때문에 발생하였으므로 객체를 공유하지 않으면 문제가 해결됨
  • 즉, 하나의 객체를 공유하지 않으면 지금까지 설명한 문제들이 발생하지 않는데 문제는 자바 문법상 하나의 객체를 여러 변수가 공유하지 않도록 강제로 막을 수 있는 방법이 없음
  • Address b = a와 같은 코드를 모든 개발자가 사용하지 않도록 한다고 해도 개발자가 실수로 b = a라고 입력을 해도 IDE상에서는 아무런 오류가 발생하지 않음
  • 자바 문법상 정상적인 코드이기 때문에 참조값을 다른 변수에 대입하는 순간 여러 변수가 하나의 객체를 공유하게 되며 객체의 공유를 막을 방법이 없음
  • 기본형은 항상 값을 복사해서 대입하기 때문에 값이 절대로 공유되지 않지만 참조형의 경우 참조값을 복사해서 대입하기 때문에 여러 변수에서 얼마든지 같은 객체를 공유할 수 있게되고 이러한 사이드 이펙트를 만드는 경우가 생김(물론 객체의 공유가 꼭 필요할 때도 있음)

(4) RefMain1_3

  • 앞서 작성한 코드와 같은 결과가 나오는 코드인데 단순히 change() 메서드만 하나 추가 되었고 change()메서드에서 인수로 넘어온 Address의 인스턴스에 있는 value값을 변경함
  • main() 메서드만 보면 a의 값이 함께 부산으로 변경된 이유를 찾기가 더 어려우며, 실무에서는 이것보다 더 코드가 복잡하므로 사이드 이펙트가 어디서 터질지 모르는 경우도 많음
  • 여러 변수가 하나의 객체를 참조하는 공유 참조를 막을 수 있는 방법이 없는데 단순히 개발자가 공유 참조 문제가 발생하지 않도록 조심해서 코드를 작성하는 것은 한계가 있음
package lang.immutable.address;

public class RefMain1_3 {
    public static void main(String[] args) {
        // 참조형 변수는 하나의 인스턴스를 공유할 수 있음
        Address a = new Address("서울");
        Address b = a;
        System.out.println("a = " + a);
        System.out.println("b = " + b);

        change(b,"부산"); // b의 값을 부산으로 변경
        System.out.println("a = " + a);
        System.out.println("b = " + b);
    }

    private static void change(Address address, String changeAddress) {
        System.out.println("주소 값을 변경합니다 ->" + changeAddress);
        address.setValue(changeAddress);
    }
}
/* 실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
주소 값을 변경합니다 ->부산
a = Address{value='부산'}
b = Address{value='부산'}
*/

3. 불변 객체

1) 도입

(1) 사이드 이팩트가 발생한 근본적인 원인

  • 지금까지 발생한 문제를 보면 공유하면 안되는 객체를 여러 변수에서 공유하여 발생한 사이드 이펙트 때문인데 객체의 공유를 막을 수 있는 방법은 없음
  • 하지만 사이드 이펙트의 더 근본적인 원인을 생각해보면 객체를 공유하는 것 자체만으로는 사이드 이펙트가 발생되는 것은 아니고 문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있음
  • a, b는 처음 시점에는 둘다 "서울"이라는 주소를 사용해야 하고 그 이후에 b의 주소를 "부산"으로 변경해야 하므로 처음에는 Address a = b 와 같이 Address 인스턴스를 a, b가 함께 사용하는 것이 서로 다른 인스턴스를 사용하는 것보다 메모리와 성능상 더 효율적일 수 있음
  • 여기까지는 아무런 문제가 발생하지 않고 오히려 더 효율적이지만 진짜 문제는 이후에 b가 공유 참조하는 인스턴스의 값을 변경하기 때문에 발생했음
  • 참조형인 객체는 처음부터 여러 참조형 변수에서 공유 될 수 있도록 설계되었으므로 이부분은 문제가 아님
  • 문제의 직접적인 원인인 공유 될 수있는 Address 객체의 값을 변경했기 때문이며 Address 객체의 값을 변경하지 못하게 설계했다면 이런 사이드 이펙트 자체가 발생하지 않았을 것임

(2) 불변 객체 도입 - ImmutableAddress

  • 객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 함
  • 내부 값이 변경되면 안되기 때문에 value 필드를 final로 선언하고 값을 변경할 수 있는 setValue()를 제거함
  • 이 클래스는 생성자를 통해서만 값을 설정할 수 있고 이후에는 값을 변경하는 것이 불가능함
  • 어떻게든 필드 값을 변경할 수 없게 클래스를 설계하면 불변 클래스를 만들 수 있음
package lang.immutable.address;

public class ImmutableAddress {

    private final String value;	// final로 변경

    // setValue() 제거
    // ... 나머지 코드는 동일 생략
}

 

(3) RefMain2

  • ImmutableAddress의 경우 값을 변경할 수 있는 setValue() 메서드 자체가 제거되었으므로 인스턴스의 값을 변경할 수 있는 방법이 없음
  • ImmutableAddress를 사용하는 개발자는 값을 변경하려고 시도하다가, 값을 변경하는 것을 불가능하다는 것을 알고 불변 객체임을 깨닫게 됨
  • 결국 새로운 ImmutableAddress 인스턴스를 생성하여 b에 대입하게 되고 결과적으로 a, b는 서로 다른 인스턴스를 참조하게 됨
package lang.immutable.address;

public class RefMain2 {
    public static void main(String[] args) {

        ImmutableAddress a = new ImmutableAddress("서울");
        ImmutableAddress 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);
    }
}
/* 실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
b -> 부산
a = Address{value='서울'}
b = Address{value='부산'}
*/

 

(3) 정리

  • 불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 막을 수 있음
  • 객체의 공유 참조 자체는 막을 수 없으므로 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이펙트가 발생함
  • 불변객체는 값을 변경할 수 없기 때문에 사이드 이펙트가 발생하면 안되는 상황이라면 불변 객체를 만들어서 사용하면 사이드 이펙트가 원천 차단됨
  • 불변 객체는 값을 변경할 수 없으므로 불변 객체의 값을 변경하고 싶다면 원하는 값으로 새로운 불변객체를 생성해야 하며 이렇게하면 기존 변수들이 참조하는 값에는 영향을 주지 않음

** 참고

  • 가변(Mutable) 객체 vs 불변(Immuatable) 객체
  • 가변은 처음 만든 이후 상태가 변할 수 있다는 뜻이고 불변은 처음 만든 이후 상태가 변하지 않는다는 뜻임
  • 예제에서의 Address는 가변 클래스이므로 이 객체를 생성하면 가변 객체가 되고 ImmutableAddress는 불변 클래스이므로 이 객체를 생성하면 불변객체가 됨

2) 예제

(1) MemberV1 - 변경 클래스

  • 변경 가능한 클래스로 생성자, 게터, 세터, 오버라이딩한 toString()을 가지고 있음
  • 위 기능은 모두 IDE로 생성할 수 있기 때문에 코드를 생략
package lang.immutable.address;

public class MemberV1 {
    private String name;
    private Address address;

    // 모든 필드값을 매개변수로하는 생성자
    // 게터, 세터
    // toString 오버라이딩 메서드
}

 

(2) MemberMainV1

  • 회원A와 회원B는 회원A, 회원B는 같은 Address인스턴스를 참조있으며 둘다 주소가 서울임
  • 회원B의 주소를 변경하면 같은 Address 인스턴스를 참조하고 있기 때문에 회원A의 주소도 변경되어 버리는 사이드 이펙트가 발생함
package lang.immutable.address;

public class MemberMainV1 {
    public static void main(String[] args) {
        Address address = new Address("서울");

        MemberV1 memberA = new MemberV1("회원A", address);
        MemberV1 memberB = new MemberV1("회원B", address);

        // 회원A, 회원B의 처음 주소는 모두 서울
        System.out.println("memberA = " + memberA);
        System.out.println("memberB = " + memberB);
        
        // 회원B의 주소를 부산으로 변경해야 함
        memberB.getAddress().setValue("부산");
        System.out.println("memberB.address -> 부산");
        System.out.println("memberA = " + memberA);
        System.out.println("memberB = " + memberB);

    }
}
/* 실행 결과
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='서울'}}
memberB.address -> 부산
memberA = MemberV1{name='회원A', address=Address{value='부산'}}
memberB = MemberV1{name='회원B', address=Address{value='부산'}}
*/

 

(3) MemberV2 - 불변 클래스 사용

  • Address를 불변 객체로 만들었던 ImmutableAddress로 변경하고 나머지 코드들도 IDE를 통해 생성
package lang.immutable.address;

public class MemberV2 {
    private String name;
    private ImmutableAddress address;

    // IDE를 활용하여 생성자, 게터, 세터, toString 생성
}

 

(4) MemberMainV2

  • 회원B의 주소를 중간에 부산으로 변경하려고 할 때 address는 불변 객체이므로 변경할 수 없어서 new ImmutableAddress("부산")으로 새로 생성해야함
  • memberB는 불변객체가 아니므로 memberB의 address필드에 새로 생성한 ImmutableAddress의 참조값을 전달할 수 있으며 결과는 memberB의 주소만 변경되 사이드 이펙트가 발생하지 않았음
package lang.immutable.address;

public class MemberMainV2 {
    public static void main(String[] args) {
        ImmutableAddress address = new ImmutableAddress("서울");

        MemberV2 memberA = new MemberV2("회원A", address);
        MemberV2 memberB = new MemberV2("회원B", address);

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

        // 회원B의 주소를 부산으로 변경해야 함
        memberB.setAddress(new ImmutableAddress("부산"));
        System.out.println("memberB.address -> 부산");
        System.out.println("memberA = " + memberA);
        System.out.println("memberB = " + memberB);

    }
}
/* 실행 결과
memberA = MemberV2{name='회원A', address=Address{value='서울'}}
memberB = MemberV2{name='회원B', address=Address{value='서울'}}
memberB.address -> 부산
memberA = MemberV2{name='회원A', address=Address{value='서울'}}
memberB = MemberV2{name='회원B', address=Address{value='부산'}}

*/

3) 값 변경

(1) MutableOnj - 가변 객체

  • add(int addValue)메서드는 기존의 value 값에 인수로 넘어온 값을 더하는 기능을 함
  • 생성자와 게터, 세터도 가지고 있음
package lang.immutable.change;

public class MutableObj {

    private int value;

    // 생성자

    public void add(int addValue) {
        value = value + addValue;
    }

    // 게터, 세터
}

 

(2) MutableMain

  • MutableObj를 10이라는 값으로 생성하고 obj.add(20)을 호출하여 기존 값에 20을 더하도록하면 obj의 값은 30으로 변경됨
package lang.immutable.change;

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
*/

 

(3) ImmutableObj

  • 필드가 final이기 때문에 세터는 만들 수 없지만 요구사항에 따라 기존 값에 새로운 값을 더해야 함
  • add()메서드의 반환타입을 불변객체인 본인으로 설정하고, 해당 메서드 안에서 기존 필드의 값과 새로 넘어온 값을 더해서 새로운 불변 객체를 생성하여 반환하도록 하면 불변도 유지하면서 새로운 결과도 만들 수 있음
package lang.immutable.change;

public class ImmutableObj {

    private final int value;

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

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

    public int getValue() {
        return value;
    }
    
}

 

(4) ImmutableMain

  • 값을 10으로 ImmutableObj인스턴스를 생성하고 obj1에 참조값을 저장 후 obj1.add(20)을 호출하여 값 변경을 시도하면 obj1의 값은 10으로 유지가되고 obj1.add(20)메서드의 결과인 obj2의 값만 30으로 출력되는 것을 확인할 수 있음
  • 이렇게 불변 객체를 설계할 때 기존 값을 변경해야 하는 메서드가 필요할 수 있는데 이럴때는 기존 객체의 값을 그대로 두고 대신에 변경된 결과를 새로운 객체에 담아서 반환하면됨
package lang.immutable.change;

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

        // add() 호출 이후에도 기존값과 신규값 모두 확인이 가능함
        System.out.println("obj1 = " + obj1.getValue());
        System.out.println("obj2 = " + obj2.getValue());
    }
}
/* 실행 결과
obj1 = 10
obj2 = 30
*/

 

(5) 실행 순서를 메모리 구조로 확인

  • 1. add(20)을 호출
  • 2. 기존 객체에 있는 10과 인수로 입력한 20을 더하고 메서드 동작에 의해 계산 결과를 기반으로 새로운 객체를 만들어서 반환함
  • 새로운 객체는 새로운 참조값을 가지게 되고 이 참조값을 obj2에 대입함

(6) ImmutableMain2 - 주의점

  • 불변 객체를 생성하고 값을 변경하는 메서드를 호출만하고 반환값을 받지 않지 않고 실행해보면 아무것도 처리되지 않은 것처럼 보일 수 있음
  • 불변 객체에서 변경과 관련된 메서드들은 보통 객체를 새로 만들어서 반환하기 때문에 꼭 반환값을 받아야 함
package lang.immutable.change;

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

        // add() 호출 이후에도 기존값과 신규값 모두 확인이 가능함
        System.out.println("obj1 = " + obj1.getValue());
    }
}
/* 실행 결과 
obj1 = 10
*/

 

(7) 정리

  • 주석으로 해당 클래스가 불변임을 표시해주기도 하지만 주석이 없더라도 해당 클래스로 들어가서 필드에 final 연산자가 붙어있으면 불변객체라고 판단할 수 있음
  • 불변 객체에서도 값을 변경할 수 있는 메서드가 있을 수 있는데, 불변객체의 메서드의 동작은 반환값으로 생성된 객체를 넘겨준다고 생각하면 됨

문제와 풀이

1) 사이드 이펙트 해결

(1) 문제 설명

  • MyDate클래스는 불변이 아니므로 공유 참조시 사이드 이펙트가 발생함
  • 이를 불변 클래스로 만들어서 문제를 해결하고 불변 클래스의 이름은 ImmutableMyDate로 작성
  • 새로운 실행 클래스는 ImmutableMyDateMain으로 작성
더보기

MyDate

package lang.immutable.test;

public class MyDate {
    private int year;
    private int month;
    private int day;

    public MyDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public void setMonth(int month) {
        this.month = month;
    }

    public void setDay(int day) {
        this.day = day;
    }

    @Override
    public String toString() {
        return year + "-" + month + "-" + day;
    }
}

 

MyDateMain

package lang.immutable.test;

public class MyDateMain {

    public static void main(String[] args) {
        MyDate date1 = new MyDate(2024,1,1);
        MyDate date2 = date1;
        System.out.println("date1 = " + date1);
        System.out.println("date2 = " + date2);

        System.out.println("2025 -> date1");
        date1.setYear(2025);
        System.out.println("date1 = " + date1);
        System.out.println("date2 = " + date2);
    }
}

 

실행 결과

date1 = 2024-1-1
date2 = 2024-1-1
2025 -> date1
date1 = 2025-1-1
date2 = 2025-1-1

(2) 정답

더보기

ImmutableMyDate

  • 필드를 모두 final로 변경하고 세터를 제거
  • 필드를 변경할 수 있는 메서드를 생성
package lang.immutable.test;

public class ImmutableMyDate {
    private final int year;
    private final int month;
    private final int day;

    public ImmutableMyDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

    public ImmutableMyDate withYear(int newYear) {
        return new ImmutableMyDate(newYear, month, day);
    }

    public ImmutableMyDate withMonth(int newMonth) {
        return new ImmutableMyDate(year, newMonth, day);
    }

    public ImmutableMyDate withDay(int newDay) {
        return new ImmutableMyDate(year, month, newDay);
    }

    @Override
    public String toString() {
        return year + "-" + month + "-" + day;
    }
}

 

ImmutableMyDateMain

package lang.immutable.test;

public class ImmutableMyDateMain {

    public static void main(String[] args) {
        ImmutableMyDate date1 = new ImmutableMyDate(2024,1,1);
        ImmutableMyDate date2 = date1;
        System.out.println("date1 = " + date1);
        System.out.println("date2 = " + date2);

        System.out.println("2025 -> date1");
        date1 = date1.withYear(2025);
        System.out.println("date1 = " + date1);
        System.out.println("date2 = " + date2);
    }
}

** 참고

  • 불변 객체에서 값을 변경하는 경우 withYear() 처럼 "with"로 시작하는 경우가 많음
  • 불변 객체의 메서드가 "with"로 이름이 지어진 경우 그 메서드가 지정된 수정사항을 포함하는 객체의 새 인스턴스를 반환한다는 사실을 뜻함
  • "with"는 관례처럼 사용되는데 원본 객체의 상태가 그대로 유지됨을 강조하면서 변경사항을 새 복사본에 포함하는 과정을 간결하게 표현한다고 보면됨

2) 정리

(1) 불변 객체를 자세히 설명한 이유

  • 자바에서 가장 많이 사용되는 String 클래스 뿐만 아니라 Integer, LocalDate등 수많은 클래스가 불변으로 설계되어있기 때문에 불변 객체가 필요한 이유와 원리를 제대로 이해해야 이런 기본 클래스들을 제대로 이해할 수 있기 때문임

(2) 모든 클래스를 불변으로 만드는 것은 아님

  • 사실 대부분의 클래스는 값을 변경할 수 있는 가변 클래스가 더 일반적임
  • 불변 클래스는 값을 정말 변경하면 안되는 특별한 경우에 만들어서 사용한다고 생각하면 됨
  • 때로는 같은 기능을 하는 클래스를 하나는 불변으로 하나는 가변으로 각각 만드는 경우도 있음
  • 클래스를 불변으로 설계하는 이유는 캐시 안정성, 멀티 쓰레드 안정성, 엔티티의 값 타입 등등 더 많음

(3) 불변 객체 이해

  • 지금은 위의 내용을 모두 이해할 순 없으므로 학습을 계속 진행하다 보면 자연스럽게 이번에 배운 불변 객체에 대한 내용을 상기 시켜보면 더욱 잘 이해하게 될 것임
  • 프로그래밍을 더 깊이있게 학습할 수록 다양한 불변 클래스 이용 사례를 만나게 되며 이해하게 될 것임
  • 지금은 불변 클래스가 어디에 사용되고 어떻게 활용 되는지보다는 불변 클래스의 원리를 이해하는 정도면 충분함