Java 에서 문자열을 다루는 대표적인 클래스로 String, StringBuffer, StringBuilder 가 있습니다.
항상 개발할때 대부분 String으로 사용하였는데, 작은 시스템에서는 크게 이슈가 발생하지는 않습니다.
그러나 큰 서비스와 시스템에서는 연산횟수가 많아지거나 멀티 쓰레드, Race Condition 등의 상황이 발생하면서 무작정 String으로만 사용한다면 성능에 큰 문제가 발생합니다.
이번 글에서는 각 특징을 이해하고, 상황에 맞는 적절한 클래스가 무엇인지 정리해보도록 하겠습니다.
1. String vs StringBuffer/StringBuilder
- String 객체는 한번 값이 할당되면 그 공간은 변하지 않으며, 이것을 불변(Immutable)성 이라 합니다.
- StringBuffer/StringBuilder 객체는 한번 값이 할당되더라도 한번 더 다른 값이 할당되면 할당된 공간이 변하며, 이것을 가변(mutable)성 이라 합니다.
- 간략하게 코드로 예를 들어보겠습니다.
String str = "String";
StringBuilder sb = new StringBuilder();
StringBuffer sbf = new StringBuffer();
sb.append("StringBuilder");
sbf.append("StringBuffer");
// 연산 전 객체들의 주소
System.out.println("String 객체의 주소 : "+str.hashCode());
System.out.println("StringBuilder 객체의 주소 : "+sb.hashCode());
System.out.println("StringBuffer 객체의 주소 : "+sbf.hashCode());
str += "Test";
sb.append("Test");
sbf.append("Test");
System.out.println("=============================");
// 연산 후 객체들의 주소
System.out.println("String 객체의 주소 : "+str.hashCode());
System.out.println("StringBuilder 객체의 주소 : "+sb.hashCode());
System.out.println("StringBuffer 객체의 주소 : "+sbf.hashCode());
- 해당 결과를 확인하면 아래와 같이 결과가 나오게 됩니다.
- String 객체의 주소는 변경된 것이 확인되고, StringBuilder와 StringBuffer 객체의 주소는 변하지 않은 것을 확인할 수 있습니다.
2. String
String str = "hello"; // String str = new String("hello");
str = str + " world"; // [ hello world ]
- 위 예제에서 살펴보면, hello 값을 가지고 있던 str 객체에 world 문자열을 더해 hello world 로 변경되었습니다.
- 그러나, 기존에 hello 값이 들어가 있던 객체 주소와 hello world 값이 들어가 있는 동일한 객체 주소는 서로 다른데, 이는 처음에는 hello 값이 들어가있는 메모리 영역을 가리키다가, hello world 값이 들어있는 메모리 영역을 가리키도록 변경된 것 입니다.
- 처음 가리키던 hello 값에 대한 메모리 영역은 Garbage로 남아있다가 GC(Garbage Collection)에 의해 정리됩니다.
- String 클래스는 불변(Immutalbe)하기 때문에, 문자열을 연산하는 시점에 새로운 String 인스턴스가 메모리에 할당이 되는 것 입니다.
- String은 불변성을 가지기 때문에 변하지 않는 문자열을 자주 읽어들일 때 사용하면 좋은 성능을 기대 할 수 있습니다.
- 그러나 문자열 추가,수정,삭제 등의 연산이 자주 일어나는 로직에서는 String 클래스를 사용하면 힙(Heap) 메모리에 많은 Garbage(가비지)가 생성되어 힙메모리 부족으로 어플리케이션 성능에 큰 영향이 있을 수 있습니다.
3. StringBuffer / StringBuilder
- 위에 보이는 것 처럼 두 클래스 모두 AbstractStringBuilder 라는 추상 클래스를 상속받아 구현되어 있습니다.
- AbstractStringBuilder 추상클래스의 멤버 변수는 다음 2가지 변수가 있습니다.
- value : 문자열의 값을 저장하는 Byte 형 배열
- count : 현재 문자열 크기의 값을 가지는 int 형 번수
- StringBuffer / StringBuilder 클래스의 문자열을 추가하고 싶으면 append() 메서드를 사용하는데, 이때 append() 메서드는 AbstractStringBuilder 에 다음과 같이 구현되어 있습니다.
public AbstractStringBuilder append(String str) {
if (str == null) {
return appendNull();
}
int len = str.length();
ensureCapacityInternal(count + len);
putStringAt(count, str);
count += len;
return this;
}
- 문자열을 추가하게 되면 추가할 문자열의 크기(길이)만큼 현재의 문자열을 저장하는 배열의 공간을 늘리고, 늘린 공간에 추가할 문자열을 넣어주는 방식입니다.
- 그렇기 때문에, 값이 변경되어도 같은 주소 공간을 참조하게 되는 것이고, 값이 변경되는 가변성을 띄게 됩니다.
StringBuffer sb= new StringBuffer("hello");
sb.append(" world");
4. StringBuffer vs StringBuilder
- 두 클래스의 가장 큰 차이점은 동기화(Synchronization)의 유무입니다.
- StringBuffer는 동기화 키워드를 지원하여 멀티쓰레드 환경에서 안전합니다.(thread-safe)
- String 클래스도 불변성으로 인해 마찬가지로 멀티쓰레드 환경에서의 안정성을 가지고 있습니다.
- StringBuilder는 동기화를 지원하지 않기 때문에 멀티쓰레드 환경에서 사용하는 것은 적합하지 않지만 동기화를 고려하지 않는 단일 쓰레드 환경에서는 StringBuffer 보다 뛰어납니다.
Java에서 synchronized 키워드는 여러개의 스레드가 한 개의 자원에 접근할려고 할 때, 현재 데이터를 사용하고 있는 스레드를 제외하고, 나머지 스레드들이 데이터에 접근할 수 없도록 막는 역할을 수행
5. 정리
컴파일러에서 분석 할 때 최적화에 따라 다른 성능이 나올 수도 있지만, 일반적인 경우엔느 아래 그림과 같이 사용하면 됩니다.
- String : 문자열 연산이 적고, 멀티쓰레드 환경일 경우
- StringBuffer : 문자열 연산이 많고, 멀티쓰레드 환경일 경우
- StringBuilder : 문자열 연산이 많고, 단일쓰레드이거나 동기화를 고려하지 않아도 되는 경우
참고
'BackEnd > Java' 카테고리의 다른 글
[Java] MDC 를 사용한 로그(Log)추적하기 (0) | 2022.07.30 |
---|---|
[Java] Lambda 정리 (0) | 2022.07.16 |
[Java] Lombok 정리 (0) | 2022.07.06 |
[Java] List 안에 있는 Map Value 찾기 (0) | 2022.01.15 |
[Java] 생성한 객체 배열 정렬하기(Comparator) (0) | 2022.01.09 |