불변 객체는 상태 변경이 불가능한 객체이다.
수정자 메서드를 막고 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로 선언하여 정적 팩토리 메서드를 통해 프로세스 전역에서 상수와 같이 사용할 수 있다.
불변 객체는 상태 변경이 불가하기에 공유객체로 사용할 수 있다.
thread-safe하다는 뜻이다.
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) 사용시 예외를 발생시켜 참조객체를 변경하지 못하도록 방어적 복사를 제공하는 함수가 존재한다. | 직렬화 역직렬화 |
[참고] 직렬화와 역직렬화에 깊은 복사 사용하는 이유
직렬화와 역직렬화 할 때, 객체의 내부구조나 데이터 형식이 변경될 수 있다.
이때 원본객체를 직렬화에 그대로 사용하는 경우 데이터 구조가 변경되어 이후의 로직에서는 사용할 수 없을 수 있다.
따라서 객체의 일관성을 유지하기 위해 깊은 복사를 통해 모든 값을 복사하여 원본객체와 완전히 참조를 끊고 직렬화, 역직렬화를 수행한다.
val objectMapper = ObjectMapper()
val person = Person("Person", 18)
val personStr = objectMapper.writeValueAsString(person)
println(person)
println(personStr)
출력결과 :
Person(name=Person, age=18)
{"name":"Person","age":18}
위와 같이 직렬화 이후에도 원본은 전혀 영향을 받지 않음을 알 수 있다.
'Language > 자바&코틀린' 카테고리의 다른 글
자바 예외의 종류와 처리방식 (0) | 2024.02.19 |
---|---|
컴파일 타임 의존성과 런타임 의존성 (0) | 2024.02.11 |
인터페이스와 추상클래스의 차이 (0) | 2023.11.15 |
객체지향 설계의 SOLID 원칙 (0) | 2023.11.15 |
java.lang.OutOfMemoryError: Java heap space 오류 해결 (0) | 2021.01.14 |