Android/학습

[Kotlin] StringBuilder (String 메모리 저장 방식과 StringBuffer를 곁들인..)

한때미 2024. 12. 12. 04:34

 

StringBuilder는 변경가능한 문자열을 다루기 위한 객체인데,

우선 StringBuilder에 대해 알아보기 전에,

 

Kotlin에서 String의 메모리 저장 방식에 대해 알아보자.

 

String은 참조형 객체로 Stack과 Heap 메모리를 이용하여 값을 가져온다.

기본형 자료형이 Stack에 저장되는 것과 달리 Heap 메모리를 추가적으로 사용

더보기

참조형 객체는 변수에 객체에 실제 값을 저장하는 것이 아닌 값이 있는 주소값을 저장한다.

대충 요런식, 원시형과 참조형 비교 이미지

 

아차차, 참고적으로

Kotlin은 모든 자료형이 참조형 자료형이다!!

 

그렇기 때문에 그림에서 num: Int도 사실 st1: String처럼 참조형으로 연결되어야 한다.

차이를 비교하려고 저렇게 작성했으나 저 그림은 엄밀히 따지자면 맞지 않는 그림이다.

Java의 int형이라면 저런 방식으로 생성되었겠지만 kotlin은 모든 자료형이 참조형이기 조금 경우가 다르다.

 

물론,, kotlin이 원시 자료형의 이점을 얻기 위해 컴파일 시점에서 원시형 변환 등등 그런 경우가 있는데 여기서는 넘어가자!

저 그림이 대충 String 데이터의 메모리 구조는 저런 개념이다! 원시형과 이런 느낌으로 다르다!라는 느낌으로~~


구조적 동등성( == ) VS 참조 동등성( === )

  • 구조적 동등성 ( == )
    두 객체가 동일한 내용이나 구조를 가지고 있는지 확인
  • 참조 동등성 ( === )
    두 객체의 메모리 주소를 검증하여 두 객체가 동일한 인스턴스인지 확인

 


 

그렇다면, 다음 코드에서 어떤 결과를 출력하게 될까?

val st1 = "test"
val st2 = "test"

println(st1 == st2)
println(st1 === st2)

 

"test"라는 값이 Heap 영역에 동일하게 생성되어서 다른 주소값을 가리키게 될까?

그렇게 된다면 true, false가 나올 것이다.

 

 

하지만 정답은 true, true이다.

 

 

실제 실행 코드
실제 실행 결과

 

여기에서 일어나는 마법은 String Pool이다.


String Pool

JVM(Java Virtual Machine)이 메모리 사용을 최적화하고 성능을 개선하기 위해 String 객체 풀을 저장하는 메모리 힙의 특수 영역이다.

 

쉽게 말하면 "test" 이런 String 리터럴 방식으로 문자열 값을 생성한다면,

String Pool을 이용하여 이미 생성된 문자열인 경우 새로 생성하지 않고 기존에 있는 메모리 주소를 같이 참조하는 것이다.

 

String Pool을 통해 같은 주소값을 참조

 

"" : String 리터럴

 

 

그렇기 때문에 st1 , st2은 동일한 주소값을 가리키게 되는 것이다.


그렇다면 String 객체는 문자열이 동일하다면 모두 동일한 주소를 가리키게 되는 것일까?

그것은 아니다.

 

String 객체를 생성하는 방식은 대표적으로 다음과 같다

  • String 리터럴(Literal): ""
  • String 생성자(Object): String()

 

여기서 String 생성자를 사용하여 문자열을 생성할 경우 String Pool이 아닌 별도의 Heap 영역에 메모리가 할당된다.

 

대충 요런식

 

 

그렇다면 st1 === st3의 값은 false가 되는 것이다.

문자열이 동일하지만 동일한 주소를 가리키지 않음.


String 생성자

Java로 치면 new를 통해 new String("test")로 다음과 같이 객체를 생성할 것이다.

String st3 = new String("test");

 

Kotlin인 경우 String 생성자에 대해 다음과 같이 제공한다.

  • StringBuffer, StringBuilder, ByteArray, CharArray

 

Kotlin에서 제공하는 String 생성자

 

CharArray를 통해 String 객체를 생성하면 다음과 같다.

val st3 = String(charArrayOf('t','e','s','t'))

 

 

실제 실행 코드

 

실제 실행 결과

 

자, 그런데 여기서

println(st1[0] === st3[0])

 

이 코드에 대해서 어떻게 동작할 것인가?

st1과 st3은 "test"로 문자는 둘 다 't'를 가리킬 것이다.

 

그렇지만 st1은 String Pool에, st3은 heap 별개 영역에 생성되니 주소가 다르지 않나? false 아니야?

 


 

 

그렇지만,

정답은 true이다.

 

실제 실행 코드
실제 실행 결과

 

 

