고코딩

[JAVA] String과 StringBuffer,StringBuilder의 차이점과 메모리 이슈 본문

JAVA

[JAVA] String과 StringBuffer,StringBuilder의 차이점과 메모리 이슈

고코딩 2020. 12. 29. 17:34

지인이 코딩테스트 문제를 풀던중 계속해서 메모리 초과가 발생하는 문제가 있었다. 문자열들을 입력받아 정렬하는 문제였는데 아무리 봐도 메모리초과가 발생할만한 곳이 없었다. 문자열도 String이 아닌 StringBuffer로 받아서 사용하고 있었기에 의심을 하지 못했다. 문제는 + 연산에서 발생했다. 생각해보니 StringBuffer에서는 append를 사용하도록 기능을 주고 있었다. 그럼 append와 + 연산은 무슨 차이일까?

String, StringBuffer,StringBuilder의 특징

String과 StringBuffer, StringBuilder의 가장 큰 차이점은 String은 Imutable이라는 점이다. Imutable은 불변하지 않는 이라는 뜻을 가지고 있는데 String은 문자열 객체가 생성되면 그 객체는 절대 변하지 않는다. StringBuffer와 StringBuilder는 문자열객체가 변할수 있다는 특징을 가지고 있다.

혹시 본인이 나는 String 문자열에 문자를 이어붙여서 문자열을 변화시킨적이 있다고 생각한 사람이 있는가? 사실 그 연산은 문자열이 변한것이 아니라 문자열 객체가 새로 생성된 것이다.

아래코드를 살펴보자

String a ="aaa";
String b = new String("bbb");

a = a+b;

1행과 2행은 코드는 다르지만 String객체를 생성하는것은 똑같다. 지금 상태에서 System.out.println(a)를 하면 aaabbb가 출력될 것이다. 근데 String은 Imutable하게 불변하다고 했다. String에서 + 연산은 aaa 값을 aaabbb로 바꾸어주는것이 아니라 aaabbb에 대해서 새로운 String 객체를 생성하여 a가 참조하도록 한다. 기존에 있던 aa는 쓰레기가 되어 나중에 가비지 컬렉션에 의해 처리된다. +연산하나만 사용했을 뿐인데 쓸데없는 객체가 생성되어 버렸다. 실제로 객체가 새로 생성되는지 코드로 알아보자.

String a ="aaa";
String b = new String("bbb");

System.out.printf("a = %6s, address : %s\n",a , System.identityHashCode(a));
System.out.printf("b = %6s, address : %s\n",b , System.identityHashCode(b));

a = a+b;
System.out.printf("a = %6s, address : %s\n",a , System.identityHashCode(a));
System.out.printf("b = %6s, address : %s\n",b , System.identityHashCode(b));
a =    aaa, address : 366712642
b =    bbb, address : 2101973421
a = aaabbb, address : 685325104
b =    bbb, address : 2101973421

실행결과를 보면 a객체는 처음에 366712642 이라는 곳에 주소가 할당되어있었는데 + 연산 이후 685325104로 새로운 메모리 공간이 할당된 것을 확인할 수 있었다.

그럼 StringBuffer에서 append 연산을 사용해보면 어떻게 될까?

StringBuffer a =new StringBuffer("aaa");
StringBuffer b =new StringBuffer("bbb");

System.out.printf("a = %6s, address : %s\n",a , System.identityHashCode(a));
System.out.printf("b = %6s, address : %s\n",b , System.identityHashCode(b));

a.append(b);
System.out.printf("a = %6s, address : %s\n",a , System.identityHashCode(a));
System.out.printf("b = %6s, address : %s\n",b , System.identityHashCode(b));
a =    aaa, address : 366712642
b =    bbb, address : 2101973421
a = aaabbb, address : 366712642
b =    bbb, address : 2101973421

실행 결과에서 보면 append연산 이후에도 같은 주소에 할당되어 있는 것을 확인할 수 있었다.

String 클래스의 장점

그럼 메모리만 많이 차지하는 String클래스는 왜 사용하고 있는 것일까? 라는 생각이 든다. 당연히 String만의 장점이 있고 JAva에서 세 가지의 클래스를 따로 제공하는 이유가 있을 것이다.

String클래스의 Imutable 속성 때문에 값이 변하지 않는다는 것을 알게 되었다. 이 말인 즉슨 여러 스레드가 데이터를 공유하더라도 동기화를 전혀 신경쓸 필요가 없다는 뜻이다. 객체가 변하지 않으니까. 또한 StringBuffer와 StringBuilder에서 toString()이 호출되면 해당 문자열을 읽기 위해 String객체를 생성해서 반환하게 되낟. 즉 연산이 적고 문자열을 읽을 일이 많은 경우에는 String클래스의 사용이 더 적절하다.

append()의 작동 방법

그럼 어떻게 append()가 작동하길래 String과 다른게 객체가 생성이 되지 않고 성능차이를 보이는 것일까?

먼저 append()의 코드를 살펴보자

StringBuffer.class

public synchronized StringBuffer append(StringBuffer sb) {
        toStringCache = null;
        super.append(sb);
        return this;
    }

멤버변수

  • 문자열의 값을 저장하는 char형 배열 value
  • 현재 문자열 크기의 값을 가지는 int형의 count
AbstractStringBuilder.class

// Documentation in subclasses because of synchro difference
    public AbstractStringBuilder append(StringBuffer sb) {
        if (sb == null)
            return appendNull();
        int len = sb.length();
        ensureCapacityInternal(count + len);
        sb.getChars(0, len, value, count);
        count += len;
        return this;
    }

봐도 이해가 안되는 함수들이 많지만 최대한 분석해보자.

StringBuffer.classAbstractStringBuilder.classappend()를 가져다 사용하고 있다. AbstractStringBuiler.class에서 int len = sb.length();로 문자열의 길이를 알아낸후 sb.getChars(0, len, value, count); 로 sb 문자열을 value에 count길이만큼 복사하게 된다.

완변한 해석은 아니지만 append()는 문자열의 공간을 늘려주어 공간을 확보한뒤 문자열을 붙여준다는 사실을 알 수 있었다.

위의 코드만 보더라도 매번 String객체를 생성하는 String클래스보다 StringBuffer가 잡아먹는 메모리의 크기가 적다는 것을 알수 있었다.

혹시 빌드중 속도가 느리다면 + 연산 대신 append를 사용해 보자. 속도가 몇배는 빨라질 것이다.

참고자료