Java에서 String은 가장 많이 사용되는 클래스이면서, 동시에 가장 많은 오해가 있는 클래스입니다. 불변성(Immutability), String Pool, 성능 최적화, 그리고 Java 11~17에서 추가된 메서드까지 완전히 정리합니다.


1. String이 불변(Immutable)인 이유

불변이란?

String s = "Hello";
s.toUpperCase();         // 새로운 String 반환
System.out.println(s);  // "Hello" — s 자체는 변하지 않음

s = s.toUpperCase();    // 참조를 새 객체로 교체
System.out.println(s);  // "HELLO"
메모리 구조:
s ──────────────────► "Hello"   (변경 불가)
s (재할당 후) ───────► "HELLO"  (새 객체)
                       "Hello"  (GC 대상)

불변으로 설계한 이유

1. String Pool 공유 가능

String a = "hello";
String b = "hello";
// 같은 객체를 공유해도 안전 — 변경 불가이므로
System.out.println(a == b);  // true (같은 Pool 객체)

2. 스레드 안전 (Thread-Safe)

// 불변 객체는 동기화 없이 여러 스레드에서 공유 가능
String shared = "공유 문자열";
// 어떤 스레드도 shared를 변경할 수 없음

3. 해시코드 캐싱

// String의 hashCode는 한 번만 계산하고 캐싱
// HashMap, HashSet의 키로 안전하게 사용 가능
private int hash; // 기본값 0, 처음 hashCode() 호출 시 계산

4. 보안

// 파일 경로, URL, DB 연결 문자열이 중간에 변경되면 보안 위협
// 불변이므로 한 번 검증하면 안전
void openFile(String path) {
    validate(path);         // 검증
    // path가 변경될 수 없으므로 안전하게 사용
    Files.open(path);
}

내부 구현

// Java 9 이후 String 내부 (compact strings)
public final class String {
    private final byte[] value;   // UTF-16 또는 Latin-1
    private final byte coder;     // LATIN1=0, UTF16=1
    private int hash;             // 캐싱된 hashCode
}

2. String Pool (intern) 동작 원리

String Pool이란?

JVM Heap 내의 특별한 영역(Java 7+: Heap, 이전: PermGen)으로, 문자열 리터럴을 캐싱합니다.

JVM 메모리:
┌──────────────────────────────────────────┐
│                  Heap                    │
│  ┌─────────────────────────────────┐     │
│  │         String Pool             │     │
│  │  "hello" ◄──── 리터럴 자동 등록 │     │
│  │  "world"                        │     │
│  └─────────────────────────────────┘     │
│                                          │
│  new String("hello") ← Pool 밖 새 객체  │
└──────────────────────────────────────────┘

리터럴 vs new String()

String a = "hello";              // Pool에서 가져옴
String b = "hello";              // Pool의 같은 객체
String c = new String("hello");  // Pool 밖 새 객체
String d = new String("hello");  // 또 다른 새 객체

System.out.println(a == b);  // true  (같은 Pool 객체)
System.out.println(a == c);  // false (다른 객체)
System.out.println(c == d);  // false (다른 객체)

System.out.println(a.equals(c));  // true (내용 동일)

intern() 메서드

String c = new String("hello");
String e = c.intern();  // Pool에서 "hello" 반환 (없으면 등록 후 반환)

System.out.println(a == e);  // true (Pool의 같은 객체)

컴파일 타임 상수 풀링

// 컴파일러가 자동으로 결합
String a = "hello";
String b = "hel" + "lo";  // 컴파일 타임에 "hello"로 결합
System.out.println(a == b);  // true

// 런타임 결합 — Pool 미적용
String prefix = "hel";
String c = prefix + "lo";  // 런타임 → new String
System.out.println(a == c);  // false

// final 변수는 컴파일 타임 상수
final String prefix2 = "hel";
String d = prefix2 + "lo";  // 컴파일 타임 → Pool 적용
System.out.println(a == d);  // true

3. String vs StringBuilder vs StringBuffer 성능 비교

특성 비교

항목 String StringBuilder StringBuffer
불변성 불변 가변 가변
스레드 안전 O (불변이므로) X O (synchronized)
성능 연결 시 느림 빠름 StringBuilder보다 느림
도입 버전 Java 1.0 Java 5 Java 1.0

성능 차이 원리

// String 연결 — 매번 새 객체 생성
String result = "";
for (int i = 0; i < 10000; i++) {
    result += i;  // 매 반복마다 새 String 생성 → O(n²)
}

// StringBuilder — 내부 버퍼 확장
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i);  // 버퍼에 추가 → O(n)
}
String result = sb.toString();
String 연결 (n번):
"" → "0" → "01" → "012" → ...
매번 새 배열 생성 + 복사 → O(1+2+3+...+n) = O(n²)

StringBuilder (n번):
[버퍼: ...........]
→ append → append → append
→ 필요 시 버퍼 2배 확장
→ O(n) amortized

컴파일러 최적화

