유랑하는 나그네의 갱생 기록

だけど素敵な明日を願っている -HANABI, Mr.children-

Study/Java

[Java] String 객체 생성법 탐구 (with String Pool)

Madirony 2024. 9. 8. 02:40
728x90

String

String?

 Java의 String 클래스, 많이 사용하는 클래스 중 하나죠? 이번 글에서는 String을 String Pool과 함께 살펴보려고 합니다.

String 객체 생성법

 String 클래스는 문자열을 표현하는 데 사용됩니다. String 객체는 불변(immutable)이며, 한번 생성된 문자열은 변경할 수 없습니다. String 객체의 생성은 두 가지 방식, 문자열 리터럴(literal) 방식new 키워드 방식으로 나뉩니다.

String str1 = "Hello, World!";
String str2 = new String("Hello, World!");
뭐가 더 효율적일까요?

 문자열 리터럴 방식이 더 효율적이고 일반적으로 사용됩니다. 무슨 이유에서 일까요? 첫번째 방법이 더 효율적인 이유는 String Pool이라는 개념과 관련있습니다. 그러면 String Pool에 대해서 알아보겠습니다.

 


 

String Pool

 Java의 String Pool은 문자열을 효율적으로 관리하기 위해 JVM(Java Virtual Machine)이 제공하는 특별한 메모리 영역입니다. 이 메모리 영역은 힙 메모리의 일부로, 동일한 값을 가진 문자열이 중복되어 저장되는 것을 방지하고 메모리 사용을 최적화하는 역할을 합니다.

 

String Pool

참고 이미지를 보면 리터럴 방식으로 생성한 String 객체와 new 키워드로 생성한 방식의 차이를 한눈에 알아볼 수 있습니다.

 


 

String 객체의 불변성 (Immutability)

 먼저, String Pool을 이해하기 위해서는 Java의 String 객체가 불변(immutable)이라는 사실을 알아야 합니다. 아까 위에서 말했었죠. 불변이 뭔가요? 말 그대로 변하지 않는다. 한 번 생성된 String 객체는 그 값을 변경할 수 없다는 겁니다. 이 불변성 덕분에 여러 개의 String 객체가 동일한 문자열 값을 가질 경우, 굳이 각각의 객체를 만들 필요 없이 하나의 객체만을 공유하는 것이 가능합니다.

 


 

String Pool의 동작 원리

 그러면 이제 문자열 리터럴 방식 new 키워드 방식에서 String 객체를 생성할 때의 차이점을 String Pool과 엮어서 설명하겠습니다.

 

리터럴 방식

 "Hello"와 같은 문자열 리터럴이 코드에 등장하면, JVM은 먼저 String Pool에서 동일한 값의 문자열이 있는지 확인합니다. 만약 동일한 문자열이 존재하면, 그 객체의 주소를 반환하고 새로운 객체를 생성하지 않습니다. 하지만 동일한 문자열이 없으면 새로운 String 객체를 생성하고 String Pool에 저장합니다. 이런 방식으로 메모리를 절약할 수 있는 것이죠.

 

new 키워드 방식

 new String("Hello")를 사용하면, String Pool을 무시하고 항상 새로운 String 객체가 생성됩니다. 이 객체는 Heap 메모리에 생성되며, String Pool에 있는 동일한 값의 문자열과는 다른 객체가 됩니다.

 

 바로 이런 차이점에서 문자열 리터럴 방식이 효율적인 겁니다. String Pool을 통해 메모리를 절약할 수 있으니까요. 심지어 문자열 비교 시, 실제 문자열의 내용이 아닌 객체의 주소를 비교하게 돼 성능이 향상되기도 합니다.

 


 

String Pool의 제한과 고려사항

 이렇게 보면 String Pool이 만능으로 느껴질 수도 있겠습니다. 하지만 힙 메모리의 일부이기 때문에 결국에 크기에 제한이 있다는 점. JVM의 메모리 설정에 따라 String Pool의 크기를 조정할 수 있지만, 매우 큰 문자열을 다수 저장할 경우 OutOfMemoryError가 발생할 수 있습니다. 그런데 String Pool에서도 참조되지 않는 String에 대해서는 GC가 수행되니 큰 걱정은 하지 않아도 될 거 같습니다.

 

 그리고 new 키워드 방식으로 만든 String 객체를 String Pool에 넣는 방법도 있습니다. intern() 메서드를 활용하면 됩니다. intern() 메서드는 String Pool에서 동일한 값의 문자열을 찾아 그 주소를 반환하거나, 동일한 문자열이 없다면 String Pool에 추가합니다.

String str1 = "Hello";  // String Pool에 저장됨
String str2 = "Hello";  // str1과 동일한 객체를 참조

String str3 = new String("Hello");  // Heap에 새로 생성
String str4 = str3.intern();  // String Pool에서 "Hello"를 참조

// true: str1과 str2는 동일한 객체를 참조
System.out.println(str1 == str2);

// false: str3은 Heap에 있는 다른 객체
System.out.println(str1 == str3);

// true: str4는 String Pool에서 str1과 동일한 객체를 참조
System.out.println(str1 == str4);

실행 결과

 


 

new 키워드 방식의 존재 의의?

 그러면 지금까지 봤을 때, 모든 면에서 리터럴 방식이 좋은데 new 키워드 방식은 왜 있는 걸까요?

 

불변성을 유지한 상태에서 독립적인 객체를 생성하기 위해서!

 Java의 String 객체는 불변(immutable)이라 한 번 생성된 String 객체의 내용은 변경할 수 없습니다. 그런데 동일한 내용을 가진 독립적인 문자열 객체가 필요한 상황이 있을 수도 있겠죠? 예를 들어, 객체의 내용은 동일하지만 메모리 상에서 독립적인 객체로 관리하고 싶은 경우라던가.. 이때, new String()을 사용해서 새로운 객체를 생성하면 되겠습니다.

 

뭐.. 추가로 보안상의 이유로 객체 재사용보다는 새로운 객체를 생성하는 게 더 안전할 수 있어서라는 이유도 들 수 있겠습니다. 하지만 자바 버전이 낮았을 때는 new 키워드 방식이 필요했습니다.

 

서브스트링 문제를 해결하기 위해서! (Java 7 이전)

 Java 7 이전에는 substring() 메서드를 호출하면 원본 문자열을 기반으로 한 새로운 문자열을 생성하지 않고, 원본 문자열에 대한 참조와 부분 문자열의 시작 및 끝 인덱스를 사용하여 서브스트링을 만들었습니다. 이로 인해 원본 문자열이 매우 길고, 서브스트링이 짧은 경우에도 원본 문자열 전체가 메모리에 유지되어야 했습니다.

 이 문제를 해결하기 위해 new String(substring)과 같이 new String()을 사용하여 서브스트링만을 독립적인 객체로 생성하는 방법이 사용되었습니다.

substring()의 변화

이미지를 참고하면 좀 더 이해가 쉽습니다.

728x90