본문 바로가기
Java

[Java] Mutable과 Immutable에 관하여

by jn4624 2022. 7. 24.
반응형

1. Mutable

  • 생성된 이후 수정 가능
  • 이미 존재하는 객체에 재할당(값 변경)
  • 값을 변경할 수 있는 메소드 제공
  • Mutable class일 경우 Getter와 Setter 존재
  • thread safe하지 않을 수 있음(병렬처리시 값 보장할 수 없게 됨)
  • StringBuffer, StrinfBuilder, java.util.Date 등이 해당

 

a. Mutable : 대표적인 StringBuffer, StringBuilder

문자열 연산에서 클래스를 한번 생성하고 연산이 필요할 때 크기를 변경하여 문자열을 변경한다.

문자열 연산이 자주 있을 때 사용하는 것이 적절하다.

 

b. StringBuffer

동기화 키워드를 지원하여 멀티스레드 환경에서 안전하다.

즉, 문자열 연산이 많고 멀티스레드 환경일 경우 사용하는 것이 적절하다.

 

c. StringBuilder

동기화를 지원하지 않는다.

싱글 스레드 환경이거나 스레드를 신경쓰지 않아도 되는 상황이면 StringBuilder를 사용하는 것이 적절하다.

즉, 문자열 연산이 많고 단일스레드이거나 동기화를 고려하지 않아도 되는 경우 사용하는 것이 적절하다.

 

d. Mutable의 속성 확인

public class Mutable {
	public static void main(String[] args) {
		StringBuilder sb = new StringBuilder();
		sb.append("abc");
		System.out.println(sb);			// abc
		System.out.println(sb.hashCode());	// 932583850
		
		System.out.println();
		
		sb.append("def");
		System.out.println(sb);			// abcdef
		System.out.println(sb.hashCode());	// 932583850
	}
}

hashCode() 메소드를 통해 StringBuilder 객체의 메모리 주소 값을 함께 출력해보았다.

 

같은 StringBuilder에 할당되었고, StrinfBuilder의 값을 abc에서 abcdef로 변경하였을 때, 메모리 주소 값이 변경되지 않았다.

이는 곧 932583850 의 메모리 주소에 할당된 abc란 값이 abcdef로 변견 것임을 나타낸다.

따라서 StrinfBuilder class는 Mutable하게 동작한다.

 

2. Immutable

  • 생성된 이후 수정 불가능
  • 이미 존재하는 객체이더라도 새로운 객체를 생성하여 재할당
  • 값을 변경할 수 있는 메소드 미제공
  • Immutable class일 경우 Getter와 Setter 미존재
  • thread safe(병렬처리시 문제 없음)
  • Legacy classes, Wrapper classes, String class 등이 해당

 

a. Immutable : 대표적인 String

문자열 연산에서 new 연산을 통해 생성된 인스턴스의 메모리 공간은 절대 변하지 않는다.

문자열에 변화를 주면 메모리 공간이 변하는 것이 아닌 새로운 String 객체를 new로 생성하여 새로운 메모리 공간을 만들어준다.

기존의 문자열은 GC에 의해 제거되어야 한다.

 

b. String

문자열 연산이 적고, 조회가 많을 때 사용한다.

불변성을 가지기 때문에 멀티스레드 환경에서의 안전성을 가지고 있다.

즉, 문자열 연산이 작고 멀티스레드 환경일 경우 사용하는 것이 적절하다.

 

c. Immutable의 속성 확인

public class Immutable {
	public static void main(String[] args) {
		String str = "abc";
		System.out.println(str);		// abc
		System.out.println(str.hashCode());	// 96354
		
		System.out.println();
		
		str += "def";
		System.out.println(str);		// abcdef
		System.out.println(str.hashCode());	// -1424385949
	}
}

hashCode() 메소드를 통해 String 객체의 메모리 주소 값을 함께 출력해보았다.

 

같은 String 객체에 할당되었지만 해당 객체의 값을 abc에서 abcdef로 변경하였을 때, 메모리 주소 값도 함께 변경되었다.

이는 곧 96354의 메모리 주소에 할당된 abc란 값이 abcdef로 변한 것이 아니라 -1424385949의 메모리 주소에 abcdef란 값을 가진 String 객체가 새로 생성된 것임을 나타낸다.

따라서 String class는 Immutable하게 동작한다.

 

d. Mutable한 객체를 Immutable한 객체로 사용

  • 방어적 복사(defensive copy)

