JAVA 불변객체
우아한 테크코스를 진행하면서 불변객체에 대해 많이 접하였습니다.
불변객체에 대한 개념을 공부하기위해 글을작성하였습니다.
불변객체란?
불변 객체는 생성 후 그 상태를 생성부터 파괴 시점까지 변경할 수 없는 객체를 의미한다.
불변 객체는 생성 후 외부에 의해 내부 상태를 변경할 수 없다.
불변객체의 예
불변객체의 좋은 예로는 String
클래스가 있다.
String
객체의 값 은 생성후 변경할 수 없다.
문자열을 결합하거나 문자열을 수정할때 새로운 String
객체를 생성하여 참조하도록하기 때문이다.
즉 String
클래스에서 말하는 불변성은 객체의 내부 상태(문자열의 내용)가 변경할 수 없다는 의미이다.
String
은 다른 객체를 참조하도록 변경될 수 있지만, 한 번 생성된 String
객체의 내용은 변경할 수 없다.
💬 다음 소스코드를 예로 들어보자.
String str = "hello world"
str = "hello java"
str = "hello povi"
-
heap
영역에 참조변수str
이 참조하고있는"hello world"
라는 문자열 객체가생성된다. -
"hello world"
객체가"hello java"
로변하는 것이아니라"hello java"
라는 새로운 객체를 만들고str
은 객체"hello java"
를 참조하게된다. -
“hello povi”
객체가 만들어지며str
은“hello povi”
객체를 참조하게된다. -
“hello world”
,“hello java”
라는 객체는 아무것도 참조하고 있지 않은 객체가 되어GC
의 대상이된다.
불변객체를 만드는 규칙
- 객체 상태를 변경하는 메서드를 제공하지 않는다.
- 클래스를 확대할 수 없도록 한다.
- 모든 필드를
private
로 설정한다. - 모든 필드를
final
로 설정한다. - 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
원시타입에서의 불변
원시 타입은 참조값이 존재하지 않기 때문에 setter
를 생성하지 않는 것만으로도 객체의 불변성을 보장한다.
💬 다음 소스코드를 예로 들어보자.
public class ImmutablePrimitiveObject {
private final int intValue;
public ImmutablePrimitiveObject(int intValue) {
this.intValue = intValue;
}
public int getIntValue() {
return intValue;
}
}
public class Main {
public static void main(String[] args) {
ImmutablePrimitiveObject obj = new ImmutablePrimitiveObject(42);
int intValue = obj.getIntValue();
// 값을 변경하는 메서드가 없으므로 외부에서는 수정 불가능
// obj.setIntValue(10); // 컴파일 오류
}
}
참조타입에서의 불변
참조타입에서의 불변은 원시타입보다 까다롭다. 원시 타입은 값 자체가 복사되기 때문에 값이 변경되지 않지만, 참조 타입은 객체에 대한 참조가 복사되기 때문에 참조된 객체의 상태가 변경될 수 있기때문이다.
💬 다음 소스코드를 예로 들어보자.
public class Member {
private final String name;
public Member(String name){
this.name = name;
}
}
public class VIPMembers {
private final List<Member> members;
public VIPMembers(List<Member> members){
this.members = members;
}
}
`VIPMember` 클래스는 내부 객체의 상태를 변경하는 메서드를 제공하지 않으며,
인스턴스 변수를 `private`, `final`로 선언하여 불변객체를 기대한다.
public class Main {
public static void main(String[] args) {
List<Member> memberList = new ArrayList<>(Arrays.asList(new Member("김"), new Member("강")));\
VIPMembers vipMembers = new VIPMembers(memberList);
System.out.println(vipMembers.getMemberList());
memberList.add(new Member("최"));
System.out.println(vipMembers.getMemberList());
}
}
실행결과를 예상해보자.
과연 "김", "강" 다음에 `vipMembers` 리스트에 "최"가 추가가 되었을까?
새로운 멤버변수가 추가되었다. 가변 객체인 memberList
를 수정하면 VIPMember
내부 인스턴스 변수 상태값도 변경된다.
💬 `memberList` 의 참조값과 `VIPMember` 가 참조하는 주소값이 같이 때문이다.
즉, 외부에서 내부의 값을 수정할 수 있어 불변객체 조건에 성립되지 못하였다.
방어적 복사
방어적 복사란 생성자의 인자로 받은 객체의 복사본을 만들어 내부필드를 초기화하거나,
getter
메서드에서 내부의 객체를 반환할 때, 객체의 복사본을 만들어 반환하는 것이다.
방어적 복사를 사용할 경우, 외부에서 객체를 변경해도 내부의 객체는 변경되지 않는다.
public class Member {
private final String name;
public Member(String name){
this.name = name;
}
}
public class VIPMembers {
private final List<Member> members;
public VIPMembers(List<Member> members){
this.members = new ArrayList<>(members);
}
}
생성자를 통해 값을 전달받을때 new ArrayList<>(members)
를 통하여 새로운 값을 참조하도록 복사하였다.
이렇게 되면 외부에서 넘겨주는 members
와 클래스 내부에서 사용하는 members
가 서로다른 주소의 값을 참조하기때문에, 외부에서 제어가 불가능한다.
이제 정말 불변객체가 되었을까?
TODO … getter
사용하여 값을 제어할때.
댓글남기기