자바 성능 튜닝 이야기 - String

ddindo·2022년 10월 31일
0
post-thumbnail

들어가기 전...

오늘 다룰 내용은 Java의 String 클래스에 대한 것이다. 평소 개발할 때 String을 주로 사용했던 나는 책을 읽으며 String을 사용하지 말라고 듣게 됐다. 평소에 생각 없이 사용하던 사소한 기능도 기능에 큰 영향을 줄 수 있다는 사실을 알게 됐다.

String과 그 외의 클래스

자바로 문자열을 표현하기 위해서 누구나 처음에 String str1 = "ABC" 와 같은 형태로 사용했을 것이다. 물론 나도 이렇게 사용했고, 지금도 그렇게 사용해왔다. 하지만 자바에는 문자열을 만들 수 있는 다른 클래스가 제공된다. 바로 StringBuilderStringBuffer 클래스이다.

이 두 클래스는 기존의 String과 속도나 메모리 차원에서 다른 차원의 속도를 보여준다.
만약 Sql query문을 String으로 만든다고 했을 떄, 다음과 같이 만들어보자


String strSQL = "";
strSQL += "select * ";
strSQL += "from ( ";
strSQL += "select A_col, B_col, ";
// 이하 생략 (400 라인)

책에서 위와 같이 쿼리를 만들어 수행하면, 10회 평균 메모리는 5MB, 응답시간은 5ms가 소모 된다고 한다.
하지만 StirngBuilder를 사용하여 다음과 같이 수행하게 된다면 메모리는 371KB, 응답시간은 0.3ms가 된다.

StringBuilder strSQL = new StringBuilder();
strSQL.append(" select * ");
strSQL.append("from ( ");
// 이하 생략

이렇게 속도가 차이 나는 이유를 알아보기 전, StringBuffer와 StringBuilder에 대해 간단하게 알아보고 가자.

StringBuffer

StringBuffer의 경우 append, insert를 통해 사용할 수 있다.  append는 말 그대로 문자열의 맨 뒤에 문자열을 붙이는 기능이다. insert는 index를 통해 원하는 위치에 문자열을 집어 넣을 수 있다.

StirngBuffer는 threadsafe하게 동작하므로 멀티 스레드 환경에서 문자열에 대해 공통으로 접근할 때 사용할 수 있다.

StringBuilder

StringBuilder 또한 append, insert를 통해 문자열을 생성할 수 있다. 하지만 StringBuffer와 같이 threadsafe 하지 않기 때문에 멀티 스레드 환경에서 사용할 수 없다. 그 대신 StringBuffer에 비해 속도가 조금 더 빠르다. 물론 두 클래스 모두 String에 비해 속도가 더 빠르다.

String이 느린 이유

자바를 배워본 사람이라면 GC(Garbage Collector)에 대해 들어 봤을 것이다. C언어의 경우 메모리를 할당하고 해제하지 않는 다면 메모리 공간의 낭비가 생기게 되고 나중에 메모리가 가득 차서 프로세스가 죽어 버릴 수 있다. 하지만, 자바에서는 인스턴스를 생성하여 메모리 공간을 차지한 뒤 따로 해제하지 않아도 연결이 끊어졌을 때 해당 공간을 비워 메모리를 확보한다. (자세한 방법은 다른 포스팅에서 다룰 예정)

이 상황을 기억하고 우리가 사용하는 String Class의 더하기 연산을 살펴보자.

String tmp = "A"; // 메모리 100번 째 저장
tmp += "B";	// 메모리 200번 째 저장
tmp += "C"; // 메모리 300번 째 저장

우리는 이 코드를 실행하면 처음에 "A"를 설정한 메모리 영역 뒤에 "B", "C"가 붙어서 저장될 것이라고 생각한다.

하지만 실제로는 아래의 그림처럼 새로운 문자열의 크기만큼 다른 공간을 할당하여 저장하게 된다.

이렇게 실행이 되니 결국엔 새로운 객체가 할당 되기 때문에 메모리 공간을 많이 먹게 된다. 또한 이로 인해 GC가 발생한다. GC의 경우 많은 자원을 먹는 동작이기 때문에 결국 응답시간도 늦어지게 된다.

하지만, StirngBuilder, StringBuffer의 경우 우리가 생각한 것 처럼 메모리 공간에서 뒤에 붙이게 된다.

정리하면, String의 경우 작은 크기의 문자열을 다룰 때 사용해도 된다. 하지만 되도록이면 StringBuilder, StringBuffer를 사용하는 것이 좋다.
특히 멀티 스레드 환경에서 ThreadSafe가 필요하다면, StringBuffer를 사용하고 그렇지 않다면, StringBuilder를 사용 하는게 좋다.

추가적으로 만약 JDK 5.0 이상의 버전이면 컴파일러에서 자동으로 String을 StringBuilder로 변환하게 된다. 하지만 반복 루프에서 String Class에 더하기 연산을 사용하게 된다면, 내부에서 StringBuilder를 계속 새로 생성하기 때문에 이 또한 문제가 발생하게 된다.

적용 사례

며칠 전에 개발을 진행할 때 For loop 안에서 String으로 더하기 연산된 코드를 발견할 수 있었다. 이 포스팅이 생각나서 Stringbuilder로 바꾸는 작업으로 변경하였다.

List<String> dataList = new ArrayList();
String target_key = "QWEFWAJIF3wAWJEFAWPIEFJJJEMDLWQ31";
int size = 0;

//변경 전
for(String data : dataList) {
	for(String value : values) {
      if (data.equals("TARGET")){
      	Stirng tmp_key = value + "3";
      }
  	}
    if(tmp_key.equals(target_key) {
    	size += 1;
    }
}


// 변경 후
for(String data : dataList) {
	for(String value : values) {
      if (data.equals("TARGET")){
      	tmp_key.append(value);
        tmp_key.append("3");
      }
  	}
    
    if(tmp_key.toString().equals(target_key) {
    	size += 1;
    }
    
    tmp_key.delete(0, tmp_key.length());
}

이 코드는 예시를 위해 작성한 코드이다. 또한 기존의 For loop 안에서 문자열 더하기 연산을 사용하지 않고 StringBuilder를 통해 문자열 생성 후 비교하고 다시 비워주는 과정을 반복하게 된다.

0개의 댓글