Mutable 객체는 생성 이후 값 변경이 자유롭게 가능하여 개발자가 의도하지 않은 객체의 변경이 일어날 수 있다는 단점이 존재한다.

단점의 주 원인은 `레퍼런스를 참조한 다른 객체에서 객체를 변경`할 수 있기 때문이다.

이를 해결하고자 객체의 변경이 필요할 경우 참조가 아닌 객체의 방어적 복사(defensive copy)를 통해 새로운 객체를 생성한 후 변경하도록 한다.

public Period(Date start, Date end) {
	validCheck(start, end);
	this.start = start;
	this.end = end;
}

Date의 경우 Mutable 객체이므로 생성된 이후 값이 변경될 수 있다.

위 코드의 경우 validCheck 함수 내에서 start와 end의 값이 변하게 될 경우 원본 start와 end 값을 알 수 없게 된다.

멀티스레딩 환경일 경우 이는 특히 더 중요한 문제점이 된다.

따라서 validCheck 함수를 수행하기 전 방어적 복사본을 만들고, 이 복사본으로 validCheck 함수를 수행하도록 코드를 변경한다. 변경한 코드는 아래와 같다.

public Period(Date start, Date end) {
	this.start = new Date(start.getTime());
	this.end = new Date(end.getTime());
	validCheck(start, end);
}

 

또한, 방어적 복사를 통해 객체의 복사본을 만들었어도 내부 요소들이 Mutable하다면 해당 객체는 Immutable할 수 없다.

import java.util.ArrayList;
import java.util.List;

public class Names {
	private final List<Name> names;
	
	public Names(List<Name> names) {
		this.names = new ArrayList<>(names);
	}
}

위 코드는 방어적 복사를 적용해 names를 파라미터로 받는 생성자를 생성하고 있는 코드이다.

import java.util.ArrayList;
import java.util.List;

public class Application {
	public static void main(String[] args) {
		Name name1 = new Name("name1");
		Name name2 = new Name("name2");
		
		List<Name> originalNames = new ArrayList<>();
		originalNames.add(name1);
		originalNames.add(name2);
		
		Names names = new Names(originalNames); // names 값 : name1, name2
		
		name2.setName("name4"); // names 값 : name1, name4
	}
}

방어적 복사를 통해 originalNames와 names 객체 내부의 names는 각기 다른 객체가 된다.

이때 마지막 줄에서 originalNames의 내부 요소인 name2의 name 인스턴스 변수를 수정한다.

name1과 name2는 names 객체와 달리 Mutable하다. 그렇다면 원본 객체인 originalNames는 여전히 Immutable한가?

아니다. 원본 객체의 name2가 변경되어 방어적 복사를 통해 생성된 객체의 name2도 변경되어 Immutable이었던 names 객체는 Mutable한 내부 요소들로 인해 Mutable 객체로 변했다.

따라서 객체를 Immutable로 만들기 위해서는 방어적 복사 외에도 객체 내부의 요소들까지 모두 Immutable로 만들어주어야 한다.

 

  • Setter 메소드 제거

Mutable 객체와 Immutable 객체의 가장 큰 차이는 생성된 이후 수정이 가능한가, 가능하지 않은가에 있다.

따라서 Mutable 객체를 Immutable하게 사용하려면 생성된 이후 변경이 불가능하게 만들어주면 된다.

class 객체 생성 이후 값을 변경하는 역할은 Setter 메소드가 맡고 있으므로 이를 제거하여 변경을 막는다. 마치 `읽기 전용`으로 파일을 여는 것과 비슷하다.

 

  • 그 외

final 사용할 것 - final class는 상속이 불가하며 final 메소드는 오버라이딩이 불가하다. 원치 않는 class와 메소드 내의 수정을 막을 수 있다.

모든 클래스 변수를 private과 final로 선언할 것 - Setter 메소드가 없을 경우 외부에서 접근할 수 없어 변경이 불가해진다. 변수들을 private으로 선언해두고 class 내부의 메소드만으로 변수를 접근 가능하게 하는 정보 은닉(Data Hiding)을 떠올리면 이해가 쉽다.

 

3. 정리

정리하면 Mutable은 객체의 수정을 허용하나, Immutable은 객체의 수정을 허용하지 않는다.

수정이 필요한 경우 Mutable 객체는 기존의 객체에 수정사항을 곧바로 반영한다. 하지만 Immutable 객체는 기존의 객체는 그대로 두고 수정사항을 반영한 새로운 객체를 생성한다는 점에서 차이가 있다.

 

 

🙏 참조 ::

반응형