String, StringBuilder, StringBuffer 차이

한눈에 보기

String은 불변 문자열이고 StringBuilder와 StringBuffer는 버퍼를 수정하는 가변 클래스다. 불변 설계의 이유, 루프 연결이 느린 원리, 동기화 유무에 따른 선택 기준을 정리한다.

최종 업데이트 2026.09.17

String은 한 번 만들어지면 내용을 바꿀 수 없는 불변(immutable) 문자열 클래스이고, StringBuilder와 StringBuffer는 내부 버퍼를 수정하며 문자열을 조립하는 가변(mutable) 클래스다. StringBuilder와 StringBuffer는 API가 사실상 같지만 StringBuffer는 모든 메서드가 synchronized로 동기화되어 있고 StringBuilder는 동기화가 없어 단일 스레드에서 더 빠르다. 문자열을 반복적으로 이어 붙이는 상황에서 셋 중 무엇을 쓰느냐가 성능과 스레드 안전성을 좌우한다.

String의 불변성과 그 이유

String의 내부 문자 데이터는 final 필드로 보관되며 수정 API가 아예 없다. concat이나 replace, toUpperCase 같은 메서드는 모두 원본을 바꾸는 것이 아니라 새 String 객체를 만들어 반환한다. 불변으로 설계된 이유는 여러 가지다. 문자열 풀(String Pool)에서 여러 참조가 하나의 객체를 안전하게 공유하려면 내용이 바뀌지 않아야 하고, 해시코드를 한 번 계산해 캐싱할 수 있어 HashMap의 키로 효율적이며, 여러 스레드가 동기화 없이 공유해도 안전하다. 클래스 이름이나 파일 경로 같은 보안 민감 값이 검증 후에 몰래 바뀌는 일을 막는 효과도 있다.

문자열 연결이 느려지는 원리

불변이기 때문에 String을 + 연산으로 이어 붙일 때마다 새 객체가 생긴다. 특히 반복문 안의 연결이 문제다. 매 반복마다 지금까지의 내용 전체를 새 버퍼로 복사하므로 시간 복잡도가 O(n^2)로 치솟고, 버려지는 중간 객체가 가비지 컬렉션(Garbage Collection) 부담을 키운다.

// 나쁜 예: 반복마다 전체 복사가 일어난다
String result = "";
for (int i = 0; i < 10_000; i++) {
    result += i;   // 매번 새 String 생성
}

// 좋은 예: 하나의 버퍼에 이어 붙인다
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
    sb.append(i);
}
String result2 = sb.toString();

참고로 한 문장 안에서의 연결, 예컨대 "a" + b + "c"는 걱정할 필요가 없다. Java 8까지는 컴파일러가 StringBuilder 코드로 치환했고, Java 9부터는 invokedynamic 기반의 StringConcatFactory가 더 최적화된 연결 코드를 실행 시점에 만들어 낸다. 문제가 되는 것은 어디까지나 루프를 돌며 String 변수에 누적하는 패턴이다.

StringBuilder와 StringBuffer의 내부 동작

두 클래스는 AbstractStringBuilder를 공통 부모로 두며, 내부에 가변 byte 배열과 현재 길이를 유지한다. append 호출 시 배열에 여유가 있으면 뒤에 이어 쓰고, 부족하면 대략 두 배 크기의 새 배열을 할당해 복사하는 동적 배열 방식으로 동작한다. 그래서 최종 길이를 예측할 수 있다면 new StringBuilder(예상크기)처럼 초기 용량을 지정해 재할당 횟수를 줄이는 것이 좋다. toString()을 호출하는 시점에 버퍼 내용을 복사한 불변 String이 만들어진다.

StringBuffer는 Java 1.0부터 있던 클래스로 모든 공개 메서드에 synchronized가 붙어 있다. 단일 스레드에서는 이 동기화가 순수한 낭비이기 때문에 Java 5에서 동기화를 뺀 StringBuilder가 추가되었고, 이후 새 코드에서는 StringBuilder가 표준 선택이 되었다. 사실 여러 스레드가 문자열 하나를 함께 조립하는 설계 자체가 드물고, 필요하더라도 메서드 단위 동기화만으로는 호출 순서까지 보장되지 않으므로 StringBuffer가 실질적 해법이 되는 경우는 거의 없다.

세 클래스 비교

구분StringStringBuilderStringBuffer
가변성불변가변가변
스레드 안전성안전 (불변이므로)비안전메서드 단위 동기화
연결 성능루프 누적 시 O(n^2)빠름동기화 비용만큼 느림
도입 시기Java 1.0Java 5Java 1.0
적합한 상황변하지 않는 문자열, Map의 키단일 스레드 문자열 조립레거시 코드, 드문 공유 조립

선택 기준 정리

기본값은 String이다. 값이 바뀌지 않는 대부분의 문자열은 불변의 안전함과 풀 공유의 이점을 그대로 누리면 된다. 루프나 조건 분기를 거치며 문자열을 조립해야 할 때만 StringBuilder를 꺼내 쓰고, 완성되면 즉시 toString()으로 불변 String으로 되돌린다. StringBuffer는 기존 코드를 유지보수할 때 만나는 정도로 이해하면 충분하다. 여러 값을 구분자로 잇는 단순한 경우라면 String.join이나 스트림의 Collectors.joining이 더 읽기 좋은 대안이 된다.

면접에서 이렇게 물어본다

  • String이 불변으로 설계된 이유를 세 가지 이상 설명해 보라.
  • 반복문에서 String을 + 로 누적하면 왜 느린지, 시간 복잡도와 GC 관점에서 설명해 보라.
  • StringBuilder와 StringBuffer의 차이와 각각을 선택하는 기준은 무엇인가.
  • StringBuilder의 내부 버퍼가 어떻게 확장되는지, 초기 용량 지정이 왜 유용한지 설명해 보라.
  • String의 + 연산이 컴파일 시 어떻게 처리되는지 Java 9 전후로 비교해 보라.
Java 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기