Java String 불변성이란?
Java String은 불변(Immutable) 객체입니다. 한 번 생성된 String 객체의 값은 변경할 수 없으며, 문자열을 수정하는 메서드들은 모두 새로운 String 객체를 반환합니다.
String 클래스는 내부적으로 final char[] value 또는 final byte[] value 필드(Java 9부터)를 사용하여 문자열을 저장하며, 이 필드는 final로 선언되어 있습니다. 또한 String 클래스 자체도 final로 선언되어 상속이 불가능합니다.
String original = "Hello";
String modified = original.concat(" World");
System.out.println(original); // "Hello" (변경되지 않음)
System.out.println(modified); // "Hello World" (새로운 객체)
핵심 개념
1. String 불변성의 메커니즘
String의 불변성은 여러 층위에서 보장됩니다:
public final class String {
private final byte[] value; // Java 9+
private final byte coder; // 인코딩 정보
// 수정 메서드들은 모두 새로운 객체를 반환
public String toUpperCase() {
// 새로운 String 객체 생성 후 반환
}
public String replace(char oldChar, char newChar) {
// 새로운 String 객체 생성 후 반환
}
}
문자열을 변경하는 모든 연산은 원본을 수정하지 않고 새로운 String 인스턴스를 생성합니다:
String str = "Java";
str.replace("J", "K"); // 새 객체 생성, str은 여전히 "Java"
str = str.replace("J", "K"); // 참조를 새 객체로 변경해야 함
2. String Constant Pool과 메모리 최적화
String 리터럴은 힙 메모리의 특별한 영역인 String Constant Pool에 저장됩니다:
String s1 = "Hello"; // String Pool에 저장
String s2 = "Hello"; // 기존 객체 재사용
String s3 = new String("Hello"); // 힙에 새 객체 생성
System.out.println(s1 == s2); // true (같은 객체 참조)
System.out.println(s1 == s3); // false (다른 객체 참조)
System.out.println(s1.equals(s3)); // true (내용은 같음)
intern() 메서드를 사용하면 힙의 String 객체를 String Pool로 이동시킬 수 있습니다:
String heap = new String("Hello");
String pool = heap.intern();
System.out.println(s1 == pool); // true (Pool의 같은 객체)
3. Thread Safety와 성능 이점
불변 객체는 본질적으로 thread-safe하여 동기화가 불필요합니다:
public class StringExample {
private String sharedString = "Initial";
// 동기화 불필요 - String은 불변이므로
public void updateString(String newValue) {
this.sharedString = newValue; // 새로운 객체 할당만 발생
}
// 여러 스레드가 안전하게 접근 가능
public String getSharedString() {
return sharedString;
}
}
해시코드 캐싱으로 성능을 최적화합니다:
public final class String {
private int hash; // 해시코드 캐시
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
// 최초 한 번만 계산
hash = h = calculateHashCode();
}
return h;
}
}
4. 가변 문자열이 필요한 경우
빈번한 문자열 수정이 필요한 경우 StringBuilder나 StringBuffer를 사용합니다:
// String 사용 시 (비효율적)
String result = "";
for (int i = 0; i < 1000; i++) {
result += "a"; // 매번 새 객체 생성
}
// StringBuilder 사용 시 (효율적)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("a"); // 내부 버퍼 수정
}
String result = sb.toString();
정리
| 측면 | 특징 | 이점 |
|---|---|---|
| 불변성 | 생성 후 값 변경 불가 | 예측 가능한 동작, 버그 감소 |
| 메모리 | String Constant Pool 활용 | 동일 문자열 재사용으로 메모리 절약 |
| 동시성 | Thread-safe 보장 | 동기화 오버헤드 없음 |
| 성능 | 해시코드 캐싱 | HashMap 등에서 성능 향상 |
| 보안 | 민감 정보 보호 | 예기치 않은 수정 방지 |
String의 불변성은 메모리 효율성, 스레드 안전성, 성능 최적화를 동시에 제공하는 Java의 핵심 설계 원칙입니다. 단, 빈번한 문자열 수정이 필요한 경우에는 StringBuilder나 StringBuffer 같은 가변 대안을 사용하는 것이 효율적입니다.