주요글: 도커 시작하기
반응형
두개 이상의 String 객체를 합치기 위하여 + 연산자를 사용할 때 방법에 따라 어떤 성능상의 차이가 있는지 알아본다.

문자열의 연결

프로그램을 작성하다 보면 두개 이상의 문자열을 연결할 필요가 있을 경우가 많다. 특히 화면에 결과를 출력하거나, 로그 파일 또는 메시지 전송 등에서 특히 자주 사용된다. 보통 문자열의 연결을 위해서 다음과 같은 코드를 사용하게 된다.

  String str1 =  "안녕하세요. ";
  String str2 =  "당신의 점수는 ";
  String str3 =  "점입니다.";
  int point = 100;
  String str = str1 + str2 + point + str3;

이때 마지막 줄의 str = str1 + str2 + point + str3 라는 코드는 자바에서 내부적으로 다음 코드와 동일하게 처리된다.

  str = new StringBuffer().append(str1).append(str2).append(point).append(str3).toString()

이 과정은 다음과 같은 행위들이 차례로 실행된다는 것을 의미한다.

  1. StringBuffer 객체의 생성
  2. String 객체 str1의 내부 문자 배열을 StringBuffer 객체에 복사
  3. String 객체 str2의 내부 문자 배열을 StringBuffer 객체에 복사
  4. point의 값을 문자열로 변환하기 위하여 String.valueOf(point) 메소드를 사용하여 point를 문자열로 변환 후에 생성된 String 객체의 내부 문자 배열을 StringBuffer 객체에 복사
  5. String 객체 str3의 내부 문자 배열을 StringBuffer 객체에 복사
  6. StringBuffer 객체에 저장된 문자 배열을 toString() 메소드를 사용하여 String 객체 생성 후 복사
이러한 과정을 통하여 str = str1 + str2 + point + str3 라는 코드는 실제로 StringBuffer 객체 생성 1회, String 객체 생성 2회, 배열 복사 5회, 숫자->문자 변환 1회를 실행하게 된다. 이를 수행하기 위해서 내부적으로 정적 또는 동적 메소드 호출이 최소한 11회 이상 발생한다.

그러나, 이것이 가장 빠른 방법이며, 실제로 그렇게 복잡한 방법은 아니다. 또한, 코드를 작성하는 사람은 이러한 복잡성을 알고 있지 않아도 상관없다. 단지 str = str1 + str2 + point + str3 와 같은 방법으로 코드를 작성하면 된다.

비효율적인 코드 찾기

위의 코드 예를 다시 잘 생각해보면, 실제 프로그램 코드에서는 조금 다른 경우가 많다는 것을 경험한 프로그래머가 많을 것이다. 먼저, 문자열이나, 점수나 문자열을 구하는 과정이 데이터베이스나, 네트웍으로부터의 입력인 경우가 많으며, 따라서 실제 코드는 다음과 같이 되기 쉽다. 즉, 차례대로 필요한 값을 구하면서 그 즉시 문자열에 더하는 방법이다.

  String str;
  str += str1;
  str += str2;
  str += point;   // 또는 str += getPoint() 등이 될 수도 있다;
  str += str3;

성능 효율을 고려하지 않는다면, 실제로 이런 코드는 아무런 문제는 없다. 그러나, 위에 설명한 문자열의 연결 연산자의 내부 동작을 고려하면, 엄청난 비효율을 예상할 수 있다. 즉, 이 코드는 단지 문자열의 연결만을 위하여 StringBuffer 객체 생성 4회, String 객체 생성 5회, 배열 복사 11회, 숫자->문자 변환 1회, 내부적 메소드 호출 21회 이상 발생한다.

이런 비효율적인 자원 낭비를 확인하기 위하여 간단한 테스트를 실행해보았다. 첫번째는 문자열들을 한번에 연결하는 경우이다.

  String str = str1 + str2 + point + str3;

두번째는 문자열들을 따로 따로 연결하는 경우이다.

  String str;
  str += str1;
  str += str2;
  str += point;
  str += str3;

위의 두가지 경우를 각각 64만회 반복하여 소요 시간과 남은 자유 메모리의 양을 확인하여 비교하면 그 차이를 쉽게 알아 볼 수 있다. 결과를 그래프로 보면 다음과 같다.


두가지 경우에 대한 시간 및 메모리의 비교를 확인해 보면, 시간은 약 3배에 가까운 차이를 보이고, 메모리도 3배에 가까운 소모를 보이는 것을 알 수 있다. 또한, 메모리 사용에 따른 garbage collection은 중간에 시간 낭비를 증폭시키는 효과를 보여준다.

즉, 웹 서비스와 같이 많은 클라이언트를 가지는 프로그램에서는 문자열 처리가 빈번하며 동시에 수십만 회에 이르는 경우가 발생할 수 있다. 이때, 메시지 생성이나 로그 등을 위하여 문자열을 합치는 경우가 많다.

예를 들면 "안녕하세요. XXX님 반갑습니다. X월 X일 현재 회원님의 고객점수는 XXX점입니다."라는 메시지를 생성한다고 생각해보자.

