최근 백준 문제를 python -> java로 바꾸면서 여러 공부를 다시 하고 있는 중인데, 일전에 사이드 프로젝트를 하면서 StringBuilder에 대해서 사용해본 경험이 있었지만, 잘 알고 쓰진않아 대충 무엇이 있다만 알고 있었다.
근데, 백준 문제를 풀면서 StringBuilder를 다시 한번 써보게 되어 이를 제대로 알아보고자 한다.
String, StringBuffer, StringBuilder 모두 문자열을 저장하고, 관리하는 대표적인 클래스로 각자 장단점이 존재한다.
먼저 String은 Immutable(불변)한 특성을 가져 다른 StringBuffer, StringBuilder와 차이가 존재
불변함은 즉 String 객체에 한번 값이 할당되면 그 공간은 변하지 않는다고 생각하면 된다.
그래서 할당된 공간이 변하지 않는 특성이라 해서 불변성이라 부른다.
여기서 잠깐! 그렇다면 불변(Immutable)과 가변(mutable)에 대해 더 알아보자!
String은 불변, StringBuffer와 StringBuilder는 가변의 성질을 가지는데, 이를 한번 확인해보도록 한다.
public class Main {
public static void main(String[] args) throws IOException {
String str = "str1";
StringBuffer sbf = new StringBuffer();
StringBuilder sbd = new StringBuilder();
sbf.append("sbf1");
sbd.append("sbd1");
System.out.println("String 객체의 주소는?: " + str.hashCode());
System.out.println("StringBuffer 객체의 주소는?: " + sbf.hashCode());
System.out.println("StringBuilder 객체의 주소는?: " + sbd.hashCode());
str = "changeStr1";
sbf.append("changeSbf1");
sbd.append("changeSbd1");
System.out.println("절취선 =================================");
System.out.println("String 객체의 주소는?: " + str.hashCode());
System.out.println("StringBuffer 객체의 주소는?: " + sbf.hashCode());
System.out.println("StringBuilder 객체의 주소는?: " + sbd.hashCode());
};
}
위의 코드를 입력을 해보니 아래와 같은 출력을 얻을 수 있었다.
먼저 String, Stringbuffer, StringBuilder 모두 타입의 변수를 선언해주었으며 수정전에 먼저 객체의 주소를 해싱하여 값을 반환해주는 hashCode()의 반환값을 출력한 다음 수정 후 반환값과 비교를 해보았다.
String은 3541024 -> -2131937296으로 변경
StringBuffer와 Builder는 동일함을 알 수 있다.
여기서 String의 객체 주소값만 변경된 것을 볼 수 있는데, 이를 조금 더 확인해보자
String 변수에 값을 할당 하는 방법은 2가지가 존재하는데,
이를 이용해 예시코드를 작성해보면
String str1 = "str";
String str2 = new String("str");
String str3 = "str";
String str4 = new String("str");
System.out.println(str1 == str2) -> false
System.out.println(str1 == str3) -> true
System.out.println(str2 == str4) -> false
분명 같은값을 넣었는데도 불구하고 boolean값이 모두 true가 아닌 이유가 무엇일까?
그건 String 타입 값 할당 방식에 따른 저장 방법이 상이하기 떄문이다.
이제 해당 부분에 대해 알아보자.
먼저 리티럴 변수로 할당 할 경우
String을 리터럴 값으로 할당하게 되면 Heap 메모리 영역 내 String Constant Pool에 저장되어 재사용된다
재사용된다는 의미는, 만약 String Constant pool안에 존재하는 리터럴 값을 사용하게 된다면 다시 만드는 것이 아닌, 메모리에 존재하는 값을 사용한다는 의미이다.
그래서 위에 str1 == str3이 true임을 알 수 있는 것이다.
왜? 이미 메모리에 존재하는 값을 사용하기 때문이다.
이 이미지를 보면 조금 더 쉽게 생각할 수 있을거 같다. s1이 Rick을 생성하여 저장하게 되면 s2가 Rick을 대입해도 String Pool에 이미 존재하는 값을 불러다 쓴다는 것을 볼 수 있다.
그럼 여기서 알아볼 수 있는것! 🐱👤
s1과 s2는 같은 rick을 사용하는데.. 왜 s3 rick은 새로 생성하는가요??.
그 이유는 String Constant Pool 을 이용하기 위해서는 오직 리터럴(Literal)을 이용한 방법만 가능하기 때문이다.
new 연산자를 이용한 생성자 주입
new 연산자를 통해 String 변수에 값이 할당되면 Heap 영역에 동적으로 메모리 공간이 할당되게 된다.
new 연산자를 이용하게 되면 String Pool에 저장되지 않아 같은 값을 넣게 되더라도 다른 메모리 공간을 참조하게 된다.
그래서 str2 == str4를 false가 나오게 되는 것이다.(새로운 메모리 공간이기에 서로 다르다.)
※ 요약 정리
리터럴(Literal) -> Heap영역 안 String Constant Pool에 저장
new 연산자 -> Heap
※ 조금 더 알아두면 좋을것
String 객체는 할당되면 메모리 공간이 변하지 않기 때문에 +연산자 혹은 concat메서드를 통해 기존 문자열에 새로운 문자열을 붙여도 새로운 객체를 생성하여 이를 저장하고 참조하도록 하여 사용한다.
StringBuffer와 StringBuilder는 문자열 연산 등으로 기존 객체의 공간이 부족하게 되면 기존의 버퍼 크기를 늘려 유연하게 동작합니다.
그럼 이제 이게 무슨말인지 알아보도록 하자.
StringBuffer와 StringBuilder는 AbstractStringBuilder
라는 추상클래스를 상속받아 구현되어 있는데,
이를 통해 두 클래스가 같은 메서드를 사용할 수 있는것이다.
먼저 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와 Builder에 문자열을 추가하게 되면 추가할 문자열 길이만큼 현재 문자열을 저장하면 배열 공간을 늘림과 동시에 추가된 문자열을 넣는 방식이다.
이것으로 append를 사용해 값이 변경되어도 같은 주소공간을 참조하게 되는것이며, 값이 변경되는 가변성의 특성을 가지게 되는 것이다.
두 클래스에 대해서 짤막하게 알아봤다. 그렇다면 이 두 클래스의 차이가 무엇이 있는지 알아보자
두 클래스의 차이점은 동기화(Synchronization)에 있다.
StringBuffer
는 동기화를 지원해 멀티스레드 환경에서도 안전하게 동작하지만, StringBuilder
는 동기화를 지원하지 않는다.
StringBuffer
는 메서드에 synchronized
키워드를 사용해 동기화가 가능한 것이다.
Java에서 synchronized 키워드는 여러개의 스레드가 한 개의 자원에 접근할 때
현재 접근중인 스레드를 제외한 나머지 스레드들이 자원에 접근하지 못하도록 막는 역할을 한다.
String은 짧은 문자열을 사용할 , 변하지 않는 문자열을 자주 사용할 때 사용한다.
또 위에 정리하지 않았지만, String이 불변한 이유에 대해서
캐싱이라는 단어를 사용한 블로그를 봤었는데,
이 내용이 위에서 설명한 String Pool과 관련된 내용이다.
그외 다른 블로그가 정리한 내용을 더 설명하면
동기화
-> 데이터가 불변하다면 Multi-Thread환경에서 동기화 문제가 발생하지 않아 안전한 결과를 나타낼 수 있다.
보안
-> 데이터가 가변적이면 특정 공격 백터에 의해서 무결성을 해칠 수 있다.
StringBuffer
는 동기화를 지원해 멀티스레드 환경에서도 안전하게 동작해 멀티스레드 환경과 삽입,삭제,수정이 빈번하게 발생하는 경우 StringBuffer가 좋을 수 있다.
StringBuilder
는 동기화를 지원하지 않아 StringBuffer
보다 속도면에서 좋다.
그래서 단일스레드 환경과 삽입,삭제,수정이 빈번하게 발생하는 경우 StringBuilder가 좋을 수 있다.
StringBuilder와 StringBuffer는 무슨 차이가 있는가?