JAVA

Java String 완전 정복: 불변성, StringBuilder, 비교 함정, Text Block까지

IT Lab 2026. 2. 17. 20:59

Java 17 기준으로 String의 불변성 이유와 성능 포인트, ==/equals 비교 함정, StringBuilder 사용 기준, Text Block 활용법을 실무 관점에서 정리합니다.

 

로그 한 줄 만들려고 문자열을 +로 계속 붙였는데, 트래픽이 오르자 CPU가 튀고 GC가 바빠지는 경험을 하실 때가 있습니다. 또 어떤 환경에서는 "a" == new String("a")가 false라서 디버깅이 길어지기도 해요. Java의 String은 “그냥 문자”가 아니라, 성능과 버그를 동시에 좌우하는 핵심 타입입니다.

핵심 개념 (Java String 불변성이 중요한 이유)

Java String Pool과 new String()의 객체 생성 차이를 보여주는 다이어그램

String은 왜 불변(Immutable)일까요?

Java의 String은 한 번 만들어지면 내용이 바뀌지 않습니다. 이 설계 덕분에 다음 이점이 생깁니다.

  • 안전성: String은 비밀번호, 토큰, 파일 경로처럼 보안/권한과 연결되는 값에 자주 쓰입니다. 불변이면 누군가 참조를 공유하더라도 값이 뒤에서 바뀌지 않아 안전합니다.
  • 캐싱/재사용: 문자열 리터럴은 String Pool에 들어가 재사용됩니다. 같은 "hello" 리터럴은 여러 곳에서 같은 인스턴스를 가리킬 수 있어 메모리 효율이 좋아요.
  • 해시 안정성: StringHashMap의 키로 매우 자주 쓰입니다. 불변이면 hashCode()가 바뀌지 않아 자료구조가 깨지지 않습니다.
flowchart LR
  A["String literal"] --> B["String Pool"]
  C["new String()"] --> D["Heap object"]
  B --> E["Shared reference"]
  D --> F["Distinct reference"]

String 리터럴은 풀에서 공유되지만, new String()은 별도 객체가 됩니다.

문자열 덧붙이기: + vs StringBuilder 선택 기준

문자열 결합은 “조립” 작업입니다. 불변인 String+로 붙이면 매번 새 객체가 만들어질 수 있어요(컴파일러 최적화가 있어도 루프 안에서는 비용이 커지기 쉽습니다).

  • 상수 결합: "a" + "b"처럼 컴파일 타임에 결정되면 최적화되어 하나의 리터럴처럼 처리될 수 있습니다.
  • 반복/루프 결합: 루프에서 result += x는 누적될수록 많은 임시 객체를 만들 가능성이 큽니다 → StringBuilder가 정답인 경우가 많습니다.
  • 멀티스레드 공유: StringBuffer는 동기화(synchronized)로 스레드 안전하지만, 단일 스레드에서는 보통 StringBuilder가 더 적합합니다.

아래 표처럼 “언제 무엇을 쓰는지”만 명확히 잡아도 성능 이슈가 크게 줄어듭니다.

상황 권장 API 이유/특징
상수 문자열 결합 + 컴파일러 최적화 가능
루프에서 반복 결합 StringBuilder 임시 객체/GC 부담 감소
여러 스레드에서 같은 버퍼 공유 StringBuffer 동기화로 안전(대신 느릴 수 있음)
포맷팅이 핵심(가독성) String.format / Formatter 편하지만 상대적으로 느릴 수 있음

문자열 비교의 대표 함정: == vs equals

  • ==참조(주소) 비교입니다.
  • equals()내용(문자열 값) 비교입니다.

String Pool 때문에 "a" == "a"가 true인 경우가 있어 “가끔은 동작하는 것처럼” 보이는 게 함정입니다. 특히 외부 입력(HTTP 파라미터, DB 조회, JSON 파싱)으로 들어온 문자열은 풀과 무관한 경우가 많아 ==는 불안정합니다.

추가로 실무에서 자주 쓰는 패턴:

  • someString.equals("X")보다 **"X".equals(someString)**가 NPE를 피하기 쉬워요.
  • 대소문자 무시 비교는 equalsIgnoreCase()를 쓰되, 로케일 규칙이 중요한 도메인(예: 터키어 i 문제)이라면 더 신중해야 합니다.

Java 17의 Text Block: 긴 문자열/JSON/SQL을 “보기 좋게”

