안정성을 더하는 불변객체
*상위 문서: 코드 리뷰: 불완전한 객체에서 나타난 문제들
A. 불변 객체란
불변 객체는 객체 생성 이후 내부 상태가 변하지 않는 객체입니다. 값이 변해야 한다면 새로운 객체 만들어 리턴하고, 참조에 의해 가변될수 있는 필드가 있다면 getter시 방어적 복사(defnesive-copy)를 통해 제공하며, 가변 가능한 객체를 인자(parameter)로 받게 될 때 또한, 방어적 복사로 받아서 사용합니다.
OOP에서 객체들이 일관성을 가지고 협력하기 위해서는 객체의 신뢰성이 중요합니다. 요청을 했을때, 기대했던 대로 일관적으로 동작을 하려면, 객체의 필드는 예상치 못하게 변화하면 안됩니다. 객체의 불완전한 상태와 가변상태 모두 예상치 못한 값을 리턴할 수 있기에 신뢰를 잃게 되고 이러한 객체들의 협력은 예상치 못한 사이드 이팩트들이 많아져서 코드가 부폐하기 쉽습니다.
B. 불변 객체의 장점
1. 스레드 세이프합니다.
여러 스레드에서 사용되어도 값이 변하지 않기에 병렬처리시 정합성이 깨질 염려가 없습니다. 반면, 값이 가변하다면 병렬처리시 동시성에서 오는 정합성 이슈를 처리해 주어야 합니다.
2. 캐시나 컬랙션의 요소로 사용하기 좋습니다.
이는 자바의 Map 공식문서에도 있는 말이지만 컬렉션의 요소로 가변 객체를 사용한다면 변경사항을 잘 관리해 주어야 하지만, 불변객체로 사용하면 이런 수고로움을 덜 수 있습니다.
“Great care must be exercised if mutable objects are used as map keys. The behavior of a map is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is a key in the map.” (가변 객체를 맵의 키들로 사용할때는 매우 주의하여야 한다. 키값으로 쓰이는 객체의 변화가 equals를 통한 비교에 영향을 줄 경우 map은 키를 통해 값을 찾아갈 수 없다. )
출처: Immutability by Alex Potanin et al., Map 인터페이스 문서
3. 단일책임원칙(SRP, Single Responsibility Principal)를 지키며 부수 효과(Side Effect)를 최소화합니다.
객체에게서 신뢰성 있는 값을 기대할 수 있고, 이는 부수효과가 없는 동작을 기대 할 수 있어 객체의 협력간 일관성을 기대할 수 있다. 덕분에 불변객체를 사용시 불변 객체 자신과 또한, 불변객체를 사용하는 객체의 동작과 상태를 캡슐화 시킵니다. 이를 통해, 객체는 재사용가능하고 유지보수 용이하며 디버깅이 쉬워지는 장점을 가지게 됩니다.
4. 가비지 컬렉션의 성능을 향상 시킬 수 있습니다.
불변객체를 사용하면 메모리를 많이 사용하게 될까봐 걱정되지만 새롭게 생성된 객체는 금방 죽는다는 Weak Genearational 가설에 따르면 생명주기가 짧은 객체는 Young Generation에서 Old Generation까지 대부분 살아남지 않고, 시간이 오래 걸리는 STW(Stop-the-world, Major GC)가 아닌 Minor GC로 제거 가능하기에 가비지 컬렉서 입장에서 큰 무리가 아닙니다.
또한, GC가 스캔할때 불변객체의 경우 사용하고 있는 필드의 객체가 사용되고 있음을 의미하기에 하위 객체들을 스캔하지 않아도 되어 GC의 스캔시간도 단축하여 줍니다.
C. 불변 객체 만드는 법
1. 모든 필드를 private, final로 선언합니다.
2. Getter시 필드에 가변가능한 객체가 있다면 방어적 복사를 통해 제공합니다.
3. 가변 가능한 객체를 인자(parameter)로 받을 때는 방어적 복사를 해서 사용합니다.
4. setter 메서드를 외부에 노출하지 않습니다.
5. getter를 오버라이드 못하도록 합니다. (클래스나 메서드에 final)
예시>
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED, force = true)
public final class MemberLocation {
public static MemberLocation UNKNOWN = new MemberLocation(null, null, null);
@Nullable
private final Double latitude;
@Nullable
private final Double longitude;
@Nullable
private final LocalDateTime timestamp;
@Builder(access = AccessLevel.PRIVATE)
private MemberLocation(@Nullable Double latitude, @Nullable Double longitude, @Nullable LocalDateTime timestamp) {
this.latitude = latitude;
this.longitude = longitude;
this.timestamp = LocalDateTime.of(timestamp.toLocalDate(), timestamp.toLocalTime());
}
public MemberLocation updateLocation(Double latitude, Double longitude, LocalDateTime timestamp) {
if (latitude == null || longitude == null || timestamp == null) {
throw new MemberException(NO_VALUE);
}
return new MemberLocation(latitude, longitude, timestamp, this.fcmToken);
}
public MemberLocation addFCMToken(String token) {
if (token == null) {
throw new MemberException(NO_VALUE);
}
return new MemberLocation(this.latitude, this.longitude, this.timestamp, token);
}
public Optional<LocalDateTime> getTimestamp(Member member) {
if (!member.isLocationServiceEnabled()) {
throw new MemberException(LOCATION_SERVICE_NOT_PERMITTED);
}
return Optional.ofNullable(this.timestamp);
}
public Optional<Double> getLongitude(Member member) {
if (!member.isLocationServiceEnabled()) {
throw new MemberException(LOCATION_SERVICE_NOT_PERMITTED);
}
return Optional.ofNullable(this.longitude);
}
public Optional<Double> getLatitude(Member member) {
if (!member.isLocationServiceEnabled()) {
throw new MemberException(LOCATION_SERVICE_NOT_PERMITTED);
}
return Optional.ofNullable(this.latitude);
}
}
D. 불변객체의 단점 및 개선방안.
불변객체의 단점은 아무래도 매번 새로운 객체를 생성해야 하는데서 오는 메모리 오버헤드 입니다. 그러나, 이는 위의 장점 4번(4. 가비지 컬렉션의 성능을 향상 시킬 수 있습니다.)에서 다루었듯이 생각보다 영향이 적을 수 있기에 염려가 된다면 부하테스트를 통해 확인해 보는 것도 좋은 방법입니다.
만약, 불변객체로 인한 오버헤드가 뚜렷하여 이를 해소하고 싶다면, String Builder를 사용하듯이 가변객체를 로컬 메서드에만 사용하여 오버헤드를 줄일 수도 있습니다. for문같이 반복되는 로직이 있다면, 변경의 영향을 제한하기위해 가변 객체를 로컬메서드에서만 사용하면서 리턴은 불변 객체로 하여 결과적으로는 불변객체의 장점과 가변객체의 장점을 동시에 누릴 수 있습니다. (About Immutability in OOP by Ioan Tinca )
출처
망나니개발자님: [Java] 불변 객체(Immutable Object) 및 final을 사용해야 하는 이유
About Immutability in OOP by Ioan Tinca
Inpa Dev님: 가비지-컬렉션GC-동작-원리-알고리즘-총정리
심화