String
- 불변 객체
- 한 번 생성되면 할당된 메모리 공간이 불변이다.
+
연산자 또는 concat 메서드를 통해 문자열을 합치면, 기존에 생성된 String 클래스에 문자열을 붙이는 것이 아니라, 새로운 String 객체에 두 문자열을 합쳐서 리턴한다.- 새로운 객체를 생성하기 때문에, 문자열 연산이 많은 경우 성능이 좋지 않다.
- 하지만, 간단하게 사용 가능하고 변경될 가능성이 거의 없는 경우, 동기화 문제를 신경쓰지 않아도 된다.(Thread-safe, 자원 공유 가능)
🤔Java에서 왜 String을 불변으로 설정했을까?
❗캐싱, 동기화, 보안 등에 이점이 있다.
- 캐싱: String Constant(상수) Pool에 각 리터럴 문자열의 하나만 저장, 캐싱하여 재사용 가능하며, 힙 공간을 절약할 수 있다.
- 해시코드 캐싱:hashCode()
구현 시 최초 1번만 계산 로직을 수행하고, 이후에는 이전에 계산한 값을 return 한다. String이 불변이기 때문에 Cashing을 활용할 수 있다.- 동기화: 불변이므로 값이 바뀔 일이 없어, 멀티스레드 환경에서 Thread-safe하다.
보안: 사용자 이름, 암호, 네트워크 연결 등과 같은 민감한 정보를 저장할 때, 불변 객체이므로 조작으로부터 안전하다. (SQL injection, 키 변경 등 불가)
StringBuffer, StringBuilder
- 가변 객체
- 내부 Buffer(데이터를 임시로 저장하는 메모리)에 문자열을 저장해두고 그 안에서 추가, 수정, 삭제 작업을 수행
- 문자열 연산 등으로 메모리 공간이 부족할 경우, 버퍼의 크기를 유연하게 늘릴 수 있다
- 동일 객체 내에서 문자열 크기를 변경할 수 있다.
- StringBuilder는 동기화를 지원하지 않는 반면, StringBuffer는 동기화를 지원한다. (StringBuffer는 메서드에서 synchronized 키워드를 사용)
- StringBuilder가 동기화 관련 로직을 수행하지 않으므로, 성능 측면에서 이점이 있다.
- 그러나, Thread-safe하지 않으므로, web, socket환경 등의 비동기 환경일 경우, StringBuffer를 사용하는 것이 안전하다.
멀티스레드 환경에서의 동작 차이
두 개의 스레드에서 StringBuffer, StringBuilder에 각각 1을 1만번 더하는 연산을 수행해보자. 우리는 두 문자열의 길이가 2만번일 것으로 예상하지만(두 개의 스레드에서 각각 만 번씩 추가하므로), StringBuffer, StringBuilder의 길이가 다르다는 것을 볼 수 있다.
StringBuffer는 동기화를 지원하여 Thread-safe하므로 문자열의 길이가 2만이지만, StringBuffer는 동기화를 지원하지 않아 문자열의 길이가 2만보다 작다.
import java.util.*;
public class Main extends Thread{
public static void main(String[] args){
StringBuffer stringBuffer = new StringBuffer();
StringBuilder stringBuilder = new StringBuilder();
new Thread(() -> {
for(int i=0; i<10000; i++){
stringBuffer.append(1);
stringBuilder.append(1);
}
}).start();
new Thread(() -> {
for(int i=0; i<10000; i++){
stringBuffer.append(1);
stringBuilder.append(1);
}
}).start();
new Thread(() -> {
try{
Thread.sleep(2000);
System.out.println("StringBuffer.length: "+ stringBuffer.length()); //20000, 동기화 지원
System.out.println("StringBuilder.length: "+ stringBuilder.length()); //16343, 동기화 미지원
}catch(InterruptedException e){
e.printStackTrace();
}
}).start();
}
}
성능 비교
String의 +
연산, StringBuffer, StringBuilder의 append()
연산의 성능을 비교해보자. 각각의 문자열에 "*"을 5만번 더한다.
연산 시간은 String > StringBuffer > StringBuilder 순으로, StringBuilder가 가장 빠른 것을 볼 수 있다. String 객체는 매번 새로운 객체를 생성해야하므로 수행시간이 기하급수적으로 늘어나지만(보통 10만번 까지 버틴다), StringBuilder, StringBuffer는 1000만번까지는 버티며 그 점점 StringBuilder와 성능차이가 나는 것을 볼 수 있다.
public static void main(String[] args) {
final int N = 50000;
String str = "";
long start1 = System.currentTimeMillis(); // 시작 시간
for (int i=0; i < N; i++){
str += "*"; //concat이 더 빠름
}
long end1 = System.currentTimeMillis(); // 종료 시간
StringBuffer stringBuffer = new StringBuffer();
long start2 = System.currentTimeMillis(); // 시작 시간
for (int i=0; i < N; i++){
stringBuffer.append("*");
}
long end2 = System.currentTimeMillis(); // 연산에 걸린 시간
StringBuilder stringBuilder = new StringBuilder();
long start3 = System.currentTimeMillis(); // 시작 시간
for (int i=0; i < N; i++){
stringBuilder.append("*");
}
long end3 = System.currentTimeMillis(); // 연산에 걸린 시간
System.out.println("String 연산: " + (end1 - start1));
System.out.println("StringBuffer 연산: " + (end2 - start2));
System.out.println("StringBuilder 연산: " + (end3 - start3));
}
'Language > java' 카테고리의 다른 글
java 기초: 지역변수와 스코프, 형변환 (0) | 2024.08.22 |
---|---|
JAVA 14: 새로운 switch문 (0) | 2024.08.19 |
자바란: 자바 표준 스펙과 구현, 컴파일과 실행 (0) | 2024.08.13 |
DI(Dependency Injection, 의존성 역전)과 Spring IoC (0) | 2023.03.04 |
java: Optional (0) | 2023.01.31 |