Java 17 기준으로 String의 불변성 이유와 성능 포인트, ==/equals 비교 함정, StringBuilder 사용 기준, Text Block 활용법을 실무 관점에서 정리합니다.
로그 한 줄 만들려고 문자열을 +로 계속 붙였는데, 트래픽이 오르자 CPU가 튀고 GC가 바빠지는 경험을 하실 때가 있습니다. 또 어떤 환경에서는 "a" == new String("a")가 false라서 디버깅이 길어지기도 해요. Java의 String은 “그냥 문자”가 아니라, 성능과 버그를 동시에 좌우하는 핵심 타입입니다.
핵심 개념 (Java String 불변성이 중요한 이유)

String은 왜 불변(Immutable)일까요?
Java의 String은 한 번 만들어지면 내용이 바뀌지 않습니다. 이 설계 덕분에 다음 이점이 생깁니다.
- 안전성:
String은 비밀번호, 토큰, 파일 경로처럼 보안/권한과 연결되는 값에 자주 쓰입니다. 불변이면 누군가 참조를 공유하더라도 값이 뒤에서 바뀌지 않아 안전합니다. - 캐싱/재사용: 문자열 리터럴은 String Pool에 들어가 재사용됩니다. 같은
"hello"리터럴은 여러 곳에서 같은 인스턴스를 가리킬 수 있어 메모리 효율이 좋아요. - 해시 안정성:
String은HashMap의 키로 매우 자주 쓰입니다. 불변이면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 배열과 컬렉션 프레임워크 입문]
'JAVA' 카테고리의 다른 글
| Java ArrayList vs LinkedList — 진짜 차이(내부 구조·성능·실무 선택 기준) (0) | 2026.02.18 |
|---|---|
| Java 배열과 컬렉션 프레임워크 입문 — Array에서 List, Set, Map까지 한 번에 잡기 (0) | 2026.02.18 |
| Java 예외 처리 제대로 하기: Checked vs Unchecked부터 실무 전략까지 (0) | 2026.02.17 |
| Java 블로그 로드맵 — Java 17 기준 40편 커리큘럼 한눈에 보기 (0) | 2026.02.16 |
| Java 인터페이스 vs 추상 클래스 실전 구분법 (default 메서드, 다중 구현 패턴까지) (0) | 2026.02.16 |