불변 가능성을 최소화하는 방법
- 불변클래스
- 불변 클래스가 아닐 경우
불변 클래스 정의
- 그 인스턴스의 내부 값을 수정할 수 없는 클래스
- 불변 인스턴스에 간직된 정보는 객체가 파괴되는 순간까지 절대 달라지지 않는다
// 불변클래스 예시. 필드에 접근할 수 있는 어떠한 방법도 없다.
public class ImmutableClass {
private String fieldA;
private String fieldB;
public ImmutableClass(String fieldA, String fieldB) {
this.fieldA = fieldA;
this.fieldB = fieldB;
}
}
불변 클래스로 만드는 5가지 규칙

1. 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다
2. 클래스를 확장할 수 없도록 한다. 상속 막기
클래스를 final로 선언하면 상속이 불가능하다.

3. 모든 필드를 final로 선언한다
시스템이 강제하는 수단을 이용해 설계자의 의도를 명확히 드러내는 방법.
4. 모든 필드를 Private으로 선언한다
클라이언트에서 직접 접근해 수정하는 일을 막아준다.
5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다
위 사진에서 mutableObject 참조 필드에 접근할 수 있는 건 ImmutableClass 내부에서만 가능함. 그리고 주의 사항은

위 사진의 첫번째 메서드 처럼 클라이언트가 가변 객체의 참조를 얻게해서는 안된다.
두번째 메서드처럼 가변 필드가 클라이언트가 제공한 객체 참조를 가리키게 해서도 안된다.
불변 객체의 장점
1. 불변 객체는 생성된 시점의 상태를 파괴될 때까지 그대로 간직한다.
불변 객체는 외부 클라이언트에게 내부 상태를 변경할 어떤 방법도 제공하지 않기 때문에 파괴될 때까지 동일한 상태 유지가 가능하다.
2. 불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요가 없다.
여러 스레드가 동시에 사용해도 절대 훼손되지 않는다. 사실 클래스를 스레드 안전하게 만드는 가장 쉬운 방법이기도 하다.
이는 1번 맥락과 같이 보면 되는데 불변 객체는 자신의 상태를 변경할 방법을 제공하지 않기 때문에 여러 스레드에서 하나의 인스턴스를 참조해도 내부 상태값을 동시에 변경할 일이 없기 때문에 안전하다는 의미이다.
그리고 한번 만든 인스턴스를 최대한 재활용하기를 권한다고 한다.

