지금까지 코테 문제를 풀면서 StringBuffer을 사용한 이유는 그 순간에 StringBuffer을 사용하는 것이 성능에 도움이 될 것이라 판단했기 때문이라기보다는, 그저 append기능을 쓰기 위해서였습니다.
그리고 문자열 연산 시 String의 문자열 합치기 기능보다 더 성능이 낫다고 들어서 정도의 이유였죠..
또 StringBuffer보다 StringBuilder가 단일 스레드 연산에 더 효율적이라는 글을 읽은 뒤로는 StringBuilder을 열심히 썼어요.
과연 그 말이 항상 옳을까요? 그리고 셋의 차이점은 무엇일까요?
String과 StringBuffer/StringBuilder은 모두 문자열을 다루는 클래스입니다.
이중 어느 것을 사용하는 게 성능이 좋을지는 각 클래스의 특징을 알아야 판단할 수 있겠죠.
각 클래스의 특징에 대해 알아봅시다.
String: 불변성(Immutable), 힙(Heap)에 저장
- String 객체는 한 번 생성되면 할당된 값을 변경할 수 없습니다. (불변)
- String 객체는 GarbageCollection이 동작하는 힙(heap)영역에 생성되는데, 힙 내에 객체가 생성되면 해당 객체의 값을 변경할 수 없습니다.
- 예를 들어 +연산자 또는 concat메서드를 사용해서 기존 문자열에 다른 문자열을 붙여도, 기존 메모리공간의 문자열이 변경되는 것이 아니라 또달른 String 객체가 생성되고 그 객체를 참조합니다.
- 따라서 문자열 연산이 적은 경우 적절합니다.
- 참고) String을 불변으로 설정한 이유는 캐싱, 보안, 동기화, 성능측면에서 이점을 얻기 위함입니다.
- 캐싱이란 재접근 확률이 높은 데이터에 빠르게 액세스할 수 있게 가까운 곳에 저장하는 것입니다. String Pool에 리터럴 문자열을 하나씩만 저장하여 재사용하면 캐싱이 가능하고, 힙 공간을 절약할 수 있습니다.
- DB 연결 시 사용자 이름과 암호가 문자열로 전달되는데, 만약 문자열이 변경 가능하다면 해커가 참조값을 변경하여 보안 문제가 일어날 수 있습니다.
- 동시에 실행되는 여러 스레드가 문자열을 공유하는 경우, 변경 위험 없이 안정적인 공유가 가능합니다.
public final class String implements java.io.Serializable, Comparable {
private final byte[] value; //final(상수)형 변수에 배열로 저장 - 불변
}
String str = "Hello"; //힙의 String Constant Pool에 Hello 저장, str이 해당 주소를 가리킴
str += " World"; //"Hello World"를 다른 주소에 새로 저장, str이 그 주소를 가리키도록 변경됨
//"Hello"를 참조하는 변수가 더이상 없으므로 GC 제거 대상이 됨
StringBuffer/StringBuilder: 가변성(mutable), 버퍼(Buffer)에 저장
- StringBuilder, StringBuffer 객체는 생성된 후 값이 변할 수 있습니다. (가변)
- StringBuffer 객체는 버퍼(buffer)라고 하는 독립적인 공간에 생성되며, 문자열의 추가/수정/삭제가 일어나도 String과 달리 매번 새로운 객체가 만들어져 공간을 차지하지 않고 기존 객체 내에서 작업 가능합니다.
- 예를 들어 append()메서드를 사용하여 기존 문자열에 다른 문자열을 붙이면 기존 객체 내용이 변경됩니다.
- 따라서 문자열 연산이 많은 경우 주로 사용합니다.
- 참고) 버퍼에 대하여
- StringBuffer 객체 생성 시 버퍼 크기를 지정해주지 않으면 기본적으로 길이 16의 char형 배열이 생성되며, 이 배열이 문자열을 저장하고 편집하기 위한 공간(buffer)로 사용됩니다.
- 문자열 연산 중 할당된 버퍼 크기를 넘으면 자동으로 버퍼 크기가 늘어납니다.
- 버퍼 크기가 변경될 때는 새로운 크기의 배열을 선언하고 이전 배열의 데이터를 복사하여 옮깁니다.
- 따라서 문자열 연산이 많을 것으로 예상되는 경우 사전에 넉넉한 크기로 선언하는 게 좋습니다.
public final class StringBuffer implements java.io.Serializable {
private byte[] value; //final(상수)키워드가 없다 - 가변
}
StringBuffer sb = new StringBuffer("Hello"); //배열의 길이가 16인 버퍼 생성, 버퍼에 Hello 저장, sb가 해당 주소를 가리킴
sb.append(" World"); //해당 주소에 저장된 "Hello"가 "Hello World"로 변경됨
'+'을 통한 문자열 합치기는 항상 지양해야할까?
사실 자바에서 String 데이터를 +연산하면 컴파일 전에 내부적으로 StringBuilder 객체가 생성되고 append()와 toString()메서드가 호출된다고 합니다.
String str = "Hello";
str += " World";
즉 위 코드를 실행하면
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World").toString;
이렇게 동작하게 됩니다.
그렇다면 이 '+' 연산이 여러 번 반복된다면 '+'연산의 반복 횟수만큼 매번 StringBuilder 객체가 생성되고 append()와 toString() 메서드가 호출됩니다. 따라서 성능이 저하되고 메모리가 낭비됩니다.
final StringBuilder sb = new StringBuilder();
for(int i=0; i<10000; i++) {
sb.append(i);
}
String str = sb.toString();
결론 : 문자열을 여러 번 합쳐야 되면 '+' 연산자는 지양해야 합니다. 이때는 위와 같이 StringBuilder 객체를 처음부터 사용하는 것이 좋습니다.
항상 String보다 StringBuffer/StringBuilder가 좋을까?
StringBuffer/StringBuilder을 사용할 때도 버퍼의 크기를 늘리고 줄이는 경우 많은 연산이 필요하므로, 문자열 수정이 많지 않으면 String이 나을 수도 있습니다. String은 단순히 읽어오는 연산에서는 StringBuffer/StringBuilder보다 빠릅니다.
StringBuffer와 StringBuilder의 차이점
두 클래스는 가변 클래스이고, 제공하는 메서드도 똑같습니다.
다만 StringBuffer는 동기화를 지원하므로 멀티스레드 환경에서 안전하게 동작하고, StringBuilder는 동기화를 지원하지 않아 멀티스레드 환경에서 안전하게 동작하지 않습니다.
StringBuffer는 메서드에서 synchronized 키워드를 사용하기 때문에 여러 스레드가 동시에 접근하려고 할 때 현재 데이터를 사용하고 있는 스레드를 제외한 나머지 스레드들이 데이터에 접근할 수 없습니다.
'Java' 카테고리의 다른 글
[JAVA] static 메서드에서 non-static 멤버에 접근 (2) | 2024.04.15 |
---|---|
[JAVA] Comparable로 객체 정렬하기 (0) | 2024.04.14 |
[JAVA] String Constant Pool (0) | 2024.03.10 |
[JAVA] 다형성이란? 다형성이 필요한 이유 | 오버라이딩, 오버로딩, 타입 변환(업캐스팅) (0) | 2024.03.06 |