이 문장에서 X로 표시된 부분은 전부 데이터베이스와 같은 곳에서 실시간으로 읽어오거나, 회원정보 객체에서 참조하여 읽어오게 된다. 그리고, 나머지 부분은 미리 작성된 문자열이다. 이러한 문자열을 합성하는 과정에서 흔히 다음과 같은 코드를 작성한다.

  String str = "안녕하세요. ";
  str += client.getName();
  str += "님 반갑습니다. ";
  str += client.getLoginDateAsString();
  str += " 현재 회원님의 고객점수는 ";
  str += clientManager.getPoint(client);
  str += "점입니다.";

대게 각 라인의 사이에 필요한 코드가 더 들어가기도 하지만 흔히 메시지를 만드는 부분만 추출하여 보면 이런 형태를 지닌다. 이경우 효율적이면서도 가장 쉬운 형태는 다음과 같은 형태이다.

  String str = "안녕하세요. " + client.getName() + "님 반갑습니다. " +
  client.getLoginDateAsString() + " 현재 회원님의 고객점수는 " +
  clientManager.getPoint(client) + "점입니다.";

그러나, 만약 위의 라인처럼 합칠 수 없는 코드가 있다면, 일단 새로운 String 형의 변수를 선언하여 중간값을 저장한 후에 필요할 때 문자열을 다 함께 합치면 된다.

  // 불가피한 이유로 미리 메소드를 호출하여 그 결과를 가지고 있어야 하는 경우
  String s1 = client.getLoginDateAsString();
  String s2 = clientManager.getPoint(client);
  String str = "안녕하세요. " + client.getName() + "님 반갑습니다. " +
  s1 + " 현재 회원님의 고객점수는 " + s2 + "점입니다.";

이 경우, 중간값을 저장하므로 자원 낭비가 있을거라고 생각하기 쉽지만, 실제로는 거의 차이를 보이지 않는다. 왜냐하면, 하나의 라인에서 전부 합치는 것에 비하여 어떠한 객체의 생성도 더 늘어나지 않기 때문이다. 단지 객체를 참조하는 s1, s2 변수만 두개 더 선언되었을 뿐이며, 이러한 객체 참조 변수는 실제로 자원을 거의 소모하지 않는다.

즉, 여러 라인에 걸쳐서 문자열을 더하는 경우처럼 추가적인 객체 생성이 있지 않으므로 주목할만한 성능의 차이는 없게 된다.

왜 이런 문자열 처리의 효율성에 주목해야 하는가?

보통 과거에는 그래픽, 소팅, 자료검색 등과 같은 분야에서 성능 향상에 대한 문제가 집중되어 왔다. 그 이유는 이런 기능들이 반복성이 높기 때문이다. 수십 회에서 수십만 회에 이르는 같은 코드의 반복이 흔히 일어나는 그래픽이나 자료 처리에서는 조그만 효율의 차이도 수십만배 증폭되어 큰 성능 차이를 보여주기 때문이다.

그 반면, 단순한 문자열 처리나 반복성이 없는 코드들은 실제로 아무리 비효율적이라고 하여도 전체적인 성능에는 거의 영향을 미치지 않았다. 즉, 이런 반복성 없는 코드들의 효율은 아무리 개선하여도 전체적인 성능 향상에는 아무런 기여도 하지 못한다.

그러나, 웹 서비스를 비롯하여 최근의 대부분의 네트워크 환경은 텍스트 중심의 메시지를 기반으로 한다. 따라서 효율적인 문자열의 처리는 성능에 큰 영향을 미치는 경우가 많다. 그 이유는 클라이언트-서버 환경에서 동시에 다수의 클라이언트가 접속하여 동일한 서비스를 요청하는 경우가 많기 때문이다.

즉, 문자열을 처리하는 행위는 반복되지 않지만, 수십만 명이 같은 웹 서비스를 요청하므로 마치 수십만 번 반복하는 효과를 발생하기 때문이다. 보통 인기있는 웹 사이트들은 하루 수백만 회가 넘는 접속이 이루어지며, 또한 대부분 같은 시간대에 요청되는 경우가 많다는 것을 고려하면, 이러한 반복효과는 엄청나다고 할 수 있다.

놀랍게도 이런 반복효과는 단순한 반복문보다 더 치명적이다. 그 이유는 조그만 비효율적코드로 하나의 서비스를 처리하는데 10%의 시간이 더 걸린다면, 그 시간동안 또 다른 사용자가 접속하여 서비스를 요청하므로, 반복효과가 가중된다. 10%의 시간동안 10%의 사용자가 추가로 접속하므로, 하나의 서비스를 처리하는데 이전보다 시스템이 10% 부족한 시간을 할당한다. 따라서, 다시 9%에 가까운 시간이 더 걸리게 된다.

이런 현상은 테스트 프로그램일 때 높은 효율을 자랑하던 프로그램이 실제 운영에서 피크 시간 때에는 맥을 못추는 이유와 관련된다.

결론

웹 서비스를 비롯하여 최근의 대부분의 네트워크 환경은 텍스트 중심의 메시지를 기반으로 한다. 따라서 효율적인 문자열의 처리는 성능에 큰 영향을 미치는 경우가 많다. 따라서 문자열 처리에서 빈번한 문자열 연결 연산자 +의 성능 향상에 관하여 알아보았다.

관련링크:

본 글의 저작권은 이동훈에 있으며 저작권자의 허락없이 온라인/오프라인으로 본 글을 유보/복사하는 것을 금합니다.

+ Recent posts