
자바에서 문자열을 다루는 핵심 타입은 String이다. 문자열은 프로그램 전반에서 매우 자주 사용되기 때문에 자바는 이를 위해 특별한 문법적 지원과 내부 최적화를 제공한다. 이 글에서는 String의 구조 생성 방식 비교 불변성 문자열 풀 그리고 가변 문자열인 StringBuilder까지 한 흐름으로 정리한다.
String은 기본형이 아니라 참조형이다.
문자열 리터럴은 사실 String 객체를 생성한다.
String str1 = "hello";
String str2 = new String("hello");
String 클래스는 내부적으로 문자를 배열에 보관하며 다음과 같은 형태로 구성된다.
public final class String {
private final byte[] value; // Java 9 이후
public String concat(String str) { ... }
public int length() { ... }
}
value 필드는 final이며 생성 이후 절대 변경되지 않는다. 이것이 불변성의 출발점이다.
리터럴 방식은 문자열 풀을 사용해 중복 객체 생성을 방지한다.
String s1 = "hello";
new 키워드를 사용하면 매번 새로운 객체가 생성된다.
String s2 = new String("hello");
String 비교에서 가장 중요한 점은 == 과 equals의 차이를 이해하는 것이다.
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
리터럴은 같은 문자열을 문자열 풀에서 공유하기 때문에 == 결과가 true가 될 수 있다.
String x = "hello";
String y = "hello";
System.out.println(x == y); // true
따라서 문자열 비교는 항상 equals를 사용해야 한다.

리터럴을 사용하면 JVM은 문자열 풀에서 동일한 문자열이 이미 존재하는지 먼저 확인한다.
있다면 그 객체를 재사용하고 없으면 새로운 객체를 생성한다.
문자열 풀을 사용하는 이유는 메모리 최적화 뿐 아니라 빠른 비교를 가능하게 하기 때문이다.
String은 내부 상태가 절대 변경되지 않는 불변 객체다.
다음 코드를 보면 concat이 기존 문자열을 수정하지 않는다는 점을 알 수 있다.
String s = "hello";
s.concat(" java");
System.out.println(s); // hello
값을 변경하려면 새로운 객체를 받아야 한다.
String s1 = "hello";
String s2 = s1.concat(" java");
System.out.println(s1); // hello
System.out.println(s2); // hello java
불변 설계는 여러 이유로 필요하다.
문자열 풀의 안정성 확보
같은 리터럴을 참조하는 변수들이 있는 상황에서 만약 문자열이 변경 가능하다면 모든 참조하는 값이 동시에 변경되는 문제가 발생한다.
스레드 안전성
값이 바뀌지 않기 때문에 여러 스레드가 동시에 접근해도 동기화가 필요 없다.
보안성
URL 경로 SQL 쿼리 네트워크 프로토콜과 같은 정보가 변경 가능하면 보안상 취약해질 수 있다.
문자열을 여러 번 변경해야 하는 경우 String의 불변성은 성능에 불리하게 작용한다.
문자열을 반복해서 변경하려고 하면 매번 새로운 객체가 생성되기 때문이다.
String result = "";
for (int i = 0; i < 100000; i++) {
result += "Hello Java ";
}
문자열 변경이 잦을 때는 StringBuilder를 사용해야 한다.
StringBuilder sb = new StringBuilder();
sb.append("A").append("B").append("C").append("D");
String result = sb.toString();
StringBuilder는 내부 배열을 직접 변경하므로 객체 생성이 반복되지 않는다.
반복문에서 문자열을 합치는 작업은 StringBuilder가 압도적으로 빠르다.
StringBuilder는 append insert delete reverse 같은 메서드가 호출될 때 자기 자신을 반환하도록 설계되어 있다.
String result = new StringBuilder()
.append("A")
.append("B")
.append("C")
.reverse()
.toString();
메서드의 반환값이 자기 자신이기 때문에 메서드를 연속으로 연결해 호출할 수 있고 코드가 간결해진다.