문자열 변환과 StringBuilder
문자열 반환값 누락을 고치고 값 변환·서식화·반복 결합에서 String과 StringBuilder를 선택하는 기준을 실행으로 확인합니다.
String은 불변이므로 결합과 변환마다 결과 값을 반환합니다. 짧은 표현식은 컴파일러가 효율적으로 최적화하지만, 반복문에서 누적 결합하면 중간 문자열과 복사가 많이 생길 수 있습니다. StringBuilder는 한 작업 범위 안에서 가변 버퍼를 사용한 뒤 마지막에 String으로 확정합니다.
반환값 누락과 문자열 불변성
불변 메서드를 가변 setter처럼 호출하면 변화가 없습니다. 다음 프로그램은 접두어를 붙였다고 생각하지만 반환된 새 문자열을 저장하지 않았습니다.
public final class IgnoredStringResultBug {
public static void main(String[] args) {
String title = "Object";
title.concat(" / String");
title.toUpperCase();
System.out.println("title=" + title);
}
}title=Objecttitle = title.concat(...)처럼 결과를 다시 대입하거나 새 변수에 받습니다.
메서드 체인으로 여러 변환을 연결할 때도 마지막 결과를 저장해야 합니다.
원본 보존이 목적이면 expanded 같은 별도 이름이 의도를 드러냅니다.
문자열 변환과 서식
String.valueOf는 기본형과 객체를 문자열로 바꿉니다.
객체는 문자 표현 계약을 사용하고 null은 문자열 "null"이 됩니다.
formatted나 String.format은 자리표시자에 맞춰 숫자 정밀도와 배치를 표현합니다.
public final class StudySummaryFormatting {
public static void main(String[] args) {
int minutes = 95;
boolean completed = true;
double hours = minutes / 60.0;
String rawMinutes = String.valueOf(minutes);
String summary = "topic=%s, minutes=%s, hours=%.2f, done=%b"
.formatted("string", rawMinutes, hours, completed);
System.out.println(summary);
}
}topic=string, minutes=95, hours=1.58, done=true서식 문자열과 인수 타입이 맞지 않으면 실행 중 예외가 발생할 수 있습니다. 고정된 내부 로그에는 편리하지만 사용자 번역 문구와 지역별 숫자 표기에는 Locale을 함께 고려합니다. 구조화 로그나 데이터 교환은 문자열 하나보다 전용 필드나 직렬화 도구를 사용합니다.
문자열 결합의 처리 과정
상수 리터럴끼리의 결합은 컴파일 시 하나로 합쳐질 수 있습니다. 변수 몇 개를 잇는 짧은 표현식도 컴파일러가 내부 결합 코드를 효율적으로 만들 수 있으므로 무조건 StringBuilder로 바꿀 필요는 없습니다.
public final class ReadableStringComposition {
public static void main(String[] args) {
String topic = "string";
int minutes = 45;
String constant = "Study" + " Log";
String line = constant + ": " + topic + "=" + minutes;
System.out.println(constant);
System.out.println(line);
}
}Study Log
Study Log: string=45한 줄짜리 메시지는 +가 가장 읽기 쉽습니다.
성능을 추측해 수동 builder 코드를 늘리기보다 반복 횟수, 데이터 크기, 호출 빈도를 측정합니다.
문자열 템플릿 생성이 병목으로 확인되면 그 좁은 범위만 개선합니다.
StringBuilder 가변 버퍼
루프에서 result = result + item을 반복하면 매번 지금까지의 내용을 복사한 새 String이 필요합니다.
StringBuilder는 내부 버퍼에 append하고 한 번만 최종 문자열을 만듭니다.
public final class StudyLogStringBuilder {
public static void main(String[] args) {
StudyEntry[] entries = {
new StudyEntry("object", 40),
new StudyEntry("immutable", 50),
new StudyEntry("string", 45)
};
StringBuilder builder = new StringBuilder("Study Log");
for (StudyEntry entry : entries) {
builder.append('\n')
.append("- ")
.append(entry.topic())
.append(": ")
.append(entry.minutes())
.append('m');
}
String report = builder.toString();
System.out.println(report);
}
private record StudyEntry(String topic, int minutes) { }
}Study Log
- object: 40m
- immutable: 50m
- string: 45mStringBuilder는 가변이므로 여러 스레드나 장기 필드에서 공유하지 않고 한 메서드 안의 조립 도구로 사용하는 것이 안전합니다.
toString()으로 결과를 확정한 뒤 외부에는 불변 String만 반환합니다.
용량과 재사용 및 동시성
예상 결과가 매우 크다면 생성자에 초기 용량을 줄 수 있지만 실제 측정 없이 정확한 수치를 맞추는 데 집착할 필요는 없습니다. builder는 용량이 부족하면 자동 확장됩니다.
한 요청에서 만든 builder를 static 필드로 재사용하면 이전 내용이 섞이거나 동시 호출이 충돌합니다.
setLength(0)으로 비우는 방식도 예외 경로에서 초기화를 놓치기 쉽습니다.
짧게 생성하고 버리는 객체의 비용은 JVM이 잘 최적화하므로 명확한 지역 소유권을 우선합니다.
StringBuffer는 메서드 동기화를 제공하지만 현대 코드에서는 외부 동시성 설계와 지역 StringBuilder가 일반적입니다. 여러 스레드가 하나의 텍스트 결과를 협력해 만든다면 각자 부분 문자열을 만들고 안전한 경계에서 결합하는 구조가 더 단순할 수 있습니다.
성능 비교를 해석할 때 지킬 조건
한 번 실행한 currentTimeMillis 차이만으로 API 우열을 결론 내리지 않습니다.
JVM은 시작 직후 클래스 로딩과 JIT 컴파일을 수행하고, 사용되지 않는 결과는 최적화될 수 있습니다.
입력 크기와 반복 횟수를 실제 서비스와 비슷하게 잡고 결과를 소비하며 충분히 예열한 벤치마크가 필요합니다.
전문 측정에는 JMH 같은 도구가 적합합니다.
대부분의 학습 기록 화면에서 항목 수십 개를 조립하는 비용은 데이터베이스나 네트워크 시간보다 작습니다. 성능 수치가 사용자 지연에 영향을 주는지 프로파일링하고, 개선 뒤에도 결과 문자열이 같은지 확인합니다. StringBuilder 선택은 “항상 빠르다”는 주문이 아니라 반복 누적이라는 구조적 신호와 측정 근거를 함께 사용합니다.
문자열 누적 방식의 선택
- 리터럴·변수 몇 개의 짧은 결합:
+또는formatted - 반복문에서 항목 수가 늘어나는 누적: StringBuilder
- 구분자로 목록 결합:
String.join또는 스트림 joining - 읽기 전용 API 반환: String
- 문자열 조립 중간 상태: 메서드 지역 StringBuilder
- 지속적 문서 편집: 전용 자료구조나 편집 모델 검토
StringBuilder를 반환해 호출자가 계속 수정하게 하면 내부 표현과 수명 관리가 새어 나갑니다. 필요한 결과가 문자 값이면 String으로 닫습니다.
연습 문제
태그 배열을 [java | object | string] 형식으로 만드세요.
첫 항목 앞과 마지막 항목 뒤에 구분자가 생기지 않아야 하며 빈 배열은 []을 반환합니다.
해설 보기
public final class TagBuilderExercise {
public static void main(String[] args) {
System.out.println(formatTags(new String[]{"java", "object", "string"}));
System.out.println(formatTags(new String[]{}));
}
private static String formatTags(String[] tags) {
StringBuilder result = new StringBuilder("[");
for (int index = 0; index < tags.length; index++) {
if (index > 0) result.append(" | ");
result.append(tags[index]);
}
return result.append(']').toString();
}
}[java | object | string]
[]인덱스가 0보다 클 때만 구분자를 먼저 붙이는 방식은 끝 구분자를 제거하는 후처리를 피합니다.
이 요구만 있다면 "[" + String.join(" | ", tags) + "]"도 더 간단한 해답입니다.