불변(Immutable)객체와 가변(mutable) 객체
불변 객체는 상태 변경이 불가능한 객체이다.
수정자 메서드를 막고 read-only 메서드만 제공하며 참조 속성의 불변성을 지키기 위해 방어적 복사 메서드가 제공되기도 한다.
ex) String, 원시타입의 래퍼타입(Boolean, Integer, Float, Long, Double), ResultSet
불변객체 생성
1. 접근지정자 private 지정
2. final 선언
3. 수정자 메서드 제공 금지
외부에서 접근이 불가하도록 private으로 선언하고,
final을 통해 초기화 이후 값 변경이 불가하게 만든다.
수정자 메서드를 제공하지 않음으로써 값이 변경되지 않음을 보장한다.
public class Member {
private final String email;
private final String passwd;
private final MemberPrivate memberPrivate;
public Member(String email, String passwd, MemberPrivate memberPrivate) {
this.email = email;
this.passwd = passwd;
this.memberPrivate = memberPrivate;
}
public String getEmail() {
return email;
}
public String getPasswd() {
return passwd;
}
public MemberPrivate getMemberPrivate() {
return memberPrivate;
}
}
4. 참조타입 속성은 복사본 제공
원시타입의 경우 객체 내부에 값 자체로 갖고 있기에 private final 선언과 수정자 메서드를 막는 것만으로 불변하게 만들 수 있으나, 참조 속성은 메모리 주소를 참조하기에 getter 메서드를 통해 접근하게 되면 참조 객체의 불변성이 깨질 수 있다.
따라서 참조 속성에 접근 하는 경우 원본이 아닌 복사본을 반환하도록 하여 불변성을 지킬 수 있다.
[예제] Member클래스의 참조 타입 속성이 아래와 같은 가변성을 갖는 경우를 생각해보자.
public class MemberPrivate {
private int age;
private Phone phone;
public MemberPrivate(int age, Phone phone) {
this.age = age;
this.phone = phone;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Phone getPhone() {
return phone;
}
public void setPhone(Phone phone) {
this.phone = phone;
}
}
우리는 아래와 같은 MemberPrivate의 수정자 메서드로 memberPrivate 객체의 상태를 변경시킬 수이다.
MemberPrivate memberPrivate = member.getMemberPrivate();
memberPrivate.setAge(20);
Member객체의 참조타입인 속성인 memberPrivate이 불변객체가 아니므로 참조타입의 속성을 변경시킬 수 있다.
이와 같은 상황을 대비하여 사용할 수 있는 방식이 원본객체가 아닌 복사본을 제공하여
원본객체에 대한 접근을 막아 상태를 변경하지 못하도록 하는 것이다.
따라서 아래와 같이 MemberPrivate 클래스에 인스턴스 변수의 참조 주소를 새로운 객체에 생성하여 반환할 수 있다.
public class MemberPrivate {
. . .
public MemberPrivate defensiveCopy() {
return new MemberPrivate(age, phone);
}
. . .
}
public class Member {
. . .
public MemberPrivate getMemberPrivate() {
return memberPrivate.defensiveCopy();
}
}
위와 같은 방식을 방어적 복사라고 한다.
여기서 의문이 들 수 있는 점이 MemberPrivate도 모두 final로 선언하면 되지 않냐라고 의문이 들 수 있는데
그렇게 할 수 있다면 그렇게 하는 방법이 가장 베스트이다.
허나, 이미 개발되어 사용 중인 클래스의 필드를 함부로 수정할 수 없는 상황이라면 위와 같은 방법을 사용할 수 있다.
(불변성을 위해 복사본을 제공하는 경우, 방어적 복사 외에 깊은 복사를 사용 할 수도 있다.)
[참고] 정적 팩토리 메서드를 통한 전역 불변객체 (상수) 사용
정적 팩토리 메서드는 정적 자원에만 접근 가능하다.
불변객체를 static final로 선언하여 정적 팩토리 메서드를 통해 프로세스 전역에서 상수와 같이 사용할 수 있다.
불변 객체는 값 자체를 나타내는 객체이므로 하나의 불변객체가 여기저기에서 사용된다면 공유객체로 상수처럼 사용하는 것이 메모리 자원 측면에서 효율적일 수 있다.
Boolean 클래스의 정적 팩토리 메서드인 valueOf 메서드를 보자.
public final class Boolean implements java.io.Serializable, Comparable<Boolean>, Constable
{
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
}
true와 false에 따라 객체를 계속 생성하는 것이 아닌 static final로 선언되어있고 미리 생성한 Boolean 객체를 반환해주고 있다.
Boolean이 갖는 값은 true와 false 뿐이므로 사용할 때마다 새롭게 생성하여 메모리 자원을 할당하는 것이 아니라
미리 생성한 객체를 반환하여 사용하는 구조이다.
불변객체의 장단점
[불변객체의 장점]
1. thread - safe
불변객체는 값 수정이 불가능하기에 여러 스레드에서 동시에 접근하더라도 동시성 문제가 생길 일이 없다.
* 동시성 문제 : 여러 스레드가 동시에 같은 데이터에 접근하고 변경할 때 발생하는 데이터 불일치 문제
2. 안전한 객체
가변 객체의 경우 상태 변경 중 예외가 발생하면 객체가 불안정한 상태에 빠질 수 있으나,
불변객체는 해당 가능성이 없다.
3. 예측 가능한 객체
객체의 값이 변경되지 않기에 디버깅 및 코드 추적에 용이한 장점이 있다.
[불변객체의 단점]
1. 구현의 복잡성
참조 객체가 존재하는 경우 불변 객체를 구현하는 로직이 복잡해질 수 있다.
2. 메모리 비효율 사용
String 문자열 연산처럼 값이 바뀔 때마다 새로운 객체를 생성하는 방식은 메모리 자원을 비효율적으로 점유할 수 있다.
3. gc 성능저하
값 변경을 위해 객체를 새로 생성해야 하므로 재사용 가능성이 낮다면 많은 gc 작업을 유발 할 수 있다.
얕은복사, 방어적 복사, 깊은복사
얕은복사
원본객체의 주소 값만 복사
Member a = new Member();
Member b = a; // 얕은 복사
방어적 복사
참조속성의 새로운 복사본을 반환하는 방법을 방어적 복사라고 한다.
이때 참조객체의 참조 객체는 값을 그대로 복사하는 것이 아닌 메모리 주소만 복사한다.
즉, 복사본은 원본의 값을 그대로 복사하지만 참조타입은 원본과 동일한 주소 참조하고 있다. -> 통만 갈아끼우는 구조
위와 같은 구조가 방어적 복사의 구조이다.
member객체의 memberPrivate 타입은 원본객체를 참조하고 있지만, getter를 통해 반환되는 객체는 복사본이다.
허나 이 복사본의 참조타입은 다시 원본의 참조 주소를 그대로 들고 있다.
따라서 아래와 같이 값을 변경하더라도 원본객체의 값은 변경되지 않는다.
MemberPrivate memberPrivate = member.getMemberPrivate();
memberPrivate.setPhone(new Phone("홍길동", "01012345678"));
허나, 방어적 복사가 완벽한 불변성을 보장하는 것은 아니다.
아래와 같이 참조속성의 참조속성까지 가져왔을 때, 해당 객체가 불변객체가 아니라면 값 변경을 막을 수 없다.
Phone phone = memberPrivate.getPhone();
phone.setOwner("임꺽정");
위 구조에서 불변성을 유지하려면 참조속성의 참조속성까지 방어적 복사를 구현해야 한다.
즉, 참조타입의 참조타입 depth가 깊어질수록 해당하는 참조타입을 모두 방어적 복사로 구현해야한다.
(기본타입의 경우 값 복사이므로 방어적 복사와 관련 없다.)
깊은복사
객체의 참조타입 내의 속성들까지 모두 복사하여 새로운 객체로 반환하는 방법
ex) 직렬화와 역직렬화에 사용
[방어적 복사와 깊은 복사 비교]
방어적 복사 | 깊은 복사 | |
메모리 효율성 | 참조 속성의 원본객체를 복사하지 않고 그대로 참조하므로 메모리를 더욱 효율적으로 사용 | 참조속성의 원본객체를 복사하여 새로운 객체를 생성하므로 메모리 오버헤드가 더 큼 |
복잡성 | 참조 객체의 불변성 여부를 따져야함 참조 객체가 단순하다면 구현이 쉬움 |
무조건 모든 값 복사 객체 그래프가 깊고 복잡한 경우 구현이 복잡해짐 |
사용 사례 | Collections 클래스의 unmodifiableList나 unmodifiableMap, unmodifiableSet과 같은 메서드는 컬렉션의 수정자 메서드(add, put, set) 사용시 예외를 발생시켜 참조객체를 변경하지 못하도록 방어적 복사를 제공하는 함수가 존재한다. | 직렬화 역직렬화 |
[참고] 직렬화와 역직렬화에 깊은 복사 사용하는 이유
- 데이터의 불변성을 유지하여 일관성 유지
- 직렬화와 역직렬화 할 때, 통신을 하기 위해 객체의 내부구조나 데이터 형식이 변경될 수 있음.
- 깊은 복사를 통해 원본객체의 일관성을 유지하고 직렬화나 역직렬화 과정에서 원본객체의 수정으로 인한 오류 방지