Text Block(""")은 여러 줄 문자열을 자연스럽게 표현합니다. JSON, SQL, HTML 템플릿처럼 줄바꿈과 들여쓰기가 중요한 문자열에서 특히 유용합니다.

  • 기존 "\n"+ 지옥을 줄여 가독성이 좋아집니다.
  • 줄 끝 개행, 들여쓰기 처리 규칙이 있으니 실제 출력 결과를 한 번 확인하는 습관이 좋습니다.
  • 문자열 템플릿(String Templates, JDK 21)은 프리뷰/변동 요소가 있어(버전 정책에 따라) 여기서는 Text Block 중심으로 다룹니다.

코드 예제 (복붙해서 바로 실행)

아래 코드는 String 불변성으로 인한 성능 차이, 문자열 비교 함정, Text Block 사용을 한 번에 확인할 수 있습니다.

import java.util.Objects;

public class StringMasteryDemo {

    public static void main(String[] args) {
        immutabilityAndConcatCost();
        stringComparisonPitfalls();
        textBlockExample();
    }

    private static void immutabilityAndConcatCost() {
        int n = 50_000;

        long t1 = System.currentTimeMillis();
        String s = "";
        for (int i = 0; i < n; i++) {
            s += i; // 루프에서 + 누적: 임시 String이 많이 생길 수 있음
        }
        long t2 = System.currentTimeMillis();

        long t3 = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder(n * 2); // 대략적인 용량 힌트(과하지 않게)
        for (int i = 0; i < n; i++) {
            sb.append(i);
        }
        String built = sb.toString();
        long t4 = System.currentTimeMillis();

        System.out.println("[concat] length=" + s.length() + ", ms=" + (t2 - t1));
        System.out.println("[builder] length=" + built.length() + ", ms=" + (t4 - t3));
    }

    private static void stringComparisonPitfalls() {
        String a = "hello";
        String b = "hello";
        String c = new String("hello");

        System.out.println("a == b : " + (a == b));       // 보통 true (풀 공유)
        System.out.println("a == c : " + (a == c));       // false (서로 다른 객체)
        System.out.println("a.equals(c) : " + a.equals(c)); // true (내용 비교)

        String nullable = null;

        // NPE 방지: 상수.equals(변수) 패턴
        System.out.println("\"OK\".equals(nullable) : " + "OK".equals(nullable));

        // null-safe 내용 비교가 필요하면 Objects.equals 사용
        System.out.println("Objects.equals(nullable, \"OK\") : " + Objects.equals(nullable, "OK"));
    }

    private static void textBlockExample() {
        String userId = "u-100";

        // Text Block: JSON/SQL 같은 멀티라인 문자열에 유리
        String json = """
                {
                  "userId": "%s",
                  "active": true,
                  "roles": ["ADMIN", "USER"]
                }
                """.formatted(userId); // Java 15+ (17에서 안정적으로 사용)

        System.out.println("[TextBlock JSON]");
        System.out.println(json);

        String sql = """
                SELECT id, name, created_at
                FROM users
                WHERE id = ?
                ORDER BY created_at DESC
                """;

        System.out.println("[TextBlock SQL]");
        System.out.println(sql);
    }
}

실무 팁

💡 실무에서는
루프에서 문자열을 붙여야 한다면 StringBuilder를 기본으로 두고, 대략적인 용량을 추정new StringBuilder(capacity)를 주는 편이 좋습니다. 특히 로그/리포트/CSV 생성처럼 길이가 커지는 작업에서 리사이징 비용이 눈에 띄게 줄어듭니다.

💡 실무에서는
문자열 비교는 규칙을 팀 차원에서 고정해 두면 버그가 크게 줄어듭니다. 예를 들어 “내용 비교는 무조건 equals/Objects.equals만 허용, ==는 금지(리터럴 비교도 금지)”처럼 정하시면, String Pool로 인해 테스트에서는 통과하지만 운영에서 깨지는 유형을 예방하기 쉽습니다.

핵심 요약

String은 불변이라 안전하고 빠를 수 있지만, 반복 결합은 StringBuilder가 유리합니다.
문자열 비교는 ==가 아니라 equals(또는 Objects.equals)로 내용 비교를 하세요.
Text Block은 JSON/SQL 같은 멀티라인 문자열 가독성을 크게 올려줍니다.

다음 글: [#12 배열과 컬렉션 프레임워크 입문]