Boolean 클래스를 예로 들면 TRUE 상수를 사용하는 클라이언트들은 미리 생성해둔 Boolean 하나의 인스턴스를 같이 사용하게 된다.
이 불변 Boolean 클래스는 내부 상태를 변경할 방법이 없기 때문에 재사용을 해도 문제가 없기 때문이다.
3. 불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
public class BigInteger extends Number implements Comparable<BigInteger> {
final int signum;
final int[] mag;
// 생략
/**
* Returns a BigInteger whose value is {@code (-this)}.
*
* @return {@code -this}
*/
public BigInteger negate() {
return new BigInteger(this.mag, -this.signum);
}
/**
* This internal constructor differs from its public cousin
* with the arguments reversed in two ways: it assumes that its
* arguments are correct, and it doesn't copy the magnitude array.
*/
BigInteger(int[] magnitude, int signum) {
this.signum = (magnitude.length == 0 ? 0 : signum);
this.mag = magnitude; // -> 원본 인스턴스가 가리키는 내부 배열을 그대로 가리킨다
if (mag.length >= MAX_MAG_LENGTH) {
checkRange();
}
}
Biginteger 를 예로 들면 negate 메서드는 새로운 BigInteger 객체를 반환한다. 이때 불변객체끼리 내부 데이터를 공유하고 있는데 BigInteger 클래스 negate 메서드 주석에 명시된 대로
it doesn’t copy the magnitude array
원본 인스턴스가 가리키는 내부 배열을 그대로 가리키고 있다.
4. 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.
값이 바뀌지 않는 구성요소들로 이뤄진 객체라면 그 구조가 아무리 복잡하더라도 불변식을 유지하기 훨씬 수월하다.
public final class ImmutableClass {
private String fieldA;
private final String fieldB;
private final MutableObject mutableObject;
public ImmutableClass(String fieldA, String fieldB, MutableObject mutableObject) {
this.fieldA = fieldA;
this.fieldB = fieldB;
this.mutableObject = mutableObject;
}
// public void setFieldA(String value) {
// this.fieldA = value;
// }
@Override
public String toString() {
return "ImmutableClass{" +
"fieldA='" + fieldA + '\'' +
", fieldB='" + fieldB + '\'' +
", mutableObject=" + mutableObject +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ImmutableClass that = (ImmutableClass) o;
return Objects.equals(fieldA, that.fieldA) && Objects.equals(fieldB, that.fieldB) && Objects.equals(mutableObject, that.mutableObject);
}
@Override
public int hashCode() {
return Objects.hash(fieldA, fieldB, mutableObject);
}
}
public static void main(String[] args) {
HashMap<ImmutableClass, String> map = new HashMap<>();
ImmutableClass immutable_A = new ImmutableClass("A", "B", new MutableObject());
map.put(immutable_A, "immutableA");
map.put(immutable_A, "immutableA again");
System.out.println("map = " + map);
}
map = {
ImmutableClass{
fieldA='A',
fieldB='B',
mutableObject=effectivejava.item17.MutableObject@35fb3008}
=immutableA again
}
불변 객체인 ImmutableClass를 HashMap 객체에 put 을 두 번하면 같은 Key값임을 인지하고 두번째 value가 첫 번째 value를 덮어 씌웠다.
ImmutableClass에 setter를 추가하여 가변 객체로 만들고 내부 상태를 변경한 후에 put을 하면 어떻게 될까?
public static void main(String[] args) {
HashMap<ImmutableClass, String> map = new HashMap<>();
ImmutableClass immutable_A = new ImmutableClass("A", "B", new MutableObject());
map.put(immutable_A, "immutableA");
immutable_A.setFieldA("change!!!! ");
map.put(immutable_A, "immutableA again");
System.out.println("map = " + map);
}
map = {
ImmutableClass{
fieldA='change!!!! ',
fieldB='B',
mutableObject=effectivejava.item17.MutableObject@1e643faf}
=immutableA again,
ImmutableClass{
fieldA='change!!!! ',
fieldB='B',
mutableObject=effectivejava.item17.MutableObject@1e643faf}
=immutableA
}
HashMap 객체가 key값에 동일한 인스턴스 참조값을 두개 들고있다.
이처럼 맵의 원소로 불변 객체를 사용하면 HashMap 입장에서는 불변식이 허물어질 걱정을 하지 않아도 된다.
5. 불변 객체는 그 자체로 실패 원자성을 제공한다.
불변 객체의 단점
- 값이 다르면 반드시 독립된 객체로 만들어야 한다.
불변 클래스를 만드는 또 다른 설계 방법
- final 클래스보다 유연항 방법 -> 모든 생성자를 private 또는 package-private 으로 만들고 public 정적 팩터리를 제공하라
정리
- 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
- 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.
- 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
'Programming > Java' 카테고리의 다른 글
[F-Lab 모각코 챌린지 16일차] Java : 블랙잭 게임 구현 (0) | 2023.06.18 |
---|---|
[F-Lab 모각코 챌린지 15일차] Java : 예외 처리 (0) | 2023.06.17 |
[F-Lab 모각코 챌린지 13일차] Java : 인터페이스와 추상클래스 (0) | 2023.06.15 |
[F-Lab 모각코 챌린지 11일차] Java : 자바의신 10장-2 (0) | 2023.06.13 |
[F-Lab 모각코 챌린지 10일차] TIL : 자바의신 10장 (0) | 2023.06.12 |
댓글