자바를 사용할때 기본 자료형(Primitive Object) 다음으로 가장 많이 사용되는 객체는 String 클래스라고 생각된다. 때로는 이런 String 자료형을 기본 자료형이라고 착각할 때도 많다(몇몇 다른 언어에서 문자열이 기본 자료형인것도 혼동에 한몫 한다). 그럼 이런 String 클래스 어떤 방식으로 사용되고 저장되는지 알아보자.
String 클래스는 immutable Object, 불변 객체이다.
🤔 응? 불변 객체란 무엇인가?
불변 객체는 객체 생성 후, 내부 상태가 일정하게 유지 되는 객체이다. 객체가 변수에 할당되는 순간 더이상 참조를 업데이트하거나 내부 상태를 변경할 수 없다.
String str = "Hello"
str += "World"
즉 위와 같이 str이 String 객체를 초기 "Hello"로 생성하고 이에 "World"를 추가하면 str 객체의 값이 변경되는 것이 아닌 새로운 "Hello World"라는 String 객체가 생성되고 str이 이를 참조하게 된다.
Java 창시자 James Gosling은 '자바에서 String이 불변으로 설계된 이유가 무엇인지'에 대한 질문을 많은 인터뷰에서 받았다고 한다.
그가 설명한 자바에서 String이 불변인 이유는 다음과 같다.
앞서 말했듯 String은 가장 널리, 자주 사용되는 데이터 구조이다. 즉, String 리터럴을 캐싱 하고 재사용하면 다른 String 변수가 Java String Pool에서 동일한 객체를 참조할 수 있기 때문에 많은 Heap 공간이 절약된다.
즉 Heap 공간 절약을 위해 JVM은 각 리터럴 문자열을 하니씩만 Java String Pool에 저장하여 문자열에 할당된 메모리 양을 최적화한다.(해당 과정을 interning이라고 한다)
String 객체는 사용자 이름, 암호, 연결 URL, 네트워크 연결 등과 같은 민감한 정보를 저장할 때 Java 애플리케이션에서 주로 사용된다. 또한 클래스를 로드하는 과정에서 JVM 클래스 로더에서도 String 객체가 사용된다.
만약 String이 변할 수 있는 경우를 가정하고 다음과 같은 프로세스를 생각해보자
위와 같이 pw를 입력받고 Validation(무결성 검사)까지 모두 진행한 후 pw 저장 작업을 진행하려하는데 다른 스레드에서 pw를 변경한다면 pw 저장 작업에 문제가 생기게 된다. 즉 String 객체의 불변성은 민감한 코드 작업의 간섭을 적게 할 수 있어 코드 작업의 보안을 높여준다.
위 보안에서의 이유와 비슷한 이유이다. String 객체를 변경할 수 없기 때문에 여러 스레드에서 액세스할 때 String 스레드가 변경되지 않으므로 String 스레드가 안전한 상태로 유지 가능하다. 따라서 불변 객체로 구현된 String 객체는 동시에 실행되는 여러 스레드에서 공유될 수 있다.
String 객체는 데이터 구조로 많이 사용 되기 때문에 HashMap, HashTable, HashSet 등과 같은 해시 구현에서도 자주 사용된다. 이러한 해시 구현에서 작동할 때 hashCode() 메서드를 사용하는데 효율적인 성능을 위해 hashCode() 메서드를 String Class에 덮어 씌우고 첫번째 hash만 계산하고 캐시한 후 이후 접근 부터는 동일한 값을 반환한다.
String Class를 불변 객체로 사용하면 Heap 메모리를 절약할 수 있고 여러 스레드 사용 시 값이 변할 우려를 없애 보안과 동기화를 보장할 수 있다. 또한 String 객체가 변경될지 걱정하지 않고 사용할 수 있기에 우리가 String을 기본 자료형처럼 착각하면서(?) 사용할 수 있는 것이다.
String은 불변하기에 사용할 때 조심해야한다. 아님 굉장한 메모리 낭비와 속도 저하를 일으킬 수 있다.
String str = "Hello"
str += "World"
위에서 살펴본 'HelloWorld' 예제에서 다음과 같이 str에 "World"를 더하면 새로운 "HelloWorld" 객체가 생성되는 것을 확인하였다.
이때 남아있던 Hello 객체는 버려져 Garbage Colletion(GC)의 대상이 된다.
그렇다면 다음과 같은 코드는 어떨까?
String temp = "";
for (int i = 1; i <= 1000; i++) {
temp += i;
}
다음 코드는 반복문을 통해 1000개의 객체가 새롭게 생성되어 총 1001개의 객체가 생성되고 이중 1000개의 객체가 GC의 대상이 된다.
GC는 하면 할수록 시스템의 CPU를 사용하고 시간도 많이 소요되기 때문에 GC의 대상이 되는 객체를 최소화해야 한다. 즉 위와 같은 코드는 메모리 낭비가 크고 GC에 의해 응답 속도에 많은 영향을 미치게 된다.(분신술을 줄이자!)
String Class는 두가지 방법으로 선언 가능하다.
1. 리터럴
String text = "text";
리터럴로 저장되는 경우 Java String Pool 영역에 저장된다
2. new 사용
String text = new String("text");
new를 사용하여 저장하는 경우 Java String Pool이 아닌 Heap영역에 저장된다. new를 사용하여 생성한 String 객체는 변할 수 있다(mutable)
아래 코드를 보면 str1과 str2의 주소값은 동일하지만 newStr1과 newStr2의 주소값은 다른것을 확인할 수 있다.
@Test
@DisplayName("String 주소 확인")
void String_Address_Test() {
String str1 = "Hello";
String str2 = "Hello";
String newStr1 = new String("Hello");
String newStr2 = new String("Hello");
assertThat(System.identityHashCode(str1) == System.identityHashCode(str2)).isTrue();
assertThat(System.identityHashCode(newStr1) == System.identityHashCode(newStr2)).isFalse();
}
만약 String에서 단순히 값 자체를 비교하고 싶을 때는 equals() 메서드를 사용한다.
equals 메서드는 다음과 같이 구성되어있는데 String 객체의 value 자체를 비교하는 것을 확인할 수 있다.
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
// String 객체의 value 자체를 비교
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
Why String is Immutable in Java? _ baeldung
String 클래스를 조심히 사용하자. _ 둔덩