w1-1- StringBuilder와 StringBuffer 차이

Sangwon Na·2021년 8월 15일
1
post-thumbnail

📌 과제 설명

java에서는 대표적으로 3가지의 클래스를 사용하여 문자열을 조작 할 수 있습니다.

  • String
  • StringBuilder
  • StringBuffer

String 클래스와 마찬가지로 StringBuilder와 StringBuffer는 문자열을 변경 할 수 있는 클래스입니다.

다만 다른점은 String불변(immutable) 의 속성을 갖고 있으며, StringBuilder/StringBuffer가변성(mutable) 가지고 있습니다.



❔ 가변성과 불변성이 무엇인가요?

1️⃣ 가변성(mutable) : 일정한 조건에서 변할 수 있는 성질.

class MutablePerson {
public int age;
public int name;

public MutablePerson(int age, int name) {
	this.age = age;
	this.name = name;
	}
}

// 외부에서 getter, setter를 통해 age와 name을 바꿀 수 있습니다.

2️⃣ 불변성(immutable) : 변하지 아니하는 성질.

String str= "a";
str= "ab";
System.out.println(str); // 실행결과: ab

출력결과가 a가 아닌 ab가 나오므로 해당 코드는 불변성이 아니지 않나라고 생각하겠지만, 여기에서 말하는 변하지 않는 성질은 자신의 주소를 의미합니다.
str은 str의 주소값을 가지고 있고, 값으로 "a"를 바라보고 있습니다, 그러다가 "ab"라는 새로운 객체를 만들고 그 주소를 str이 바라보게(참조)하는 것입니다.

즉 str은 본연의 주소값은 바뀌지 않았습니다.

쉽게 설명하자면 제가 지나가는 사람을 가르킨다고 그 사람이 된 게 아니라는 겁니다. 저(str)는 저(str)이기 때문이죠. 이를 불변성을 가졌다고 합니다.

자바 언어는 byte, short, int, long, char, boolean, float, double 총 8개의 기본 Primitive Type 자료형 외에는 대부분 Reference Type (참조타입)입니다.

참조변수들을 실행될 때마다 데이터들을 스택메모리 영역에 뒀다 뺐다 하는게 비효율적이므로, 힙 영역에 내용(값)이 저장되고, 스택 영역은 힙 영역(공동영역)에 올라간 값의 주소값을 참조(저장)하게 됩니다.




❔ 가변성과 불변성을 만든 이유는 뭘까?

자바 프로그램은 JVM의 Runtime Data Area로 올라가서 실행하게 됩니다.


영역설명
MethodJVM이 시작될 때 생성되는 공간으로,
클래스 정보, 변수 정보, static으로 선언한 변수가 저장되고 모든 스레드가 공유하는 영역
Stack지역변수나 메서드의 매개변수, 임시적으로 사용되는 변수, 메서드의 정보가 저장되는 영역
Heap동적으로 생성된 객체(인스턴스)가 저장되는 영역으로 GC의 대상이 되는 공간
PC Register스레드가 시작될 때 생성되며, 현재 수행중인 JVM의 명령어 주소를 저장하는 공간입니다.
스레드가 어떤 부분을 어떤 명렁어로 수행할지를 저장하는 공간입니다.
JNIJAVA가 아닌 다른 언어로 작성된 코드를 위한 공간입니다.

위 그림에서 눈여겨 볼곳은 힙영역은 모든 스레드가 공유하고, 나머지 공간(Stack, PC Register, Navtive Method Stack)에서는 스레드가 하나씩 공간을 차지하여 사용합니다. 이 말은 Heap영역에 저장된 객체는 다른 스레드가 가져다 사용 할 수 있다는 말입니다. 따라서 중복되는 객체가 있다면, 공유(힙영역)에 두고 가져다 쓰면 하나씩 차지하는 스택보다 메모리 공간에 이점이 있습니다. 따라서 스택메모리 영역에 매번 똑같은 객체를 넣었다 뺐다 하는것보단 힙영역에 두는게 좋습니다.

1️⃣ 다른 스레드가 가져다 사용 할 수 있다는게 무슨 뜻이고, 무엇이 좋아지나요?

public class StringClassExam {
 public static void main(String[] args) {
         String str1 = "Constant Pool";
         String str2 = "Constant Pool";
         System.out.println(str1 == str2); // 실행결과: true
 }
}

// 이쯤에서 등장하는 Connstant Pool !!!
// Constant Pool 상수풀
// String Pool 스트링풀

str1str2 는 스택영역에 서로 다른 스레드로 생성되어, 비교를 하면 false가 나올 것 같았는데, 실행결과는 true가 나왔습니다!

상수 풀(Constant Pool)은 상수(리터럴)을 저장하는 Heap 영역에 있는 공간입니다.

