일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch12
- 스프링 고급 - 스프링 aop
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch4
- jpa 활용2 - api 개발 고급
- 스프링 mvc2 - 타임리프
- 자바 기본편 - 다형성
- 스프링 mvc1 - 스프링 mvc
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch2
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch13
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch11
- 2024 정보처리기사 수제비 실기
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch6
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch14
- 코드로 시작하는 자바 첫걸음
- 스프링 입문(무료)
- 자바의 정석 기초편 ch1
- 게시글 목록 api
- 스프링 mvc1 - 서블릿
- @Aspect
- Today
- Total
나구리의 개발공부기록
불변 객체, 기본형과 참조형의 공유, 공유 참조와 사이드 이펙트, 불변 객체(도입, 예제, 값 변경) 본문
불변 객체, 기본형과 참조형의 공유, 공유 참조와 사이드 이펙트, 불변 객체(도입, 예제, 값 변경)
소소한나구리 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) 불변 객체 이해
- 지금은 위의 내용을 모두 이해할 순 없으므로 학습을 계속 진행하다 보면 자연스럽게 이번에 배운 불변 객체에 대한 내용을 상기 시켜보면 더욱 잘 이해하게 될 것임
- 프로그래밍을 더 깊이있게 학습할 수록 다양한 불변 클래스 이용 사례를 만나게 되며 이해하게 될 것임
- 지금은 불변 클래스가 어디에 사용되고 어떻게 활용 되는지보다는 불변 클래스의 원리를 이해하는 정도면 충분함