이게 어떻게 된 일일까?

우리가 Kotlin에 익숙해지면서 까먹고 있던 사실이 있다.

그것은 바로 String, 문자열은 원래..

 


문자열은 내부적으로 **Char 배열(CharArray)**로 저장된다.

즉, st1이 실제로 가리키는 메모리는 다음과 같이 생겼을 것이다.

String: "test"  -->  힙 메모리의 CharArray = ['t', 'e', 's', 't']

 

String 리터럴이든 String 생성자든 char의 시작 주소를 가리키는 String 객체이기 때문에 동일한 주소를 가리키는 것이다.

st1, st2의 주소는 서로 다르지만 "test"를 가리키는 주소(char 배열의 시작 주소)는 같다.

대충 요런식

 

엄밀히 말하자면,

실제 "test"라는 값을 가진 것이 아니라 ['t', 'e', 's', 't']의 시작 주소를 가리키고 있는 것이다.

** 주의 : ['t', 'e', 's', 't']의 char 배열 데이터는 실제로 String Pool 내부 또는 Heap 메모리 내부 중 어느 곳에 생성되는지 확인하지 않았습니다. 그림은 개념적으로 표현하였습니다. (String Pool 내부에 생성되지 않을 수 있음)

대충 이렇다는 말씀


't' 문자 객체를 생성하여 st1의 "test"의 't'와 비교
실행 결과

 


 

또, 이것과 별개지만 kotlin은 모든 객체가 참조 자료형으로 존재하지만 원시형 타입으로 컴파일되는 등 숨은 최적화 작업이 있다.

그래서 다음과 같은 이상한 동작을 선보이는 경우가 있는데.. 다음에 자세히 공부해 보면 좋을 것 같다.

To Be Continued...

 

Int형에 대한 주소값 비교
납득하기 어려운 결과


다시 돌아와서

String은 불변 객체이다.

 

이때까지 String이 메모리에 어떻게 저장되는지에 대해 살펴보았는데,

결국은 불변 객체로 저장된다는 것이다.

 

 

예를 들어,

var strTest = "test String"를 할 경우

"test String"에 대해 저장하고

 

strTest = "test String not work"를 할 경우

"test String not work"를 가리키는 새로운 참조 메모리를 생성하여 할당할 것이다.

 

그리고 기존에 "test"를 가리키는 메모리를 해제할 것이다.

Heap 메모리는 참조가 없을 경우 GC에서 정리한다.

 

 

그렇기 때문에 문자열을 계속 변경해줘야 하는 작업을 할 때

메모리 할당과 해제를 반복하는 낭비를 하게 된다.

 

 


 

StringBuilder

StringBuilder는 String과 다르게 변경 가능한 문자열을 지원해 준다.

즉, 문자열을 변경해도 객체를 새로 생성하지 않고 기존 객체를 계속 사용한다.

 

그렇기 때문에 잦게 문자열이 변경되거나 여러 문자열 조작 작업을 효율적으로 수행할 수 있다.

물론 String 보다 처리 속도가 느린 편이다.

어떻게?

StringBuilder는 초기화되는 문자열보다 여유로운 용량의 버퍼 공간을 만들어둔다.

그리고 그 버퍼 공간을 이용하여 문자열 변경에 대응한다.

 

StringBuilder 생성

 

 

StringBuilder vs StringBuffer

StringBuffer는 StringBuilder와 동일한 변경가능한 문자열을 지원해 준다.

StringBuilder와 큰 차이점은 동시성 문제를 해결해 준다는 것이다.

 

StringBuffer는 StringBuilder와 동일하게 AbstractStringBuilder를 상속받는다.
StringBuilder의 함수이다.
synchronized 작업이 되어 있는 StringBuffer 함수이다.

 

 

그렇기 때문에 멀티 스레드 환경처럼 여러 스레드에서 해당 객체 접근해야 할 경우 StringBuffer를 사용해야 예상치 못한 동작을 방지한다.

 


추가적으로... 

 

JDK9 invokeDynamic을 활용하여 String 성능이 개선되었다고 한다.

그래서 실제로 String을 이용한 문자열 변경과 StringBuilder를 사용한 경우에서 크게 속도 차이가 발생하지 않는다고 한다.

 

또한, StringBuilder는 변경한 문자를 일종의 포인트로 가리키는 개념이라 실제 String보다 처리 속도가 느린 부분이 있는데 변경가능한 문자열을 어떻게? 에 대한 부분에서 메모리 시점에서 좀 더 자세히 알아보면 좋을 것 같다.

 

이번에는 String에 대해 초점을 맞춰 알아봤다.

앞에 나온 부분과 StringBuilder, StringBuffer에 대해 좀 더 조사해 봐도 좋을 것 같다.

 

 


참고자료

더보기