[자료구조] String, StringBuffer, StringBuilder에 대해서

Hayoon·2023년 10월 9일
0

자료구조

목록 보기
4/5

String, StringBuffer, StringBuilder에 대해 궁금해진 이유

String, StringBuffer, StringBuilder는 모두 Java에서 문자열을 다루기 위한 클래스다.
코딩 문제를 풀 때 문자열 관련 문제에서 String[], String을 사용한다. 그리고 출력을 할 때에는 나는 StringBuilder를 사용한다. StringBuilder가 String보다 더 빠르기 때문에 조금이라도 시간초과를 줄이기 위함이다.(왜 빠른지도 몰랐음; 추가적으로 내장함수가 좀 더 유연하다?)
StringBuffer를 쓰는 사람도 있는데, 과연 이 셋의 차이는 뭘까?

String

Java에서 String은 불변(immutable) 객체다. 즉, 한 번 생성된 String 객체의 값은 변경될 수 없다. String 클래스에 있는 모든 메소드들은 원래의 문자열을 변경하는 것이 아니라 새로운 String 객체를 반환한다.
예를 들어 replace(), substring(), toLowerCase(), toUpperCase() 등과 같은 String 클래스의 메소드들을 사용하면 원래의 문자열 자체를 수정하는 것이 아니라 수정된 새로운 문자열을 가진 새로운 String 객체를 반환한다.

String str = "Hello";
str.toUpperCase();
System.out.println(str); // "Hello"

위 코드에서 출력되는 값은 "HELLO"가 아닌 "Hello"이다. 왜냐하면 str.toUpperCase() 호출이 원래 str 객체를 대문자로 바꾸는 것이 아니라 대문자 버전의 새 String 객체를 만들고 그것을 반환하기 때문이다. 따라서 원래 str 변수가 참조하는 String 객체("Hello")는 변하지 않는다.

String str = "Hello";
str = str.toUpperCase();
System.out.println(str); // "HELLO"

str = str.toUpperCase(); 라인에서 toUpperCase()가 대문자 버전의 새 String 객체를 만들고, 그 참조값을 다시 str 변수에 할당하게 된다. 이러한 특성 때문에 문자열 연산이 많은 경우 성능 저하가 발생할 수 있다.

💡 성능이 저하하는 이유는?
String 클래스의 불변성은 메모리와 성능 측면에서 단점이 될 수 있다. 위에서도 언급했지만, String 객체가 불변성을 가지기 때문에, 문자열에 대한 어떤 변경 작업이든 간에 항상 새로운 String 객체가 생성된다. 이는 메모리를 추가로 사용하며, 가비지 컬렉터(Garbage Collector)가 더 자주 동작하게 만들어 시스템의 전반적인 성능 저하를 초래할 수 있다.

StringBuilder나 StringBuffer 같은 가변성(mutable)을 가진 클래스를 사용하면 메모리 사용과 성능 저하 문제를 완화할 수 있다.
그러나 모든 상황에서 StringBuilder나 StringBuffer를 사용해야 하는 것은 아니다. 불변 객체는 내부 상태가 바뀌지 않기 때문에 쓰레드 안전(thread-safe)하고, 해시 테이블 등의 자료구조에서 키로 사용할 수 있다.

  • 쓰레드 안전: 여러 쓰레드가 동시에 접근하더라도 프로그램의 실행 결과가 올바르게 보장되는 것을 의미한다. 즉, 멀티 쓰레드 환경에서도 객체의 상태를 안정적으로 유지할 수 있다.
    예를 들어, 두 개의 쓰레드가 동일한 객체에 동시에 접근하여 값을 변경하는 경우, 어떤 순서로 작업이 이루어졌는지에 따라 결과가 달라질 수 있는데, 이런 상황을 'race condition'이라고 한다. 쓰레드 안전의 클래스나 메소드는 이러한 race condition을 방지하기 위해 동기화 메커니즘을 사용하여 한 번에 하나의 쓰레드만 해당 코드 영역을 실행하도록 한다.

  • 해시 테이블에서 Key로 String을 사용하는 이유: String은 한 번 생성되면 그 값이 변하지 않으므로, 같은 문자열은 항상 같은 해시값을 가진다. 따라서 String 객체를 키로 사용하면 일관된 해싱과 값 비교를 기대할 수 있다. 또한, String 객체가 불변성(immutable)을 가지므로 여러 곳에서 공유해서 사용해도 문제가 없다. 한 곳에서 변경하더라도 다른 곳에 영향을 주지 않기 때문이다.

StringBuffer

StringBuffer는 가변(mutable) 객체이다. 즉, StringBuffer 인스턴스는 자신의 내부 값을 직접 변경할 수 있다. 멀티 쓰레드 환경에서 안전하게 사용할 수 있도록 동기화를 지원한다.(thread-safe) 이는 StringBuffer의 메소드들이 synchronized 키워드를 사용하여 구현되어 있음을 의미한다.

💡 synchronized?
synchronized는 Java에서 제공하는 동기화 메커니즘이며, 한 번에 하나의 쓰레드만 해당 코드 블록을 실행할 수 있도록 한다. 다른 쓰레드가 이미 synchronized 블록을 실행 중인 경우, 그 쓰레드가 블록을 완전히 실행하고 나갈 때까지 해당 블록에 접근하는 다른 쓰레드들은 대기 상태로 만든다.