// Java 컴파일러는 + 연산을 자동으로 StringBuilder로 변환
String a = "Hello" + " " + "World";
// 컴파일 후:
String a = new StringBuilder().append("Hello").append(" ").append("World").toString();

// 단, 반복문 내에서는 매번 새 StringBuilder 생성
for (int i = 0; i < n; i++) {
    result += i;
    // 컴파일 후: result = new StringBuilder(result).append(i).toString();
    // 여전히 O(n²)!
}

Java 9+ StringConcatFactory

// Java 9부터 invokedynamic 기반 최적화
// 단순 연결은 StringBuilder보다 빠를 수 있음
// 하지만 반복문 내 연결은 여전히 StringBuilder 권장

사용 가이드라인

// 1. 단순 리터럴 조합 → String 리터럴
String name = "Hello, " + userName + "!";

// 2. 반복문 내 연결 → StringBuilder
StringBuilder sb = new StringBuilder(capacity);
for (String s : list) {
    sb.append(s).append(", ");
}

// 3. 멀티스레드 공유 버퍼 → StringBuffer (드문 경우)
StringBuffer sharedBuffer = new StringBuffer();

4. 문자열 연결 성능 상세

벤치마크 비교 (JMH 기준)

연산 횟수: 100,000회
┌─────────────────────┬────────────────┐
│  방법               │  시간 (상대)   │
├─────────────────────┼────────────────┤
│ String + (반복)     │ 100x (기준)    │
│ concat() (반복)     │  80x           │
│ StringBuilder       │   1x (최적)    │
│ StringBuffer        │   1.2x         │
│ String.join()       │   1.5x (내부 SB│
└─────────────────────┴────────────────┘

String.join() / Collectors.joining()

// 구분자로 연결
String result = String.join(", ", "A", "B", "C");  // "A, B, C"

List<String> list = List.of("Apple", "Banana", "Cherry");
String joined = String.join(" | ", list);  // "Apple | Banana | Cherry"

// Stream에서
String csv = list.stream()
    .collect(Collectors.joining(", ", "[", "]"));
// "[Apple, Banana, Cherry]"

5. String 메서드 총정리

기본 메서드

String s = "Hello, World!";

// 길이 / 비어있는지
s.length()           // 13
s.isEmpty()          // false
s.isBlank()          // false (Java 11+, 공백만 있어도 true)

// 검색
s.charAt(0)          // 'H'
s.indexOf('o')       // 4 (첫 번째)
s.lastIndexOf('o')   // 8 (마지막)
s.contains("World")  // true
s.startsWith("Hello")// true
s.endsWith("!")      // true

// 추출
s.substring(7)       // "World!"
s.substring(7, 12)   // "World"

// 변환
s.toLowerCase()      // "hello, world!"
s.toUpperCase()      // "HELLO, WORLD!"
s.trim()             // 앞뒤 ASCII 공백 제거
s.strip()            // 앞뒤 유니코드 공백 제거 (Java 11+)
s.replace('l', 'r')  // "Herro, Worrd!"
s.replace("World", "Java")  // "Hello, Java!"

// 분리 / 결합
s.split(", ")        // ["Hello", "World!"]
String.join("-", "A", "B")  // "A-B"

// 변환
s.toCharArray()      // char[]
s.getBytes()         // byte[] (기본 charset)
String.valueOf(42)   // "42"

Java 11 추가 메서드

// isBlank() — 공백만 있으면 true
"   ".isBlank()   // true
"  a".isBlank()   // false

// strip() — 유니코드 공백 처리 (trim()보다 권장)
"\u2000Hello\u2000".strip()       // "Hello"
"\u2000Hello\u2000".trim()        // "\u2000Hello\u2000" (제거 안 됨)
"  Hello  ".stripLeading()        // "Hello  "
"  Hello  ".stripTrailing()       // "  Hello"

// lines() — 줄 단위 Stream
"line1\nline2\nline3".lines()
    .forEach(System.out::println);
// line1
// line2
// line3

// repeat() — 반복
"ab".repeat(3)  // "ababab"

Java 12 추가 메서드

// indent() — 들여쓰기 추가/제거
String text = "Hello\nWorld";
text.indent(4);
// "    Hello\n    World\n"

// transform() — 변환 함수 적용
String result = "  hello  "
    .transform(String::strip)
    .transform(String::toUpperCase);
// "HELLO"

Java 15 추가 메서드

// formatted() — String.format()의 인스턴스 버전
String msg = "Hello, %s! You are %d years old."
    .formatted("Alice", 30);
// "Hello, Alice! You are 30 years old."

Java 16 추가 메서드

// stripIndent() — 텍스트 블록 들여쓰기 제거
String html = "  <html>\n    <body>\n  </html>";
html.stripIndent();

// translateEscapes() — 이스케이프 시퀀스 해석
"Hello\\nWorld".translateEscapes();  // "Hello\nWorld"

6. 정규표현식과 String

기본 활용

String email = "user@example.com";

// matches() — 전체 문자열이 패턴과 일치?
email.matches("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}");  // true

// replaceAll() / replaceFirst()
String text = "Hello 123 World 456";
text.replaceAll("\\d+", "#");   // "Hello # World #"
text.replaceFirst("\\d+", "#"); // "Hello # World 456"

// split() 정규표현식
"a1b2c3".split("\\d");  // ["a", "b", "c"]

Pattern / Matcher (고성능)

import java.util.regex.*;

// 패턴 미리 컴파일 (반복 사용 시 필수)
Pattern pattern = Pattern.compile("\\d+");
Matcher matcher = pattern.matcher("abc123def456");

// 찾기
while (matcher.find()) {
    System.out.println(matcher.group());  // 123, 456
    System.out.println(matcher.start());  // 시작 인덱스
}

// 그룹 캡처
Pattern datePattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = datePattern.matcher("2026-05-01");
if (m.matches()) {
    String year  = m.group(1);  // "2026"
    String month = m.group(2);  // "05"
    String day   = m.group(3);  // "01"
}

성능 주의사항

// 나쁜 예: 반복 호출 시마다 Pattern 컴파일
for (String s : list) {
    if (s.matches("\\d+")) { ... }  // 매번 Pattern.compile() 호출!
}

// 좋은 예: 미리 컴파일
private static final Pattern DIGIT_PATTERN = Pattern.compile("\\d+");

for (String s : list) {
    if (DIGIT_PATTERN.matcher(s).matches()) { ... }  // 재사용
}

7. 텍스트 블록 (Text Block, Java 13 Preview → Java 15 정식)

기본 문법

// 기존 방식 — 가독성 나쁨
String json = "{\n" +
              "  \"name\": \"Alice\",\n" +
              "  \"age\": 30\n" +
              "}";

// 텍스트 블록 — 훨씬 읽기 쉬움
String json = """
        {
          "name": "Alice",
          "age": 30
        }
        """;

들여쓰기 규칙

// 닫는 """ 위치가 들여쓰기 기준점
String text = """
        Hello
        World
        """;
// → "Hello\nWorld\n"  (공통 들여쓰기 8칸 제거)

String text2 = """
        Hello
        World
""";  // 닫는 """가 컬럼 0에 위치
// → "        Hello\n        World\n"  (들여쓰기 유지)

실용 예제

// SQL
String sql = """
        SELECT u.id, u.name, o.total
        FROM users u
        JOIN orders o ON u.id = o.user_id
        WHERE u.active = true
          AND o.total > :minAmount
        ORDER BY o.total DESC
        """;

// HTML
String html = """
        <html>
            <body>
                <p>Hello, %s!</p>
            </body>
        </html>
        """.formatted(name);

// JSON
String json = """
        {
            "status": "ok",
            "message": "%s"
        }
        """.formatted(message);

이스케이프 처리

// \n 줄 바꿈 억제 (긴 줄 가독성 유지)
String oneLine = """
        This is a very long line \
        that continues here.
        """;
// → "This is a very long line that continues here.\n"

// \s — 공백 유지 (후행 공백 보존)
String padded = """
        one  \s
        two  \s
        """;

8. String 관련 주요 패턴

문자열 → 기본형 변환

int    i = Integer.parseInt("42");
double d = Double.parseDouble("3.14");
long   l = Long.parseLong("1234567890");
boolean b = Boolean.parseBoolean("true");

// 안전한 파싱
try {
    int val = Integer.parseInt(input);
} catch (NumberFormatException e) {
    // 처리
}

기본형 → 문자열

String s1 = String.valueOf(42);      // "42"
String s2 = Integer.toString(42);    // "42"
String s3 = "" + 42;                 // "42" (권장하지 않음)

문자열 비교 함정

// == 절대 사용 금지 (Pool 외부 객체)
String a = new String("hello");
String b = new String("hello");
a == b           // false
a.equals(b)      // true ← 항상 이것을 사용

// 대소문자 무시
a.equalsIgnoreCase("HELLO");  // true

// null 안전 비교 (NPE 방지)
"hello".equals(userInput);    // 리터럴을 앞에
Objects.equals(a, b);         // 둘 다 null-safe

9. 전체 요약

String 핵심 정리:
┌────────────────────────────────────────────────────────┐
│  불변성: 모든 메서드는 새 String 반환                  │
│  Pool:   리터럴은 자동으로 Pool에 등록, == 비교 주의   │
│  연결:   반복문에서 + 금지 → StringBuilder 사용        │
│  비교:   항상 equals(), == 절대 금지                   │
│  Java11: strip(), isBlank(), lines(), repeat() 활용    │
│  Java15: 텍스트 블록 """ 적극 활용                    │
└────────────────────────────────────────────────────────┘

선택 가이드:
단순 조합     → String 리터럴 + 텍스트 블록
반복 연결     → StringBuilder
스레드 공유   → StringBuffer (드문 경우)
패턴 검색     → Pattern.compile() 미리 컴파일

카테고리:

업데이트: