자바의 객체는 기본적으로 Heap 영역에 할당되고 Stack 영역에 참조값을 갖는 참조 타입 변수를 통해 데이터에 접근한다
이러한 객체는 Mutable(가변) 객체와 Immutable(불변) 객체로 나뉜다
❌ Immutable Object (불변 객체)
- 불변 객체란 데이터 변경이 불가능한 객체를 말한다
- 단순히 생각하면 final 키워드처럼 초기화 이후 값을 변경할 수 없다고 생각할 수 있다
- 하지만 불변 객체는 객체의 데이터 수정이 아예 불가능한 것이 아니라 Heap 영역에 저장된 값을 수정할 수 없는 것이다
- 불변 객체의 종류로는 String, Boolean, Integer, Float, Long, Double 등이 있다
- 이는 String 타입을 제외하고는 원시(primitive) 타입의 래퍼 타입이다
- 불변 객체는 멀티 스레드 환경에서도 안전하게 사용할 수 있다는 신뢰성을 보장한다
- 프로그래머가 커스텀 객체를 생성하여 내부 상태가 변경되지 않도록 하면, 그 또한 불변 객체가 된다
💡 Immutable Object의 불변 개념
- new 연산자로 객체를 생성하면 Heap 영역에 객체가 생성되고, 래퍼런스 값을 갖는 변수가 Stack에 생성된다
- 이 때 불변 객체는 이 객체의 값을 Heap 영역에서 바꿀 수 없다는 뜻이다!
- 따라서 불변 객체의 값이 바뀌는 경우에는 수정된 것이 아닌, Heap 영역에 새로운 객체가 생성된 것이다
불변 객체의 예시
Integer i = 1;
i = 3;
위처럼 보기엔 불변 객체의 값이 수정된 것처럼 보인다
그러나 실제로는 객체의 값이 변경된 것이 아니라 새로운 객체를 생성하고 이 객체에 대한 참조값을 변경한 것이다
기존에 1로 할당되었던 객체는 Garbage로 남아있다가 GC(Garbage Collection)에 의해 사라지게 된다
불변 객체의 장점
1. Thread-Safe하여 멀티 스레드 프로그래밍에 유용하며, 동기화를 고려하지 않아도 된다
- 멀티 쓰레드 환경에서 동기화 문제가 발생하는 이유는 공유 자원에 동시에 쓰기(write) 때문이다
- 그러나 만약 공유 자원이 불변의 자원이라면, 항상 동일한 값을 반환하기 때문에 동기화를 고려하지 않아도 된다
- 이는 안정성을 보장할 뿐만 아니라 동기화를 하지 않음으로써 성능 측면으로도 좋다
2. 실패 원자적인(Failure Atomic) 메소드를 만들 수 있다
- 가변 객체를 통해 어떤 작업을 하던 도중 예외가 발생하면 해당 객체가 불안정한 상태에 빠질 수 있다
- 그리고 불안정한 상태를 갖는 객체는 또 다른 에러를 유발할 수 있다
- 하지만 불변 객체는 어떠한 예외가 발생하더라도 메소드 호출 전의 상태를 유지할 수 있다
- 또한 예외가 발생해도 오류가 발생하지 않은 것처럼 다음 로직을 처리할 수 있다
3. Cache나 Map 또는 Set 등의 요소로 활용하기에 더욱 적합하다
- 만약 캐시나 Map, Set 등의 원소인 가변 객체가 변경되었다면 이를 갱신하는 등의 부가 작업이 필요할 것이다
- 하지만 불변 객체라면 한 번 데이터가 저장된 이후에 다른 작업들을 고려하지 않아도 되므로 사용하는데 용이하다
4. 부수 효과(Side Effect)를 피해 오류 가능성을 최소화할 수 있다
- 부수 효과란 변수의 값이 상태 등의 변화가 발생하는 효과를 의미한다
- 만약 객체의 수정자(Setter)를 통해 여러 객체들에서 값을 변경한다면 객체의 상태를 예측하기 어려워진다
- 바뀐 상태를 파악하기 위해서는 메소드들을 살펴봐야하고, 이는 유지보수성을 저하시킨다
- 따라서 이러한 부수 효과가 없는 순수 함수들을 만드는 것이 상당히 중요하다
- 이 때 불변 객체는 기본적으로 값의 수정이 불가능하기 때문에 변경 가능성이 적으며, 객체의 생성과 사용이 상당히 제한된다
- 따라서 메소드들은 자연스럽게 순수 함수들로 구성될 것이고, 다른 메소드가 호출되어도 객체의 상태가 유지되기 때문에 안전하게 객체를 다시 사용할 수 있다
- 이러한 불변 객체는 오류를 줄여 유지보수성이 높은 코드를 작성하도록 도와준다
💡 순수함수란?
- 부수효과가 없는 함수
- 어떤 함수에 동일한 인자를 주었을 때 항상 같은 값을 리턴하고, 외부의 상태를 변경하지 않는 함수
5. 다른 사람이 작성한 함수를 예측가능하며 안전하게 사용할 수 있다
- 불변성(Immutability)이 보장된 함수는 누가 사용하더라도 값이 변하지 않음을 보장받을 수 있다
- 따라서 협업 시에도 도움을 준다
6. 가비지 컬렉션의 성능을 높일 수 있다
- 불변의 객체는 한 번 생성된 이후에 수정이 불가능한 객체로, Java에서 final 키워드를 사용하여 불변의 객체를 생성할 수 있다
- 이렇게 객체를 생성하기 위해서는 객체를 갖는 또 다른 컨테이너 객체(ImmutableHolder)도 존재한다는 것인데, 당연하게도 불변 객체(Object value)가 먼저 생성되어야 컨테이너 객체가 이를 참조할 수 있을 것이다
- 즉, 컨테이너는 컨테이너가 참조하는 가장 젊은 객체들보다 더 젊다(늦게 생성됨)는 것이다
- Object 타입의 value 객체 생성
- ImmutableHolder 타입의 컨테이너 객체 생성
- ImmutableHolder가 value 객체를 참조
- 이러한 점은 GC가 수행될 때 가비지 컬렉터가 컨테이너 객체 하위의 불변 객체들은 skip할 수 있도록 도와준다
- 왜냐면 해당 컨테이너 객체(ImmutableHolder)가 살아있다는 것은 하위의 불변 객체들(value) 역시 처음에 할당된 상태로 참조되고 있음을 의미하기 때문이다
public class MutableHolder {
private Object value;
public Object getValue() { return value; }
public void setValue(Object o) { value = o; }
}
public class ImmutableHolder {
private final Object value;
public ImmutableHolder(Object o) { value = o; }
public Object getValue() { return value; }
}
@Test
public void createHolder() {
// 1. Object 타입의 value 객체 생성
final String value = "MangKyu";
// 2. Immutable 생성 및 값 참조
final ImmutableHolder holder = new ImmutableHolder(value);
}
- 결국 불변의 객체를 활용하면 가비지 컬렉터가 스캔해야 되는 객체의 수가 줄어서 스캔해야 하는 메모리 영역과 빈도 수 역시 줄어들기 때문에 GC가 수행되어도 지연 시간을 줄일 수 있다
- 따라서 필드값을 수정할 수 있는 MutableHolder보다는 필드값을 수정할 수 없는 ImmutableHolder를 사용하는 것이 좋다
- Holder의 값이 바뀌는 경우라면 MutableHolder를 사용하는게 더 나아보일수도 있지만, 가비지 컬렉터 입장에서 생명주기가 짧은 객체를 처리하는 것은 어렵지 않으며, 오히려 MutableHolder의 값이 지속되어 old-to-young 참조가 일어나는 것이 더 큰 성능 저하를 야기한다
불변 객체의 단점
- 객체의 값이 할당될 때마다 새로운 객체가 필요하여 메모리 누수와 성능 저하가 발생할 수 있다
불변 객체를 생성하는 방법
1. final 키워드
Java에서는 불변성을 확보할 수 있도록 final 키워드를 제공한다
Java의 변수들은 기본적으로 가변적인데, 변수에 final 키워드를 붙이면 참조값을 변경 못하도록 하여 불변성 확보가 가능하다
final String name = "yammy";
name = "jiwon"; // 컴파일 에러
위처럼 final이 붙은 변수의 값을 변경하려고 하면 컴파일 에러가 발생한다
하지만 final 키워드가 객체의 상태 변경을 못하도록 막는 것은 아니다
final List<String> list = new ArrayList<>();
list.add("hello");
위처럼 List 객체가 final로 선언되었지만 새로운 객체를 추가해도 에러가 발생하지 않는다
이와 같은 문제가 발생할 수 있기 때문에 참조에 의해 값이 변경될 수 있는 점을 유의해야 한다
2. 불변 클래스
불변 객체를 생성하기 위해서는 다음과 같은 규칙을 따라서 클래스를 생성해야 한다
- 멤버 변수를 final로 설정한다
- class를 상속하지 못하도록 선언한다 (class를 final로 선언하거나 생성자를 private으로 선언한다)
- 객체를 생성하기 위한 생성자 또는 정적 팩토리 메소드를 추가한다
- 참조에 의해 변경 가능성이 있는 경우 방어적 복사(defensive-copy)를 이용하여 전달한다
public final class ImmutableClass {
private final int age;
private final String name;
private final List<String> list;
private ImmutableClass(int age, String name) {
this.age = age;
this.name = name;
this.list = new ArrayList<>();
}
public static ImmutableClass of(int age, String name) {
return new ImmutableClass(age, name);
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public List<String> getList() {
return Collections.unmodifiableList(list);
}
}
위의 코드를 보면 내부 생성자를 만드는 대신 객체의 생성을 위한 정적 팩토리 메소드를 제공한다
또한 참조를 전달하여 클라이언트에 의해 수정 가능성이 있는 list를 방어적 복사하여 제공하고 있다
Java에서는 생성자를 선언하지 않으면 기본 생성자가 자동으로 생성되는데, 그러면 다른 클래스에서 해당 객체를 자유롭게 호출할 수 있다
그렇기 때문에 내부 생성자를 만드는 대신 정적 팩토리 메소드를 통해 객체를 생성하도록 강요하는 것이 좋다
또한 배열이나 다른 객체 또는 컬렉션은 참조가 전달되어 수정 가능성이 있다
그렇기 때문에 참조를 통해 변경이 가능한 경우에는 방어적 복사를 통해 값을 반환해야 한다
⭕ Mutable Object (가변 객체)
- 가변 객체는 불변 객체와 달리 Heap 영역에 생성된 객체를 변경할 수 있다
- 즉, Java에서 Class의 인스턴스가 생성된 이후에 내부 상태가 변경 가능한 객체이다
- 우리가 Java에서 사용하는 대부분의 객체는 가변 객체이다
- 또한 프로그래머가 커스텀 객체를 생성하여 내부 상태를 변경할 수 있게 만드면, 그 또한 가변 객체가 된다
- 대표적인 가변 객체는 List, ArrayList, HashMap, StringBuilder, StringBuffer 등이 있다
- 가변 객체는 멀티 스레드 환경에서 사용할 경우, 별도의 동기화 처리가 필요하다
- 이렇게 동기화 처리된 객체 중 하나가 StringBuffer이다
✨ 정리
- Mutable은 객체의 수정을 허용하지만, Immutable의 경우 객체의 수정을 허용하지 않는다
- 수정이 필요할 경우 Mutable 객체는 기존의 객체에 수정사항을 곧바로 반영한다
- 하지만 Immutable 객체의 경우 기존의 객체는 그대로 두고 수정사항을 반영한 새로운 객체를 생성한다
🎯 예상 FAQ
1. 가변 객체 vs 불변 객체
가변 객체
- Java에서 Class의 인스턴스가 생성된 이후에 내부 상태를 변경할 수 있는 객체
- 가변 객체는 멀티 스레드 환경에서 사용하려면 별도의 동기화 처리가 필요하다
- 대표적인 가변 객체는 ArrayList, HashMap, StringBuilder, StringBuffer 등이 존재한다
불변 객체
- Java에서 Class의 인스턴스가 생성된 이후에 내부 상태 변경이 불가능한 객체
- 불변 객체는 멀티 스레드 환경에서도 안전하게 사용할 수 있다는 신뢰성을 보장한다
- 대표적인 불변 객체는 String 등이 존재한다
2. 불변 객체의 장점
- Thread-safe하여 병렬 프로그래밍에 유용하며, 동기화를 고려하지 않아도 된다
- 실패 원자적인(Failure Atomic) 메소드를 만들 수 있다
- Cache, Map, Set 등의 요소로 활용하기에 적합하다
- 부수 효과(Side Effect)를 피해 오류 가능성을 최소화할 수 있다
- 다른 사람이 작성한 함수를 예측 가능하며 안전하게 사용이 가능하다
- GC의 성능을 높일 수 있다
3. 불변 객체를 만드는 방법
- 모든 필드에 대해 final을 설정한다
- 필드에 참조 타입이 있을 경우, 해당 객체도 불변성을 보장해야 한다
- 필드에 컬렉션이 존재할 경우, 생성자 및 getter에 대해 방어적 복사를 수행해야 한다
4. 방어적 복사 vs Unmodifiable
- 방어적 복사 : A 리스트와 B 리스트 사이의 참조를 끊는 행위
- Unmodifiable : 참조를 끊지 않고 단순히 특정 리스트에서 요소의 변경이 일어날 경우 예외를 던진다
- 따라서 생성자 단계에서 방어적 복사를 취하지 않고, getter에서 Unmodifiable만 취할 경우 초기 생성자로 주입한 컬렉션의 변화가 생기면 불변성이 깨진다
5. 방어적 복사를 사용하면 항상 불변성을 보장하는가?
- 그렇지 않다
- 방어적 복사는 컬렉션의 요소에 대한 얕은 복사를 수행하므로 컬렉션의 참조 타입이 가변 객체라면, 복사하려는 컬렉션의 요소가 변경될 경우 불변성이 깨진다
참고)
https://choiblack.tistory.com/47
https://cantcoding.tistory.com/41
https://mangkyu.tistory.com/131
https://velog.io/@guswlsapdlf/Java%EC%9D%98-Mutable%EA%B3%BC-Immutable
https://steady-coding.tistory.com/559
'야미스터디 > Java' 카테고리의 다른 글
[Java] 인터페이스와 추상클래스 📌 (0) | 2022.08.18 |
---|---|
[Java] 오버라이딩 vs 오버로딩 📌 (0) | 2022.08.17 |
[Java] 접근 제한자 / 생성자 / SOLID 📌 (0) | 2022.08.06 |
[JAVA] static 변수 (0) | 2022.07.26 |
[Java] 객체지향 vs 절차지향 📌 (0) | 2022.07.22 |
댓글