Java 문자열 연산 비교: + 연산 vs StringBuilder vs StringBuffer

Java에서 String 객체는 불변(immutable)입니다. 한번 생성된 문자열은 변경할 수 없으며, 이러한 특성 때문에 문자열 연산을 할 때 다양한 방식이 존재합니다. 매번 '+' 연산자만을 이용해서 문자열을 합친다면 큰 문제가 발생할 수도 있습니다. 

Java 공식 문서의 Class String 부분을 보면 제일 윗 부분에 "모든 문자열은 String 클래스의 인스턴스로 구현되어 있고, 상수이며, 생성된 후에는 값을 변경할 수 없다"라고 명시되어 있습니다.

https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/String.html


'+' 연산자를 이용한 문자열 연결

'+' 연산자는 가장 일반적인 문자열 연결 방법입니다. 적은 양의 문자열 연결에 적합합니다. 이 적은 양이 어느 정도 인지가 애매한데 이건 아래에서 성능 비교를 해보면서 다시 알아보겠습니다.

JDK 1.5 이후의 컴파일러 최적화

JDK 1.5부터 Java 컴파일러는 String의 '+' 연산을 내부적으로 StringBuilder를 사용하여 최적화 합니다.

// 내가 작성한 코드
String result = "Hello" + " " + "World";

// 컴파일러가 변환하는 코드 (JDK 1.5 이상)
String result = new StringBuilder().append("Hello").append(" ").append("World").toString();

한 줄 vs 여러 줄 String 연산의 차이

한 줄에서 여러 문자열을 연결할 때는 하나의 StringBuilder 객체만 생성되어 효율적으로 작동합니다.

// 한 줄 연산 - 효율적 (하나의 StringBuilder 사용)
String result = "Hello" + " " + "World" + "!";

하지만 여러 줄에 걸쳐 '+' 연산자를 사용하면, 각 구문마다 새로운 StringBuilder 객체가 생성되어 성능이 저하됩니다.

// 여러 줄 연산 - 비효율적 (여러 StringBuilder 객체 생성)
String result = "Hello";
result += " "; // StringBuilder 생성
result += "World"; // StringBuilder 생성
result += "!"; // StringBuilder 생성

특히 반복문 안에서의 '+' 연산은 매 반복마다 새로운 StringBuilder 객체를 생성하므로 매우 비효율적입니다.

// 반복문 안에서의 사용 - 심각한 성능 저하
String result = "";
for (int i = 0; i < 10000; i++) {
result += "a"; // 매 반복마다 새 StringBuilder 객체 생성
}


StringBuilder 활용

StringBuilder는 변경 가능한(mutable) 문자열 클래스로, 문자열 조작 작업에 최적화되어 있습니다. 새 객체 생성 없이 기존 객체를 수정할 수 있어 대량의 문자열 연산에서 '+' 연산자보다 월등히 뛰어난 성능을 보입니다. 다만 동기화를 지원하지 않아 스레드에 안전하지 않으며, '+' 연산자보다 코드가 다소 복잡해질 수 있습니다.

StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
String result = sb.toString();

// 루프에서의 효율적인 사용
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append("a");
}
String result = sb.toString();


StringBuffer 활용

StringBuffer는 StringBuilder와 기능적으로 동일하지만, 스레드에 안전하다는 차이점이 있습니다. StringBuilder와 동일한 API를 제공하며 멀티스레드 환경에서 안전하게 사용할 수 있습니다. 하지만  StringBuilder보다 성능이 떨어지기 떄문에 멀티스레드 환경이 아니면 StringBuilder를 사용하는 게 좋습니다.

StringBuffer sb = new StringBuffer();
sb.append("Hello");
sb.append(" ");
sb.append("World");
String result = sb.toString();


성능 비교

실제 성능 차이를 확인하기 위한 간단한 벤치마크 테스트를 해봤습니다. 결과는 아래와 같습니다. + 연산자가 압도적으로 오래 걸리고, StringBuilder와 StringBuffer의 차이는 미미합니다.

+ 연산자: 751ms
StringBuilder: 1ms
StringBuffer: 2ms

public class StringPerformanceTest {
public static void main(String[] args) {
int iterations = 100000;

// + 연산자
long startTime = System.nanoTime();
String result = "";
for (int i = 0; i < iterations; i++) {
result += "a";
}
long endTime = System.nanoTime();
System.out.println("+ 연산자: " + (endTime - startTime) / 1000000 + "ms");

// StringBuilder
startTime = System.nanoTime();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < iterations; i++) {
sb.append("a");
}
result = sb.toString();
endTime = System.nanoTime();
System.out.println("StringBuilder: " + (endTime - startTime) / 1000000 + "ms");

// StringBuffer
startTime = System.nanoTime();
StringBuffer sbf = new StringBuffer();
for (int i = 0; i < iterations; i++) {
sbf.append("a");
}
result = sbf.toString();
endTime = System.nanoTime();
System.out.println("StringBuffer: " + (endTime - startTime) / 1000000 + "ms");
}
}


사용 가이드라인

문자열 연산 방식을 선택할 때는 사용 환경과 상황을 고려해야 합니다. 단순한 문자열 연결이나 한 줄에서 이루어지는 적은 양의 연결 작업에는 '+' 연산자가 충분합니다. 여러 줄에 걸친 문자열 연산이나 반복문 안에서의 문자열 조작에는 명시적으로 StringBuilder를 사용하는 것이 성능 상 유리합니다. 멀티스레드 환경에서 문자열 작업이 필요한 경우에는 동기화를 지원하는 StringBuffer를 사용하는 것이 안전합니다.


결론

솔직히 대부분의 상황에서는 뭘 쓰든 체감이 안될 것 같기도 하지만 + 연산, StringBuilder, StringBuffer의 차이를 모르고 있다면 언젠 가는 큰 낭패를 볼지도 모를 일입니다. 귀찮더라도 적절한 문자열 연산 방식을 선택하는 습관을 가지도록 합시다.

댓글

이 블로그의 인기 게시물

Spring Boot Initializr 사용법 상세 가이드

SQL 테이블 ALIAS 규칙: 가장 효과적인 방법과 장단점 비교

PostgreSQL 로컬 PC(윈도우)에 설치하기 - 17.4버전 기준