1. 자바에서 +연산으로 문자열 연결
Java String 클래스는 + 연산으로 쉽게 두개의 스트링을 잇는 것이 가능하다.
public static void main(String[] args) throws Exception {
String a = "Hello, ";
String b = "World!";
a = a + b;
System.out.println(a); // 출력결과 : Hello, World!
}
정말 편리하게 사용하고 있는 기능이다. 그런데, String 은 불변 객체라고 알고 있기에, 내부적으로 어떻게 동작하는지 궁금해진다. 객체를 새로 만들어서 두개 스트링을 복사해서 붙여넣는 것일까? 그렇다면 이 과정은 비효율적이지는 않을까? 라고 생각해 볼 수 있다.
eclispe나 vscode에서 브레이크 포인트를 찍어서 실제로 어떤 과정이 이루어지는지 살펴보자.
2. +연산의 내부 동작 과정 살펴보기
a = a + b; 에 브레이크 포인트를 찍고, 동작과정을 살펴봤다.
1) StringBuilder 생성자 호출
//StringBuilder.class
@HotSpotIntrinsicCandidate
public StringBuilder(String str) {
super(str); //str에는 "Hello, "가 들어가 있다.
}
"Hello, " 문자열이 StringBuilder 클래스의 instance로 생성이 된다. StringBuilder는 AbstractStringBuilder를 상속받고 있고, super(str)에서 AbstractStringBuilder의 생성자를 호출한다.
2) AbstractStringBuilder 생성자 호출
AbstractStringBuilder(String str) {
int length = str.length();
int capacity = (length < Integer.MAX_VALUE - 16)
? length + 16 : Integer.MAX_VALUE;
final byte initCoder = str.coder();
coder = initCoder;
//value에 byte의 배열이 저장된다. 실제로 character가 저장되는 부분
value = (initCoder == LATIN1)
? new byte[capacity] : StringUTF16.newBytesFor(capacity);
//append 함수로 str을 value에 복사해준다.
append(str);
}
value는 문자열을 저장하고 있는 배열이다. str의 길이를 체크하여 초기 capacity는 str.length() + 16으로 value의 배열을 만들어준다. 여기서는 "Hello, " 가 공백포함 길이가 7이므로 capacity는 7 + 16 = 23으로 계산된다. 이후 value에 str을 넣어주기 위해 append 함수가 호출된다.
3) append 호출
public AbstractStringBuilder append(String str) {
if (str == null) {
return appendNull();
}
int len = str.length();
//str이 추가되었을 때 value의 크기보다 커지는지 확인한다
//만약 더 커진다면 기존 길이* 2 + 2 만큼으로 value를 재할당하고
//기존에 있던 value값을 복사해준다.
ensureCapacityInternal(count + len);
//count위치에 str를 복사해준다.
putStringAt(count, str);
count += len;
return this;
}
putStringAt이라는 부분에서 value로 str의 copy가 발생하게 된다. 이게 value에는 "Hello, "가 저장되었다.
4) 두번째 문자열을 파라미터로 append 호출
a + b에서 a에 관한 작업이 끝나고, a에서 b를 파라미터로한 append가 호출되어 b를 연결시켜준다.
5) toString 호출
@Override
@HotSpotIntrinsicCandidate
public String toString() {
//새로운 String을 생성한다.
// Create a copy, don't share the array
return isLatin1() ? StringLatin1.newString(value, 0, count)
: StringUTF16.newString(value, 0, count);
}
a = a + b;에서 a로 asign하는 연산을 위해서 toString이 호출된다. 이때 String 클래스의 instance가 새롭게 생성되게 되며, value를 새로운 String 객체로 전부 copy해주는 작업이 발생한다.
3. 과정 정리
public static void main(String[] args) throws Exception {
String a = "Hello, ";
String b = "World!";
//a = a + b; 은 아래 과정과 같다.
StringBuilder builder = new StringBuilder(a); // 1. 생성자 호출
builder.append(b); //2. b 스트링 추가
a = builder.toString(); // 3. 스트링 변환
System.out.println(a); // 출력결과 : Hello, World!
}
결국 a = a + b;는 위에서 볼 수 있듯이 코드 3줄를 축약시켜 놓은 형태라고 할 수 있다. + 연산이 일어날 때마다 새롭게 StringBuilder클래스의 인스턴스를 생성하는 작업이 발생되게 된다.
4. 퍼포먼스 측정
+연산과 StringBuilder를 통한 String append가 얼마나 성능차이가 나는지 확인해보았다. 10,000번 동안 a에다 b를 연결하는 과정을 수행해보았다.
public static void main(String[] args) throws Exception {
String a = "Hello, ";
String b = "World!";
// +속도 측정
int maxIter = 10000;
long t0 = System.currentTimeMillis();
for (int i = 0; i < maxIter; i++) {
a += b;
}
long t1 = System.currentTimeMillis();
System.out.println(t1 - t0);
// StringBuilder 속도 측정
a = "Hello, "; // 초기화
t0 = System.currentTimeMillis();
StringBuilder builder = new StringBuilder(a);
for (int i = 0; i < maxIter; i++) {
builder.append(b);
}
builder.toString();
t1 = System.currentTimeMillis();
System.out.println(t1 - t0);
// + 의 내부 동작이 일치하는지 검증
a = "Hello, ";
t0 = System.currentTimeMillis();
for (int i = 0; i < maxIter; i++) {
StringBuilder builderCheck = new StringBuilder(a);
builderCheck.append(b);
a = builderCheck.toString();
}
t1 = System.currentTimeMillis();
System.out.println(t1 - t0);
}
//결과
+ : 173ms
StringBuilder : 2ms
검증 : 172ms // + 연산과 비슷한 수준으로 측정되었다.
+ 연산의 경우는 173ms가 걸린반면, StringBuilder를 사용한 경우에는 2ms밖에 걸리지 않았다. 반복횟수가 많아질 수록 이차이는 무시할 수 없었다.
5. 마무리
+연산에서 계속 새롭게 StringBuilder를 객체를 생성하고 toString의 호출로 String 객체 또한 생성한다. 따라서, 반복적으로 발생하는 + 의 경우에는 사용을 지양해야할 필요가 있다고 보여진다. 편리하게 사용하는만큼 성능저하가 있으므로 주의해야겠다.
'JAVA' 카테고리의 다른 글
Adapter Pattern 과 SLF4J (0) | 2021.08.27 |
---|---|
Log4j 알아보기 (0) | 2021.08.16 |
Volatile Java (0) | 2021.08.16 |
Java 익명 클래스와 람다 표현식의 다른 점 (0) | 2021.07.13 |
LinkedHashMap 알아보기 (0) | 2020.12.11 |