String 뿐 아니라, 모든 종류의 숫자, 문자열, 식별자 이름, Class 및 Method 에 대한 참조와 같은 값이 저장됩니다.

이에 대한 이점은 똑같은 값이 이미 힙영역 메모리에 올라가있다면, 매번 새롭게 만들 필요 없이 주소값만 가져다 쓰면 메모리 공간 절약 에 이점을 얻을 수 있기 때문입니다. 따라서 str1과 str2는 "String Pool"의 주소를 가르키고 있기 때문에 둘은 같다고 볼 수 있습니다.
⚠ Constant Pool은 클래스단위로 관리되기때문에, 한 Class에 등장하는 동일한 리터럴 값은 여러번 메모리에 Load되지 않지만, 다른 클래스라면 중복으로 리터럴 값이 메모리에 여러번 올라갑니다. 그렇다면 위 코드는 서로 다른 클래스 String str1String str2 은 왜 같다고 하는 걸까요?

그 정답은 Java String 처리의 특징 중에 하나인 String Pool을 이용하기 때문입니다. (자주 쓰기 때문에, String타입만을 위해 존재!)
따라서 String클래스는 같은 문자열 리터럴이라면 같은 곳을 참조하게 됩니다.(String Pool)

 String str3 = "리터럴 방식으로 생성"; // Constant Pool처럼 String 리터럴을 넣는 걸 String Pool이라 합니다.
 String str4 = new String("new 방식으로 생성"); // intern()을 사용하면 새롭게 객체를 생성해도 동일한지 비교가 가능합니다.

즉 서로 다른 String 클래스(new방식)로 만든게 아니라면, String 클래스(리터럴)는 같습니다.
String Constant Pool에서 주소값을 가져왔기 때문에 str1과 str2는 같습니다.







intern()을 사용하면 새롭게 객체를 생성해도 동일한지 비교가 가능합니다.

	public class StringPoolExample  
	{  
		public static void main(String[] args)   
		{  
			String s1 = "Java";  
			String s2 = "Java";  
			String s3 = new String("Java");  
			String s4 = new String("Java").intern();  
			System.out.println((s1 == s2)+", String are equal."); // true  
			System.out.println((s1 == s3)+", String are not equal."); // false  
			System.out.println((s1 == s4)+", String are equal."); // true  
		}  
	}  
  • JVM에서 관리하는 문자열 풀에서 해당 문자열을 조회하여 존재하는 경우 반환, 아닌 경우 풀에 문자열을 등록하고 해당 문자열을 반환하는 메서드이면서 이 과정 자체를 작동하게 합니다.
  • String 객체는 불변 객체이기 때문에 동일한 객체가 공유될 수 있는 특징을 가지고 있습니다.
  • 이 특징을 잘 활용하기 위해서는 동일한 문자열을 가지는 String 객체가 단 하나만 존재하도록 유지할 필요가 있습니다.
  • JVM 내부에서 초기에는 비어있는, 문자열 객체를 관리하는 풀(pool)을 생성합니다.
  • 이후, String 객체의 intern 메서드가 호출되면 이 풀에 해당 문자열과 같은(String.equals() 반환값이 true인) String 객체가 존재하는 경우 해당 객체를 반환하고, 존재하지 않는 경우 해당 객체를 풀에 추가하고 해당 객체를 반환하게 됩니다.



2️⃣ 그래서 String을 불변객체로 만든 이유가 뭐예요?

  1. String Pool의 사용이 가능하다.
  • Java에서 가장 많이 사용하는 자료형은 String인데, 매번 새롭게 생성할 필요 없이 중복된 값들은 Heap영역에서 주소값을 참조 할 수 있기 때문에
  1. 보안상 위협으로부터 지켜낼 수 있다.
  • Database Connection 시 username, password 등이 가변형이면 참조되는 값을 변경 할 수 있기 때문에 보안상 취명적이기 때문에
  1. multi-thread 환경에서 안전하다
  • 여러개의 thread가 동시에 접근한다 하더라도 항상 동일한 값을 보장하기 때문에
  1. 불변객체이기 때문에 항상 동일한 hashcode를 반환한다.
  • 불변객체이기 때문에 생성되는 시점 이후부터는 항상 동일한 hashcode를 반환합니다.

    이 덕분에 매번 새로운 hashcode를 계산할 필요가 없기 때문에 Map 객체의 key를 String으로 줍니다.



    3️⃣ 지금까지 내용은 보면, 불변객체가 좋은것 같은데, 가변객체는 언제 쓰나요?

    String 클래스와 마찬가지로 StringBuilder와 StringBuffer는 문자열을 변경 하는 클래스라고 서론을 시작하였습니다.

    말 그대로 문자열의 수정을 할 경우 (정확히는 자주 수정하는 경우), 불변성은 오히려 해가 됩니다.

    public static void main(String[] args) {
        String str = "OOP";
        str += " Hard!";
    }

    Heap영역에 String Pool에는 "OOP"라는 String이 있어서, 뒷부분에 문자열을 붙여서 쓸것 같지만, "OOP Hard!"라는 객체가 없어서 Heap영역에 새롭게 객체가 생성되었습니다.

