item 17 : 변경 가능성을 최소화하라
1. 불편 클래스
불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.
1) 불변 클래스란?
인스턴스의 내부 값을 수정할 수 없는 클래스다. 불변 인스턴스에 간직된 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다.
2. 클래스를 불변으로 만들기 위해 지켜야 하는 5가지 규칙
1) 객체의 상태를 변경하는 메서드(변경자) setter를 제공하지 않는다.
하지만 불변 객체의 중요성이 대두되며 setter 사용을 지양하는 흐름으로 변화되었다.
Date 클래스는 다음과 같은 많은 변경 메서드들이 있었는데...
하지만 변경할 수 없고, 멀티 스레드에서도 안전한 날짜 & 시간 관련 클래스가 필요했다.
그래서 등장한 LocalDate
클래스는 setter 를 제공하지 않는 불변 클래스 임
🧐 만약 객체의 상태를 변경하는 메서드를 제공한다면?
바깥에서 객체의 상태를 변경할 수 있음
클래스를 불변으로 만들 수 없음
2) 클래스를 확장할 수 없도록 한다.
하위 클래스에서 부주의하게 혹은 나쁜 의도로 객체의 상태를 변하게 만드는 사태를 막아준다. 상속을 막는 대표적 인 방법은 클래스를 final
로 선언하는 것이다.
상속의 방법을 막는 방법 2가지
클래스를 final로 설정하는 것
클래스를 상속받을 수 있다면, 해당 클래스는 값이 변경될 수 있음을 의미하는데, 이게 어떻게 가능할까를 final로 하지 않았을 때를 보면!
setter를 제거함으로써 객체의 상태를 변경하는 메서드 제공 하지 않도록 수정
그렇다면 값을 바꾸려면? private로 내부 값 바꿀 수는 없으나 바뀐 것처럼 사용 가능
Dog
클래스 (Animal을 상속):
Main
클래스:
실행 결과:
이 코드가 보여주는 점:
상속을 통한 동작 변경:
Dog
클래스는Animal
클래스를 상속받아type()
메서드를 오버라이딩하고 있다. 이를 통해 부모 클래스의 동작을 자식 클래스에서 변경할 수 있음을 보여준다.필드 숨김(Field Hiding):
Dog
클래스에서type
필드를 다시 선언함으로써, 부모 클래스의type
필드를 가리고 있다. 이는 좋은 설계가 아니며, 혼란을 초래할 수 있다.불변성 위반 가능성:
Animal
클래스는 자신의 필드를private
으로 선언하고 있지만, 상속을 통해 자식 클래스에서 동작이 변경될 수 있다. 따라서 불변성을 보장하기 위해서는 클래스의 상속을 막거나(final 클래스), 필드를final
로 선언하는 등의 조치가 필요하다.
불변성을 유지하는 방법:
클래스를
final
로 선언하여 상속을 금지
필드를
private final
로 선언하여 필드가 한 번 초기화된 후 변경되지 않도록 한다.
메서드를
final
로 선언하여 오버라이딩을 방지한다.
수정된 Animal
클래스 (불변성을 유지하도록):
Animal
클래스 (불변성을 유지하도록):이렇게 수정하면 Animal
클래스를 상속할 수 없으며, 필드도 변경할 수 없게 되어 불변성이 유지된다.
여기서 의문 : 이렇게하면클라이언트가 결국 수정도 못해서 불변 클래스는 오히려 안좋은게 아닌가?
답변 : 불변 클래스를 사용하면 클라이언트가 객체의 상태를 직접 수정할 수 없기 때문에 불편하다고 느낄 수 있습니다. 하지만 불변 클래스는 여러 가지 장점을 제공하며, 이러한 이점을 통해 코드의 안정성과 신뢰성을 높일 수 있습니다.
클라이언트가 값을 변경하고 싶을 때
불변 클래스에서는 객체의 상태를 직접 변경할 수 없지만, 변경된 값을 가진 새로운 객체를 생성하여 반환하는 방법을 사용할 수 있습니다. 이는 함수형 프로그래밍에서 흔히 사용하는 패턴으로, 객체의 불변성을 유지하면서 필요한 변경을 가능하게 합니다.
예시: Animal
클래스에 변경 메서드 추가
사용 방법
위 예시에서 animal
객체는 여전히 "Cat"
타입을 유지하며, newAnimal
은 "Dog"
타입을 갖는 새로운 객체입니다. 이처럼 불변 객체를 사용하면 원본 객체의 상태를 변경하지 않으면서도 원하는 값을 가진 객체를 생성할 수 있습니다.
불변 클래스는 다음과 같은 이유로 유용합니다:
안전하고 신뢰할 수 있는 코드 작성: 상태 변경에 따른 예기치 않은 버그를 줄일 수 있습니다.
멀티스레드 환경에서의 안전성 확보: 동기화 없이도 스레드 안전성을 보장합니다.
유지보수 용이성: 객체의 상태가 변하지 않으므로 코드의 복잡성이 감소합니다.
결국 여러개의 객체를 생성하는 건 메모리를 많이 차지하는 것과도 연관이 있는거 아닌가?
답변 : 맞습니다. 불변 객체를 사용할 때 변경할 때마다 새로운 객체를 생성하므로, 메모리 사용량이 증가할 수 있다는 우려가 있습니다. 이는 불변 클래스의 단점 중 하나로 언급되기도 합니다. 하지만 실제로는 이러한 메모리 사용 증가가 심각한 문제를 일으키는 경우는 드뭅니다.
불변 객체 사용으로 인한 메모리 사용 증가는 존재하지만, 현대 JVM의 최적화와 프로그래밍 기법을 통해 그 영향을 최소화할 수 있습니다.
불변 클래스의 장점인 안정성, 스레드 안전성, 유지보수성 향상은 메모리 사용량 증가로 인한 단점을 상쇄하고도 남습니다.
실제 개발에서는 불변 객체를 우선적으로 사용하고, 메모리 사용이 문제가 되는 부분에 한해 최적화 기법을 적용하는 것이 좋습니다.
public 정적 팩터리 메서드를 제공하는 방법
모든 생성자를 private 혹은 package-private(default)로 만들 후, public 정적 팩터리 메서드를 제공하는 방법
package 외부에서는 사실상 final 클래스와 동일하게 동작할 뿐더러, 내부에서는 해당 클래스를 상속하여 여러 클래스를 생성할 수 있기 때문에 훨씬 유연한 방법
사용법
🌱 인스턴스 캐싱을 통한 메모리 효율 개선
정적 팩토리 메서드를 사용하면 캐싱을 통해 동일한 값을 가진 객체의 중복 생성을 방지할 수 있다.
설명
CACHE
맵:ConcurrentHashMap
을 사용하여 스레드 안전하게 캐싱을 구현한다.키는
type
문자열이고, 값은 해당Animal
객체이다.
computeIfAbsent
메서드:CACHE
에 해당type
의 객체가 없을 경우에만 새로운 객체를 생성하고, 이미 존재하면 그 객체를 반환한다.
메모리 사용량 감소:
동일한
type
을 가진 객체를 재사용하므로 불필요한 객체 생성을 줄여 메모리 효율을 높일 수 있다.
캐싱된 객체 사용 예시
animal1
과animal2
는 동일한 인스턴스를 참조하므로==
연산 결과가true
이다.이는 메모리 사용량을 줄이고 객체 비교 시 효율성을 높인다.
그래서 LocalDate 클래스는 final 클래스로 선언하여 상속을 방지하고 있다.
3) 모든 필드를 final로 선언한다. 시스템이 강제하는 수단을 이용해 설계자 의 의도를 명확히 드러내는 방법이다.
자바 언어에서 final을 사용하면 값을 바꿀 수 없기 때문에, 명시적으로 값을 불변
으로 만들 수 있다.
새로 생성된 인스턴스를 동기화 없이 다른 스레드에 전달하더라도 문제없이 동작하게끔 동작하는 데에도 필요하다.
멀티 스레드에서도 안전하다고함
Date 클래스 내부적으로 CalendarDate
라는 클래스가 사용되는데..
CalendarDate 클래스의 경우 모든 필드가 변경 가능한 필드인데 반해, LocalDate
는 모두 final 이 붙어있다.
4) 모든 필드를 private으로 선언한다.
필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아준다. 기술적으로는 기본 타입 필드 나 불변 객체를 참조하는 필드를 public final
로만 선언해도 불변 객체가 되지만, 이렇게 하면 다음 릴리스에서 내부 표현을 바꾸지 못하므로 권하지는 않는다고 함
5) 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야 한다.
이런 필드는 절대 클라이언트가 제공한 객 체 참조를 가리키게 해서는 안 되며, 접근자 메서드가 그 필드를 그대로 반환해서도 안 된다. 생성자, 접근자, readObject 메서드 모두에서 방어적 복사를 수행해야 한다.
Date 클래스는 내부 캘린더 객체의 날짜 필드를 변경하는 메서드를 호출한다.
반면에 LocalDate 의 경우에는 return 문에서 항상 새로운 객체를 생성하여 반환한다.
3. 이 모든 것을 지키는 클래스 예제
1) 모든 필드를 private final
로 선언
필드 접근을
private
로 제한하여 외부에서 직접 접근하지 못하게 하고,final
로 선언해 초기화 후 필드의 값을 변경할 수 없도록 했다.
2) 생성자에서 방어적 복사 수행
생성자에서 외부에서 전달된
Date
객체가 원본 그대로 저장되지 않도록 복사본을 만들어 저장했다. 이렇게 하면 외부에서 전달한Date
객체를 수정하더라도Person
객체의birthDate
필드에 영향을 줄 수 없다.
3) getter
메서드에서 방어적 복사 수행
getBirthDate
메서드에서는 원본birthDate
객체 대신 복사본을 반환한다. 이를 통해 외부 코드에서 반환된Date
객체를 변경하더라도,Person
객체의 필드에는 영향을 주지 않는다.
4) setter 메서드를 제공하지 않음
변경자(setter) 메서드를 제공하지 않아서, 객체가 생성된 후 필드 값을 변경할 수 없도록 했다.
5) 상속 불가 (final
클래스)
이 클래스는
final
로 선언되어 있어, 상속을 통해 확장할 수 없도록 했다. 이를 통해 하위 클래스가 부모 클래스의 불변성을 해치거나 상태를 변경하는 것을 방지했다.
4. 불변 객체의 장단점
1) 불변 객체의 장점
스레드 안전성: 불변 객체는 스레드 간에 안전하게 공유될 수 있으므로 동기화 작업 없이도 안전하게 사용할 수 있다.
안정성: 불변 객체는 상태가 변하지 않으므로 안정적이고 예측 가능한 동작을 합니다. 함수형 프로그래밍에서 자주 사용하는 패턴이다.
캐싱 및 재사용 가능성: 불변 객체는 공유 및 재사용이 가능하다. 예를 들어
ZERO
,ONE
,I
와 같은 상수를 재사용하여 메모리 사용을 최적화할 수 있다.
2) 불변 객체의 단점
값 변경 시 새 객체 생성: 불변 객체는 값을 변경하려면 항상 새로운 객체를 생성해야 한다. 이로 인해 성능 저하나 메모리 사용 증가가 발생할 수 있다.
예를 들어,
Complex
객체에서 덧셈이나 뺄셈을 수행할 때마다 새로운Complex
객체가 생성되며, 원래 객체는 그대로 남는다.큰 객체나 다단계 연산을 수행할 때는 불변 객체의 성능 문제가 두드러질 수 있다. 이런 경우 가변 동반 클래스를 사용하거나 다단계 연산을 위한 최적화를 적용할 수 있다.
5. 불편 클래스의 특징
불변 객체는 단순함
멀티 스레드 환경에서도 동기화할 필요가 없음
원자성을 제공
값이 달라지면, 별도의 객체를 만들어야 한다는 것
정적 팩토리 메서드를 통한 인스턴스 캐싱
1) 불변 객체는 단순함
불변 객체는 생성된 시점의 상태를 파괴될 때까지 그대로 간직한다.
모든 생성자가 클래스 불변식 보장한다면 그 클래스를 사용하는 프로그래머가 다른 노력을 들이지 않더라도 영원히 불변으로 남는다. 반면 가변 객체
는 임의의 복잡한 상태 에 놓일 수 있다. 변경자 메서드가 일으키는 상태 전이를 정밀하게 문서로 남겨놓지 않은 가변 클래스는 믿고 사용하기 어려울 수도 있다.
2) 멀티 스레드 환경에서도 동기화할 필요가 없음
불변 객체는 값이 변경되면 새로운 객체로 반환하기 때문에 멀티 스레드 환경에서도 안전하다.
가변 객체(private int x;)의 경우에는 멀티 스레드 환경에서 getter, setter 에서 이 발생할 수 있다.
반면에 불변객체(private final int x;) 의 경우에는 setter 에서 항상 다른 객체를 반환한다.
그래서 멀티 스레드 환경에서도 안전하게 객체를 다룰 수 있다.
3) 값이 달라지면, 별도의 객체를 만들어야 함
불변객체는 setter 에 의해 상태가 변경되면, 새로운 객체를 생성해서 반환힌다.
새로운 객체 생성의 비용의 단점
값이 변경될 때마다 새로운 객체를 생성해야 하므로 메모리와 성능 비용이 증가할 수 있다.
예를 들어, 큰
BigInteger
에서 비트 하나를 바꾸면 전체를 복사해야 한다.
성능 문제 해결 방안
다단계 연산을 기본 기능으로 제공하여 중간에 불필요한 객체 생성을 줄인다.
가변 동반 클래스를 제공하여 성능을 향상시킬 수 있다.
예: String ←→ StringBuilder, StringBuffer BigInteger ←→ MutableBigInteger
BigInteger 의 내부 연산 과정에서 값이 어려번 바뀌는 경우가 있다.
이런 경우에 MutableBigInteger 를 이용해서 새로운 객체를 생성하지 않고 값을 변경한 다음
최종 결과를 다시 BigInteger 로 변환하여 반환하기도 한다.
4) 그 외 장점
정적 팩토리 메서드를 통한 인스턴스 캐싱:
불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 중복 생성을 방지하고 메모리 사용량과 가비지 컬렉션 비용을 줄일 수 있다.
정적 팩토리 메서드를 사용하면 클라이언트를 수정하지 않고도 캐싱 기능을 추가할 수 있어 성능 향상에 도움이 된다.
예시:
방어적 복사 불필요:
불변 객체는 상태가 변경되지 않으므로 자유롭게 공유할 수 있으며, 방어적 복사가 필요 없다.
따라서
clone
메서드나 복사 생성자를 제공하지 않는 것이 좋다.
내부 데이터의 공유 가능:
불변 객체는 내부적으로 가변 객체를 공유할 수 있다.
예를 들어,
BigInteger
의negate
메서드는 내부 배열을 복사하지 않고 원본과 공유하여 새로운 인스턴스를 생성한다.
복잡한 객체의 불변식 유지 용이:
다른 불변 객체를 구성 요소로 사용하면 복잡한 객체의 불변식을 유지하기 쉽다.
불변 객체는 맵의 키나 집합(Set)의 원소로 사용하기에 적합하다.
실패 원자성 제공:
불변 객체는 상태가 변하지 않으므로 예외 발생 시에도 객체의 유효한 상태가 유지된다.
5) 불변 클래스를 만드는 추가적인 설계 방법
상속을 금지하는 방법:
클래스를
final
로 선언하여 상속을 막을 수 있지만, 더 유연한 방법은 생성자를private
또는 패키지 전용으로 만들고public
정적 팩토리 메서드를 제공하는 것이다.이렇게 하면 외부에서는 클래스를 확장할 수 없으며, 내부적으로는 여러 구현 클래스를 활용할 수 있다.
객체 캐싱과 성능 향상:
정적 팩토리 메서드를 사용하면 나중에 객체 캐싱 기능을 추가하여 성능을 향상시킬 수 있다.
동일한 값을 갖는 불변 객체를 캐싱하여 메모리 사용량을 줄일 수 있다.
필드의
final
여부:성능을 위해 모든 필드를
final
로 선언하지 않고, 외부에 보이는 상태만 불변으로 유지할 수 있다.내부적으로 계산 비용이 큰 값을 지연 초기화하여 캐싱할 때 사용한다.
주의사항
직렬화 시 주의점:
Serializable
을 구현하는 불변 클래스에서 가변 객체를 참조하는 필드가 있다면readObject
나readResolve
메서드를 제공해야 한다.readObject
메서드에서 방어적 복사와 유효성 검사 수행:방어적 복사를 통해 외부에서 전달된 가변 객체의 참조를 끊고, 내부에서만 사용하는 복사본을 만듭니다.
유효성 검사를 다시 수행하여 객체의 일관성을 확인한다.
readResolve
메서드를 사용하여 불변성 유지:역직렬화된 객체를 새로 생성된 객체로 대체하여 불변성을 유지한다.
불변 객체를 사용:
Date
대신 불변 클래스인Instant
또는LocalDateTime
등을 사용한다.
그렇지 않으면 보안 문제가 발생할 수 있다.
보안 문제의 핵심은 역직렬화 과정에서 불변 클래스의 불변성이 깨질 수 있다는 점이다. 이로 인해 예상치 못한 객체 상태 변경이나 악의적인 객체 주입이 가능해져 프로그램의 보안과 안정성이 위협받을 수 있다.
보안 문제가 발생하는 이유:
역직렬화 시 생성자 미호출:
자바의 직렬화 과정에서 객체를 역직렬화할 때 생성자가 호출되지 않습니다.
따라서 생성자에서 수행하던 방어적 복사나 유효성 검사가 무시됩니다.
이로 인해 외부에서 조작된 데이터가 객체의 내부로 주입될 수 있다.
가변 객체의 직접 참조 노출:
불변 클래스 내부에서 가변 객체를 참조하고 있으면, 역직렬화 시 가변 객체에 대한 직접 참조가 외부에 노출될 수 있다.
공격자는 이 가변 객체를 수정하여 불변 클래스의 내부 상태를 변경할 수 있다.
역직렬화 공격(Deserialization Attack):
공격자는 직렬화된 데이터를 조작하여 악의적인 객체나 데이터를 주입할 수 있다.
이를 통해 클래스의 불변성을 깨뜨리고, 프로그램의 동작을 변경하거나 보안 취약점을 노출시킬 수 있다.
정리
PhoneNumber와 Complex 같은 단순한 값 객체는 항상 불변으로 만들자(자바 플랫폼에서도 원 래는 불변이어야 했지만 그렇지 않게 만들어진 객체가 몇 개 있다. java.util. Date와 java.awt.Point가 그렇다)String과 Biginteger처 럼 무거운 값 객체도 불변으로 만들 수 있는지 고심해야 한다.
Getter가 있다고 해서 무조건 Setter를 만들지 말자. 클래스가 꼭 필요한 경우가 아니라면 불변임을 보장하는 것이 설계적 관점에서 바람직하다.
모든 클래스를 불변으로 만들 수 없다. 하지만 가변 클래스더라도 변경할 수 있는 부분은 최소한으로 줄이자. 그렇게 되면 객체를 예측하기 쉬워지고 오류 발생 가능성이 줄어든다.(변경이 없게 만든 후 모두
final
선언을 하고, 특별한 이유가 없다면 모두 private final 선언을 하자.)생성자는 불변식 설정이 모두 완료된 상태의 객체를 생성해야 한다. 즉, 확실한 이유가 없다면 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도 public으로 제공해서는 안된다. 객체를 재사용할 목적으로 상태를 다시 초기화하는 메서드도 올바르지 않다.
성능상의 이유로 불변 클래스를 사용할 수 없다면, 불변 클래스와 쌍을 이루는 가변 동반 클래스를 제공하는 것을 고려해라.
이미지 출처 : https://jwkim96.tistory.com/302
Last updated