Java를 공부하다 보면 '불변 객체' 라는 말을 들어봤을 것이다. 불변 객체가 무엇인지 알아보자!
1. Immutable이란?
Immutable은 '불변의, 변경할 수 없는' 이라는 뜻을 가지고 있다. 객체 지향 프로그래밍에서 불변객체(Immutable object)는 생성 후 그 상태를 바꿀 수 없는 객체를 말한다. 대표적으로 String, Boolean, Integer, Float, Long 등이 있다.
예를 들어, String a = "apple" 에서 a = "banana"로 재할당이 가능하다. a가 참조하고 있는 heap 영역의 객체가 바뀌는 것이지 heap영역에 있는 값이 바뀌는 것이 아니다. 즉, 값이 변경되는 것이 아니라 새로운 객체를 만들고 그 객체를 a가 참조하게 하는 것이다. 이렇게 하면, "apple" 값을 지니고 있는 객체는 아무도 참조하고 있지 않는 객체가 되며 gc대상이 된다.
2. Immutable의 특징
- 장점
- 생성자, 접근 메서드에 대한 방어 복사가 필요없다.
- 멀티스레드 환경에서 동기화 처리없이 객체를 공유할 수 있다.
- 불변이기 때문에 객체가 안전하다. (Thread-safe)
- 단점
- 객체가 가지는 값마다 새로운 객체가 필요하다.
- 메모리 누수와 새로운 객체를 계속 생성하기 때문에 성능저하를 발생시킬 수 있다.
3. 대표적인 불변 객체, String
자바에서 대표적인 불변 클래스인 String이 있다. String은 기본 타입(Primitive Type)이 아닌 참조 타입(Reference Type)이다. 즉, String은 클래스이다. Java에서 String 객체는 특별히 String constant pool(Heap할당)에서 따로 관리가 된다. 아래 예시를 같이 보자.
# int 변수
int num = 10;
num = 20;
# String 변수
String str = "abc"
str = "def"
위 코드처럼 num과 str 변수를 생성하면 아래와 같이 메모리에 저장된다.
int 변수 num은 변수에 할당된 스택 메모리에 값을 바로 저장하고 있기 때문에 10에서 20으로 값을 변경할 경우 변수가 갖고 있는 메모리에서 값을 변경한다. 이것을 가변이라고 할 수 있다.
반면에, String 변수 str은 Heap 영역 중에서 String constant pool이라는 곳에 메모리를 할당받아 거기에 값을 저장하고, str은 바로 그 주소 값을 참조하게 된다. 따라서, str="abc" 후에 str="def"가 실행되어 str이라는 변수가 갖는 참조 값이 바뀌게 된다. 이것은 데이터가 바뀌는 것 아니라서 불변성이라고 할 수 있다.
그렇다면, 왜 Java에서 String은 불변일까? String을 불변으로 관리함으로써 많은 이점을 얻는다.
- String pool을 통해 String을 관리함으로써 Java는 Runtime에서 Heap 영역의 많은 메모리를 절약할 수 있습니다. 왜냐면 같은 값을 갖는 String에 대해 같은 메모리를 참조하게 할 수 있기 때문입니다. 만약 String이 불변이 아니었다면, 해당 메모리에 값이 언제 바뀔지 알 수 없기 때문에 String pool 형태로 관리할 수 없게 됩니다. 예를 들어 a, b, c라는 String 변수가 모두 같은 메모리를 가리킬 때 a의 값을 바꿔버리면 b와 c의 값도 바뀌는 문제가 발생할 수 있습니다.
- String이 불변이 아니라면 보안상의 문제를 야기할 수 있습니다. 예를 들어, DB의 username과 password 라던가, 소켓 통신에서 host와 port에 대한 정보가 String으로 다루어지기 때문에 String이 불변이라야 해커의 공격으로부터 값이 변경되는 것을 예방할 수 있습니다.
- String이 불변이기 때문에 멀티 쓰레딩 환경에서 안전(thread-safe)합니다. 값의 변경 가능성이 없기 때문에 멀티 쓰레딩 환경에서 동기화 문제를 걱정하지 않아도 됩니다.
- Java는 String의 hashcode를 생성 단계에서부터 캐싱합니다. 따라서 String의 hashcode는 쓰일 때마다 매번 계산되지 않습니다. 이 특징은 특히 객체의 hashCode를 Key로 사용하는 HashMap의 경우에 효과를 발휘합니다. 다른 객체는 키로 쓰일 때마다 hashCode를 계산하는데 비해 String은 캐싱을 하고 있기 때문에 다른 객체를 Key로 했을 때보다 String을 Key로 했을 때 더 빠른 속도로 사용할 수 있습니다. Java에서 String의 hashcode가 캐싱될 수 있는 이유가 바로 String이 불변이기 때문에 가능한 것입니다.
4. Immutable Class를 만들어보기
Immutable Class를 만들기 위해서 2가지를 알면 된다.
1. 모든 필드를 final로 선언한다.
2. 접근 메서드를 구현하지 않는다. (Setter 메서드)
Immutable 클래스를 만들어보자!
public final class ImmutableObj {
private final int num;
ImmutableObj(int name) {
this.num = num;
}
public int getNum(){
return num;
}
}
불변객체를 작성하면 위와 같은 모양을 갖는다.
불변 객체 작성 시 유의사항을 알아보자.
1) reference type이 필드로 있는 경우
Amount.java
public class Amount {
private int value;
public Amount(int value){
this.value = value;
}
public void setValue(int value){
this.value = value;
}
public int getValue(){
return value;
}
@Override
public String toString() {
return "amount = "+value;
}
}
ImmutableReference.java
public final class ImmutableReference {
private final int num;
private final Amount amount;
public ImmutableReference (int num, Amount amount){
this.num = num;
this.amount = amount;
}
public int getNum() {
return num;
}
public Amount getAmount() {
return amount;
}
}
해당 불변객체는 int라는 primitive type, Amount라는 reference type을 가지고 있다. 이 불변객체는 어떤 문제가 있을까?
@DisplayName("불변 객체 테스트")
class ImmutableReferenceTest {
@Test
void immutableTest() {
Amount amount = new Amount(10);
ImmutableReference immutableReference = new ImmutableReference(10, amount);
System.out.println(immutableReference.getAmount()); // (1)
Amount newAmount = immutableReference.getAmount();
newAmount.setCount(50);
System.out.println(immutableReference.getAmoount()); // (2)
결과
amount=10
amount=50
ImmutableReference로 부터 amount를 가져온 이후에 외부에서 amount의 값을 변경했다. 하지만 변경된 값이 newAmount에만 적용된 것이 아닌 ImmutableReference에도 적용된 것을 확인할 수 있다.
이 객체의 문제는 불변객체의 필드로 선언한 Amount 객체의 참조가 외부와 연결되어 있어서 발생한 것이다. 그렇기 때문에 안전한 객체를 만드려면 불변객체 내부의 필드 또한 불변객체로 선언되어야 한다. 혹은 방어적 복사를 이용해 참조관계를 끊어줘야 한다.
ImmutableReference객체를 방어적 복사를 이용해 참조를 끊도록 수정해보겠다.
public final class ImmutableReference {
private final int num;
private final Amount amount;
public ImmutableReference (int num, Amount amount){
this.num = num;
this.amount = new Amount(amount.getValue()); // (1)
}
public int getNum() {
return num;
}
public Amount getAmount() {
return new Amount(amount.getValue()); // (2)
}
}
결과
amount=10
amount=10
(1) : 전달받은 Amount 객체가 외부에서 변결될 수 있어 참조를 끊음
(2) : 외부로 전달한 Amount 객체가 외부에서 변경될 수 있어 참조를 끊음
위와 같이 방어적 복사를 이용해 참조관계를 끊고 실행하면 아까와 다르게 외부의 변경에도 불변객체는 값이 변하지 않는다. Amount를 방어적 복사가 아닌 불변 객체를 이용하면 마찬가지로 객체의 불변성을 지킬 수 있다.
2) Collection이 필드로 있는 경우
ImmutableCollection.java
public final class ImmutableCollection {
private final int num;
private final List<Integer> list;
public ImmutableCollections(int num, List<Integer> list) {
this.num = num;
this.list = list;
}
public int getNum() {
return num;
}
public List<Integer> getList() {
return list;
}
}
해당 불변객체는 List라는 CollectionType을 필드로 가지고 있다. 여기에선 어떤 문제가 있을까?
List를 ImmutableCollection로 전달하여 해당 List를 가져온 후 새로운 요소를 추가해보겠다. 과연 ImmutableCollection은 상태가 어떻게 될 지 확인해보겠다.
@DisplayName("불변 객체 Collection 테스트")
class ImmutableCollectionTest {
@Test
void immutableTest() {
List<Integer> list = new ArrayList<>();
list.add(1);
ImmutableCollection immutableCollection = new ImmutableCollection(10, list);
System.out.println(immutableCollection.getList());
List<Integer> newList = immutableCollection.getList();
newList.add(2);
System.out.println(immutableCollection.getList());
}
}
결과
[1]
[1, 2]
Collection 역시 참조가 연결되어 있기 때문에 위와 같이 ImmutableCollection의 상태가 변경된 것을 확인할 수 있다.
이를 방어하기 위해선 방어적 복사, UnmodifiableList와 같은 Unmodifiable Collection을 사용하는 방법이 있다. Unmodifiable Collection은 값 변경 시 예외를 발생시킨다.
그래서 직접 예외를 처리해주어야 하기 때문에 방어적 복사를 이용해서 문제를 해결해보자.
public final class ImmutableCollection {
private final int num;
private final List<Integer> list;
public ImmutableCollections(int num, List<Integer> list) {
this.num = num;
this.list = new ArrayList<>(list); // 변경
}
public int getNum() {
return num;
}
public List<Integer> getList() {
return new ArrayList<>(list); // 변경
}
}
결과
[1]
[1]
이제 외부의 변경에도 Collections의 값은 변하지 않는 것을 확인할 수 있다.
하지만 위 코드에서 사용한 List<Integer>의 Integer은 불변객체였다. 가변객체가 들어온다면 어떻게 될까? 해당 Collection을 List<Amount>로 변경해서 다시 테스트 해보자.
2) Collection의 요소가 가변객체인 경우
ImmutableCollection.java
public final class ImmutableCollection {
private final List<Amount> amounts;
public ImmutableCollection(List<Amount> amounts) {
this.amounts = new ArrayList<>(amounts);
}
public List<Amount> getAmounts() {
return new ArrayList<>(amounts);
}
}
@DisplayName("불변 객체 Collection 테스트")
class ImmutableCollectionTest {
@Test
void immutableTest() {
Amount amount = new Amount(1);
Amount amount2 = new Amount(2);
List<Amount> amounts = List.of(amount, amount2);
ImmutableCollection immutableCollection = new ImmutableCollection(amounts);
System.out.println(immutableCollection.getAmounts());
amount2.setValue(100);
System.out.println(immutableCollection.getAmounts());
}
}
}
결과
[amount=1, amount=2]
[amount=1, amount=100]
방어적 복사를 했음에도 바깥 Collection의 참조는 끊어졌지만 내부 Object에 대한 참조는 끊기지 않음을 확인할 수 있다.
결국 ImmutableCollection 객체는 불변성이 보장되지 않는 객체인 것이다. 불변객체를 사용하기 위해서는 사용되는 필드 마찬가지로 불변객체를 사용하는 것이 안전하다.
[참고]
https://velog.io/@conatuseus/Java-Immutable-Object%EB%B6%88%EB%B3%80%EA%B0%9D%EC%B2%B4
[Java] Immutable Object(불변객체)
면접에서 "자바에서 불변객체에 대해 설명해주세요.."라는 질문을 받았다.속으로 'final만 붙이면 불변객체 아닌가?'라는 생각을 했지만 불변객체에 대해 공부하지 않아 모른다고 했다...그래서
velog.io
https://limkydev.tistory.com/68
[Java] Immutable Class (불변 클래스)
안녕하세요 이번시간은 Immutable Class(불변 클래스)에 대해서 알아보겠습니다. Immutable 란? Immutable을 사전적으로 찾아보면, 불변의, 변경할 수 없는 이라는 뜻임을 알 수 있습니다. 사전적인 의미에
limkydev.tistory.com
[Java] 불변 객체(Immutable Object) 에 대해 알아보자
예제 및 테스트 코드는 github 에서 확인 가능합니다.불변 객체(Immutable Object)란?객체 생성 이후 내부 상태가 변하지 않는, 변경할 수 없는 객체를 이야기합니다.불변객체는 내부 상태를 변경하는
devoong2.tistory.com
https://readystory.tistory.com/139
Java의 String 이야기(1) - String은 왜 불변(Immutable)일까?
제목에 적혀있다시피 Java에서 String은 불변(Immutable)입니다. 그럼 불변이란 무엇일까요? 불변은 말 그대로 아닐 불(不) 변할 변(變). 변하지 않는 것을 의미합니다. 그럼 Java에서 String이 불변이라는
readystory.tistory.com
'공부 > 자바' 카테고리의 다른 글
JVM 내부 구조와 메모리 영역 (2) | 2024.10.17 |
---|---|
어노테이션(Annotation) (0) | 2024.07.03 |
열거형(Enum) (0) | 2024.07.03 |
제네릭스(Generics) (0) | 2024.06.12 |
컬렉션 프레임워크(Collections Framework) (0) | 2024.06.10 |