예를 들어, 여러 쓰레드가 동시에 StringBuffer 객체의 append() 메소드를 호출하더라도, 'synchronized' 키워드 때문에 한 번에 하나의 쓰레드만이 append() 메소드를 실행할 수 있다. 이런 방식으로 StringBuffer 클래스는 멀티 쓰레딩 환경에서도 안정적으로 문자열 조작 작업을 수행할 수 있다.

이러한 동기화는 추가적인 오버헤드(성능 저하)를 초래하기 때문에 단일 쓰레딩 환경에서는 비효율적일 수 있다. 따라서 단일 쓰레딩 환경에서 문자열 조작 작업이 많은 경우, 동기화 기능이 없어 오버헤드가 적은 StringBuilder 클래스를 사용하는 것이 일반적이다. (내가 StringBuilder를 출력문으로 사용하는 이유!)

💡 오버헤드 발생 원인?
컨텍스트 스위칭: 동시에 실행 가능한 쓰레드가 많은 경우, CPU는 이들 사이를 빠르게 전환하며 각각의 작업을 조금씩 처리한다. 하지만, 쓰레드가 synchronized 메소드나 블록을 기다리고 있다면, CPU는 대기 중인 쓰레드로 컨텍스트를 전환해야 한다. 이로인해 상당한 시간과 자원을 소모한다.

대기 시간: 여러 쓰레드가 synchronized 메소드나 블록에 접근하려고 할 때, 한 번에 하나의 쓰레드만 접근할 수 있으므로 나머지 쓰레드들은 대기 상태로 전환된다. 이런 방식으로 실행이 지연되거나 차단되는 것 역시 성능 오버헤드를 초래한다.

여러 쓰레드가 별도의 인스턴스 접근일 경우?

만약 여러 쓰레드가 각각 별도의 StringBuffer 인스턴스를 가지고 있다면, 그들은 서로 다른 메모리 영역을 사용하므로 동시에 append() 메소드를 호출해도 문제가 되지 않는다.
그러나 만약 여러 쓰레드가 같은 StringBuffer 인스턴스에 접근하려고 한다면, 같은 객체에 대해 여러 쓰레드가 동시에 변경 작업을 수행하는 것(race condition)을 방지하여 데이터 일관성을 유지할 수 있다.

StringBuilder

StringBuilder 클래스는 StringBuffer와 매우 유사하며, 둘 다 문자열을 변경할 수 있는 가변 객체이다. StringBuilder와 StringBuffer는 내부 문자열을 직접 변경하는 메소드를 제공하므로, 문자열 조작 작업이 많은 경우에 사용하면 효율적이다.

StringBuilder랑 StringBuffer 차이는?

바로 동기화 지원 여부다.

  • StringBuffer: 모든 주요 메소드가 synchronized로 동기화되어 있다. 이 때문에 멀티쓰레드 환경에서 안전하지만, 동기화 오버헤드 때문에 단일 쓰레드 환경에서는 비효율적.

  • StringBuilder: synchronized가 없으므로 동기화를 지원하지 않는다. 따라서 멀티 쓰레드 환경에서는 안전하지 않지만, 단일 쓰레드 환경에서는 동기화 오버헤드가 없으므로 더 빠른 성능을 보여준다.

단일 쓰레딩 환경에서 대량의 문자열 조작 작업이 필요한 경우: StringBuilder
멀티 쓰레딩 환경에서 안전한 문자열 조작이 필요한 경우: StringBuffer
위의 원칙에 따라 적절한 클래스를 선택하는 것이 중요 !

String과는 다르게 가변 객체(mutable object)를 키로 사용하는 경우, 해당 객체의 상태(값)이 변경되면 그에 따른 해시값 역시 변화하게 된다. 이것은 데이터 불일치를 초래할 수 있으므로 보통 가변 객체를 해시 테이블 등의 자료구조에서 키로 사용하는 것은 권장하지 않는다.

요약

  • String: 불변성(Immutable), 한 번 생성된 String 객체는 변경할 수 없음. 문자열을 조작하는 모든 작업은 새로운 String 객체를 생성
    쓰레드 안전(Thread-safe): 불변성 때문에 여러 쓰레드에서 동시에 사용해도 안전

  • StringBuffer: 가변성(Mutable), StringBuffer 객체는 변경 가능하며, 문자열 조작 작업은 원래의 객체를 직접 변경
    쓰레드 안전(Thread-safe): 모든 주요 메소드가 synchronized로 동기화되어 있으므로 멀티 쓰레드 환경에서 안전
    성능: 동기화 오버헤드 때문에 단일 쓰레딩 환경에서는 비효율적

  • StringBuilder: 가변성(Mutable), StringBuffer과 마찬가지로, StringBuilder 객체도 변경 가능하며, 문자열 조작 작업은 원래의 객체를 직접 변경
    쓰레드 비안전(Thread-unsafe): 동기화를 지원하지 않으므로 멀티 쓰레드 환경에서는 안전 X
    성능: 단일 쓰레드 환경에서 대량의 문자열 연산이 필요한 경우, 동기화 오버헤드가 없어서 StringBuffer보다 성능적으로 유리

profile
Junior Developer

0개의 댓글