이번 글은 Java에서 가장 많이 사용하는 클래스 중 하나이지만,
실제로 내부 동작을 정확히 아는 사람은 많지 않다.
면접에서도 자주 나오는 질문이다.
"StringBuilder와 StringBuffer의 차이가 synchronized 말고 또 있나요?"
또는
"String a = a + b는 실제로 어떻게 동작하나요?"
이번 글에서는 이 질문에 답할 수 있도록 내부 원리까지 설명한다.
StringBuilder vs StringBuffer
둘 다 문자열을 변경(Mutable)하기 위한 클래스이며, 가장 큰 차이는 Thread-safe 여부이다.
하지만 이것만 알고 끝내면 절반만 이해한 것이다.
먼저 String부터 이해해야 한다.
String은 immutable(불변 객체)이다.
String str = "Hello";
str += " World";
많은 사람들이
Hello
↓
Hello World
로 문자열이 수정되었다고 생각한다.
하지만 실제로는 아니다.
실제 메모리에서는
String str = "Hello";
Heap(String Pool)
"Hello"
▲
│
str
그리고
str += " World";
가 실행되면
Heap(String Pool)
"Hello"
"Hello World"
▲
│
str
기존 문자열은 그대로 있고,
새로운 String 객체가 생성된다.
String은 왜 immutable일까?
이유는 매우 많다.
String Pool 재사용
HashMap Key 안정성
Thread-safe
보안
등의 이유 때문이다.
하지만 문제가 있다.
반복문에서 String +
String result = "";
for (int i = 0; i < 3; i++) {
result += i;
}
많은 사람들이
result
↓
0
↓
01
↓
012
한 객체가 계속 수정된다고 생각한다.
실제로는
""
↓
"0"
↓
"01"
↓
"012"
객체가 매번 새로 생성된다.
메모리
Heap
""
"0"
"01"
"012"
총 4개의 객체가 만들어진다.
1000번 반복하면?
1000개의 String 객체 생성
↓
GC 대상 증가
↓
성능 저하
그래서 StringBuilder가 등장했다.
StringBuilder는 immutable이 아니라 mutable이다.
예제
StringBuilder sb =
new StringBuilder();
sb.append("Hello");
sb.append(" World");
메모리
StringBuilder
----------------
char[]
H
e
l
l
o
W
o
r
l
d
----------------
객체 하나를 계속 수정한다.
새 객체를 만들지 않는다.
append()는 어떻게 동작할까?
sb.append("ABC");
실제로는
기존 char[]
↓
뒤에 문자 복사
↓
끝
이다.
새로운 String을 생성하지 않는다.
내부 구조
StringBuilder 내부는 거의
class StringBuilder {
char[] value;
int count;
}
형태이다.
예를 들어
StringBuilder sb =
new StringBuilder();
생성하면
char[]
--------------------
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
capacity = 16
count = 0
--------------------
append
sb.append("ABC");
결과
char[]
--------------------
A B C _ _ _ _ _ _ _ _ _ _ _ _ _
count = 3
--------------------
append
sb.append("DEF");
결과
char[]
--------------------
A B C D E F _ _ _ _ _ _ _ _ _ _
count = 6
--------------------
공간이 부족하면?
예를 들어
capacity가
16
인데
20글자를 넣으면
새로운 배열을 만든다.
기존
16
↓
새 배열 생성
↓
34
↓
기존 내용 복사
↓
append
실제로 JDK는
newCapacity
=
(oldCapacity * 2) + 2
방식으로 증가시킨다.
StringBuffer는?
거의 동일하다.
내부 구조도 비슷하다.
char[]
count
append()
insert()
모두 동일하다.
단 하나의 차이가 있다.
synchronized
StringBuffer
public synchronized
StringBuffer append(String str)
StringBuilder
public
StringBuilder append(String str)
StringBuffer는
모든 메서드가 synchronized이다.
synchronized가 무엇일까?
Thread1
↓
append()
↓
Lock 획득
↓
작업
↓
Lock 해제
↓
Thread2 수행
즉,
동시에 두 Thread가
append를 수행하지 못한다.
StringBuilder는
Thread1
↓
append()
Thread2
↓
append()
동시 실행 가능
이다.
그래서
Lock 없음
↓
더 빠름
이다.
성능 비교
String +
객체 계속 생성
↓
가장 느림
StringBuffer
Mutable
+
Lock
↓
중간
StringBuilder
Mutable
+
Lock 없음
↓
가장 빠름
그런데 + 연산도 빠르던데?
좋은 질문이다.
컴파일러는
String result =
"Hello" + " World";
를
컴파일 시
"Hello World"
로 바꾼다.
이를
Constant Folding
이라고 한다.
또
String result =
a + b;
는
컴파일러가
String result =
new StringBuilder()
.append(a)
.append(b)
.toString();
로 변환한다.
즉,
우리가
a + b
를 작성해도
내부적으로는
StringBuilder
↓
append()
↓
toString()
이 수행된다.
그럼 항상 +를 써도 되는가?
아니다.
한 번만 연결
String result =
firstName + lastName;
괜찮다.
컴파일러가 최적화한다.
반복문
String result = "";
for (...) {
result += value;
}
매 반복마다
StringBuilder 생성
↓
append
↓
toString
↓
새 String 생성
이 반복된다.
그래서 반복문에서는
반드시
StringBuilder sb =
new StringBuilder();
for (...) {
sb.append(value);
}
return sb.toString();
를 사용하는 것이 좋다.
실무 기준
일반 문자열
String name =
first + last;
그냥 String 사용.
반복문
StringBuilder sb =
new StringBuilder();
권장.
멀티스레드 공유 객체
StringBuffer
가능하지만,
실무에서는 거의
StringBuilder
+
외부 Lock
또는
Concurrent 자료구조
를 사용한다.
StringBuffer를 직접 사용하는 경우는 많지 않다.
그림으로 정리
String
append
↓
새 String 생성
↓
기존 객체 유지
StringBuilder
append
↓
기존 char[] 수정
↓
객체 그대로
StringBuffer
append
↓
Lock 획득
↓
기존 char[] 수정
↓
Lock 해제
면접 포인트
StringBuilder와 StringBuffer의 차이
| Mutable | Mutable |
| synchronized 없음 | synchronized 있음 |
| Thread-safe 아님 | Thread-safe |
| 더 빠름 | 상대적으로 느림 |
String + 는 내부적으로 어떻게 동작하는가?
컴파일러가
a + b
를
new StringBuilder()
.append(a)
.append(b)
.toString()
형태로 변환한다.
반복문에서 StringBuilder를 사용하는 이유는?
String은 immutable이므로 +를 반복하면 매번 새로운 객체가 생성된다.
StringBuilder는 하나의 내부 배열(char 배열 또는 최신 JDK에서는 byte 배열 기반 구현)을 재사용하면서 내용을 변경하므로 객체 생성과 GC 부담을 크게 줄일 수 있다.
다음 글 추천
다음은 BigDecimal을 사용하는 이유이다.
이 글에서는
System.out.println(0.1 + 0.2);
의 결과가
0.30000000000000004
가 되는 이유부터,
- IEEE 754 부동소수점
- 금융 시스템에서 BigDecimal을 사용하는 이유
- new BigDecimal("0.1")과 new BigDecimal(0.1)의 차이
- equals()와 compareTo()의 함정
까지 실무 중심으로 이어서 설명할 수 있다.
'language > java' 카테고리의 다른 글
| ThreadLocal 원리 (0) | 2026.06.16 |
|---|---|
| BigDecimal을 사용하는 이유 (0) | 2026.06.16 |
| Integer Cache (0) | 2026.06.16 |
| AutoBoxing / Unboxing (0) | 2026.06.16 |
| Serialization / Deserialization (0) | 2026.06.16 |
댓글