따라서 String 문자열을 수정할때마다 매번 Heap 메모리영역에 불필요하게 공간을 차지합니다.
그래서 한번 생성된 "OOP" 객체 메모리 주소에 수정된 문자열을 저장하는게 좋겠다는 생각을 하여, StringBuilder와 StringBuffer가 탄생하게 되었습니다.

문자열 빌더 클래스 (oracle.com) 의 내용을 보면, 기본적으로 용량이 빈 가변길이 16개의 배열을 만드는 걸 확인할 수 있습니다.

당연히 문자열의 길이가 16을 넘어서면 capacitu()가 계산하여 길이를 늘립니다.

문자열을 배열처럼 다루게 된다면, index를 사용하여
문자열 뒷부분부터 추가하거나 .append

시작 index와 끝index를 지정하여 특정문자열 부위를 지우거나 .delete
추가 등을 할 수 있습니다. .insert

❔ StringBuilder와 StringBuffer의 차이는 무엇인가요?

구분StringStringBufferStringBuilder
StorageString poolHeapHeap
Modifiable(변경 할 수 있는)No(immutable)Yes(mutable)Yes(mutable)
Thread safeYesYesNo
SynchronizedYesYesNo
PerformanceFastSlowFast

멀티스레드 지원 유무입니다.
메모리 힙영역은 여러 스레드가 동시에 접근 가능하기 때문에 문제가 발생할 수 있습니다.

따라서 StringBuffer에서는 synchronized을 지원하여, 스레드 안전(영어: thread safety)을 추구하였습니다.

thread safety란?
멀티 스레드 프로그래밍에서 일반적으로 어떤 함수나 변수, 혹은 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음을 뜻합니다.

⚠ StringBuilder는 멀티스레드를 지원하지 않으므로, StringBuffer보다 빠릅니다.

마우스 커서 올린 후 Ctrl + Click 시 각 Class가 어떻게 정의되어 있는지 코드를 확인 할 수 있습니다.

	// StringBuilder - append() 시
	@Override
    @IntrinsicCandidate
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

	// StringBuffer  - append() 시 - synchronized 가 추가되어 있는걸 확인이 가능합니다.
    @Override
    @IntrinsicCandidate
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }





👩‍💻 요구 사항과 구현 내용

        String a = "Hello";
        a = a + " World";
        String b = "Hello World";

        System.out.println("a : " + a); //Hello World
        System.out.println("b : " + b); //Hello World
        System.out.println(a == b); // false

위에 설명한 것처럼 String은 Heap영역에 매번 새로운 객체를 생성하게 됩니다. 따라서 서로 다른 객체라서 false를 반환합니다.
String 문자열을 조작할때마다 불필요하게 메모리 공간을 차지하는 것을 방지하기 위해 StringBuilder와 StringBuffer가 나왔습니다.


        StringBuilder sb = new StringBuilder();
        sb.append("Hello");
        sb.append(" World");
        System.out.println(sb.toString()); //Hello World

.append 를 여러번 써서, 문자열을 추가하면 String처럼 Heap영역에 여러개 생성되는게 아닐까 생각할수도 있습니다. (그럼 쓸 필요가 없죠!)
따라서 StringBuffer와 StringBuilder는 .toString() 을 할 때 지금까지 문자열 조작(.append같은)한 결과를 반환합니다.
즉 문자열 연산을 매번 하더라도 새로운 객체가 매번 생성되지 않습니다.





✅ PR 포인트 & 궁금한 점

힙과 스택에 대해 다시 한번 개념정리를 하였고, String에서 Constant Pool과 String Pool 대해 새롭게 알게 되었습니다.
멀티스레드에 대해선 아직 잘 몰라서, 마무리 부분을 급하게 끝낸것 같아서, 지속적으로 업데이트 해나갈 생각입니다.






Reference

자바 메모리 구조 뿌시기 JVM이란? - YouTube

[Javascript] 불변객체? 불변성? 그게 뭐에요?!! :: Efforts never betray you.

Java - Constant pool과 String pool - 조금 늦은, IT 관습 넘기 (JS.Kim) (breakingthat.com)

The StringBuilder Class (The Java™ Tutorials > Learning the Java Language > Numbers and Strings) (oracle.com)

스레드 안전 - 위키백과, 우리 모두의 백과사전 (wikipedia.org)

profile
나상원의 LOG

0개의 댓글