String 클래스는 잘 사용하면 상관없지만, 잘못 사용하면 메모리에 많은 영향을 주기 때문이다.
그렇다면 잘못 사용한 사례를 살펴보자.
String 클래스를 잘 못 사용한 사례
보통 서버에서는 DB 데이터를 갖고와서 그 데이터를 화면에 출력하는 시스템을 가지고 있다.
- 쿼리 문장을 만들기 위해 String 클래스
- 결과를 처리하기 위한 Collection 클래스
위 두 클래스를 가장 많이 사용하게 된다.
일반적으로 사용되고 가장 단순하게 만드는 쿼리 작성 문장을 살펴보자.
String sql = "";
sql += "select * ";
sql += "from ( ";
sql += "select A_column, ";
sql += "B_column ,";
// 생략된 쿼리 (약 400라인) ...
...
이렇게 문자열을 더하는 패턴으로 작성하면 개발 시에는 편할지라도 메모리를 많이 먹는다.
이것을 다음과 같이 StringBuilder로 바꾼다.
StringBuilder sql = new StringBuilder();
sql.append(" select * ");
sql.append(" from ( ");
sql.append(" select A_column, ");
sql.append(" B_column , ");
// 생략된 쿼리 (약 400라인) ...
...
위 두 코드를 10회 평균 사용한다고 가정했을 시
- 메모리 사용량 5MB → 371KB
- 응답 시간 5ms → 0.3ms
이렇게 엄청난 성능을 개선하는 결과를 얻을 수 있을 것이다.
StringBuffer 클래스와 StringBuilder 클래스
JDK 5 기준으로 문자열을 만드는 클래스는 다음 3개가 가장 많이 사용된다.
- String
- StringBuffer
- 쓰레드에 안전하게 (ThreadSafe) 설계됨
- 여러 개의 쓰레드에서 하나의 StringBuffer 객체를 처리해도 문제 발생X
- StringBuilder (JDK 5에서 추가됨)
- 단일 쓰레드에서의 안전성만 보장
- 여러 개의 쓰레드에서 하나의 StringBuilder 객체를 처리하면 문제 발생O
이 때 StringBuffer와 StringBuilder는 메소드가 동일하지만, 이런 차이점이 있다.
그럼 동일한 4개의 생성자를 StringBuffer 기준으로 살펴보자.
- StringBuffer()
- 아무 값도 없는 StringBuffer 객체 생성함
- 기본 용량은 16개의 char
- StringBuffer(CharSequence seq)
- CharSequence를 매개변수로 받아 그 seq 값을 갖는 StringBuffer 생성
- StringBuffer(int capacity)
- capacity에 지정한 만큼의 용량을 갖는 StringBuffer 생성
- StringBuffer(String str)
- str의 값을 갖는 StringBuffer를 생성
🤔 어? CharSequence는 무엇이죠?
- CharSequence는 인터페이스이다
- 이 인터페이스를 구현한 클래스는 다음 4개가 있다
- CharBuffer
- String
- StringBuffer
- StringBuilder
- 이 인터페이스는 StringBuffer 나 StringBuilder로 생성한 객체를 전달할 때 사용
public class StringBuilderTest1 {
public static void main(String args[]) {
StringBuilder sb = new StringBuilder();
sb.append("ABCDE");
StringBuilderTest1 sbt = new StringBuilderTest1();
sbt.check(sb);
}
public void check(CharSequence cs) {
StringBuffer sb = new StringBuffer(cs);
System.out.println("sb.length = " + sb.length());
}
}
예시를 들면 위 코드와 같다.
StringBuilder를 CharSequence로 받고 StringBuffer로 처리하면 오류가 발생할까?
결론은 "sb.length = 5"라고 정상적으로 처리된다.
StringBuffer 또는 StringBuilder로 값을 만든 후 굳이 toString을 수행하여 필요 없는 객체를 만들어서 넘겨주기보다는 CharSequence로 받아서 처리하는 것이 메모리 효율이 더 좋다.
StringBuffer와 StringBuilder 메소드 중 가장 자주쓰는 2개
- append() : 기존의 값의 맨 끝에 넘어온 값을 덧붙이는 작업 수행
- insert() : 지정된 위치 이후에 넘어온 값을 덧붙이는 작업 수행
- 이 때 지정한 위치까지 값이 할당되어 있지 않으면 StringIndexOutOfBoundsException 발생
StringBuffer sb = new StringBuffer();
// 잘 사용한 예시 1
sb.append("ABCDE");
sb.append("FGHIJ");
sb.append("KLMNO");
// 잘 사용한 예시 2
sb.append("ABCDE")
.append("FGHIJ")
.append("KLMNO");
// 잘 못 사용한 예시
sb.append("ABCDE" + "=" + "FGHIJ");
sb.insert(3, "123");
System.out.println(sb);
잘 못 사용한 예시처럼 append() 메소드 내에 + 연산자 사용은 X
문자열을 더하면 StringBuffer를 사용하는 효과가 전혀 없게 된다.
그러므로 되도록 append() 메소드를 이용해 문자열을 더하도록 하자.
String vs StringBuffer vs StringBuilder
final String aValue = "abcde";
for (int i = 0; i < 10; i++) {
String a = new String();
StringBuffer b = new StringBuffer();
StringBuilder c = new StringBuilder();
for (int j = 0; j < 10000; j++) {
a += aValue;
}
for (int j = 0; j < 10000; j++) {
b.append(aValue);
}
String temp = b.toString();
for (int j = 0; j < 10000; j++) {
c.append(aValue);
}
String temp2 = c.toString();
System.out.println(System.currentTimeMillis());
}
- 문자열을 더하기 위한 객체 a, b, c 생성
- 각각의 객체를 10,000번씩 수행하면서 각 객체에 "abcde"를 추가
- StringBuffer와 StringBuilder가 String 클래스의 객체로 변환되는 동일한 역할을 할 수 있도록 toString() 호출
- 이러한 행위를 10번 반복
- 앞서 수행된 결과와 다른지 확인하기 위해 현재 시간 출력
이렇게 수행되는 테스트이다.
회계 프로그램과 같이 복잡한 시스템의 경우 쿼리 하나가 4페이지 넘어가는 경우도 있다.
어떤 경우에는 PreparedStatement 문장을 만들기 위해 다음과 같이 반복하는 경우도 있다.
for(int i = 0; i < a.size(); i++) {
sql += ", ?";
}
일부 시스템의 경우 배치 프로그램을 자바 기반으로 작성하는 경우가 많다.
이 경우에는 반복 횟수가 더 많기 때문에 이 부분에 더욱 유의해야 한다.
자 그럼 아까 쓴 코드의 결과를 살펴보자.
주요 소스 부분 | 응답 시간(ms) | 비고 |
a += "a"; | 95,801.41ms | 95초 |
b.append("a"); String temp = b.toString(); |
247.48ms 14.21ms |
0.24초 |
c.append("a"); String temp2 = c.toString(); |
174.17ms 13.38ms |
0.17초 |
다음은 메모리 사용량이다.
주요 소스 부분 | 메모리 사용량(byte) | 생성된 임시 객체수 | 비고 |
a += "a"; | 100,102,000,000 | 4,000,000 | 약 95GB |
b.append("a"); String temp = b.toString(); |
29,493,600 10,004,000 |
1,200 200 |
약 28MB 약 9.5MB |
c.append("a"); String temp2 = c.toString(); |
29,493,600 10,004,000 |
1,200 200 |
약 28MB 약 9.5MB |
응답 시간은 String보다 StringBuffer가 약 367배 빠르고, StringBuilder가 약 512배 빠르다.
메모리는 StringBuffer와 StringBuilder보다 String이 약 3,390배 더 사용한다.
왜 이런 결과가 나올까?
a = a + aValue라는 코드를 수행하면 a에 aValue를 더한 새로운 String 객체가 만들어진다.
이전의 a객체는 필요 없는 쓰레기 값이 되어 GC의 대상이 된다.
즉, 위 코드를 3번 수행하면 다음 그림과 같다.
그림처럼 문자열을 더할수록 새로운 String 객체가 만들어지고, 그 전의 객체는 쓰레기가 된다.
이런 작업을 수행하면서 메모리를 많이 사용하게 되고, 응답 속도에도 많은 영향을 미치게 된다.
GC를 많이 할수록 시스템 CPU를 사용하게 되고 시간도 많이 소요되기에, 메모리 사용을 언제나 최소화하는 것은 당연한 일이다.
StringBuffer 또는 StringBuilder는 String과 다르게 새로운 객체를 생성하지 않고, 기존 객체의 크기를 증가시키면서 값을 더하게 된다.
이러한 String의 특성을 어떻게 튜닝에 적용할까?
String을 쓰는 것은 무조건 나쁘고, 무조건 StringBuffer와 StringBuilder를 사용해야 할까?
이러한 고민에 대한 답변은 다음과 같다.
- String은 짧은 문자열을 더할 경우 사용
- StringBuffer는 쓰레드에 안전한 프로그램이 필요할 때나 개발중인 시스템의 부분이 쓰레드에 안전한지 모를 경우 사용
- StringBuilder는 쓰레드에 안전한지 여부가 전혀 관계없는 프로그램을 개발할 때 사용
자바 버전에 따른 차이
public class VersionTest {
String str = "Here " + "is " + "a " + "sample.";
public VersionTest() {
int i = 1;
String str2 = "Here " + "is " + i + " samples.";
}
}
위 코드를 역 컴파일하면 버전에 따라 다음과 같이 나온다.
JDK 1.4
public class VersionTest {
public VersionTest() {
str = "Here is a sample.";
int i = 1;
String s = "Here is " + i + " sample.";
}
String str;
}
역 컴파일한 소스를 보면 자바 컴파일러가 컴파일할 때 알아서 문자열을 더해놓은 것을 볼 수 있다.
그래도 중간에 int나 다른 객체가 들어가면 직접 작성한 코드처럼 더하도록 되어있다.
결국 필요 없는 객체는 생성이 된다는 의미이다.
JDK 5
public class VersionTest {
public VersionTest() {
str = "Here is a sample.";
int i = 1;
String str2 = (new StringBuilder("Here is "))
.append(i).append(" samples.").toString();
}
String str;
}
JDK5부터는 문자열을 더하도록 프로그래밍 했다면 컴파일할 때 위와 같이 변환된다.
개발자의 실수를 어느 정도 피할 수 있게 된 것이다.
정리하며
문자열을 처리하기 위한 클래스는 크게 3가지
- String
- 가장 많은 메모리 사용
- 응답 시간 제일 느림
- StringBuffer
- StringBuilder
JDK 5이상이면 컴파일러가 자동으로 StringBuilder로 변환해준다.
하지만 반복 루프를 사용하여 문자열을 더할 때에는 객체가 계속 추가된다는 사실은 변함없다.
그러므로 String 클래스를 쓰는 대신에, 쓰레드와 관련이 있으면 StringBuffer, 쓰레드 안전여부와 상관이 없으면 StringBuilder를 사용하는 것을 권장한다.
"자바 성능을 결정짓는 코딩 습관과 튜닝 이야기" 도서 참고
'프로그래밍 > Java' 카테고리의 다른 글
Java Stream(스트림)은 원본 데이터를 변경할 수 있다!? (0) | 2023.03.27 |
---|---|
JVM(자바가상머신)이란? - Part5, Garbage Collection (1) | 2022.10.15 |
JVM(자바가상머신)이란? - Part4, Runtime Data Area (0) | 2022.09.29 |
[Java] sort와 parallelSort 비교 (0) | 2022.09.18 |
Java - 값이 null,공백 등 Blank인지 확인하기 (0) | 2022.04.07 |