본문 바로가기

프로그래밍/JAVA

String 클래스 깊숙히 이해하기



이번 강좌에서는 String 클래스를 좀더 깊숙히 알아보는 시간을 갖고자 합니다.

 

우선 String = "12"; 즉, 스트링 객체에 12 라는 문자열이 들어 있습니다.

 

위 String 객체에 담긴 12 라는 문자열은 내부적으로 어떻게 처리되어 저장될까요?

 

일단 좀더 깊숙히 들어가 봅시다.

 

첫번째, String 객체의 내부 비밀

 

String 객체는 final 한 클래스로서 상속(확장) 이 불가능 합니다.

또한, String 객체는 내부적으로 char 배열에 데이터를 저장하여 보관하고 있습니다.

 

 

 

 

<그림 1> String 클래스

 

 

 

 

<그림 2> String 생성자

 

 

 

 

 

실제 String str = new String("ABC") 한개의 String 객체를 생성할때

 

생성자 내부적으로 String 객체를 char 배열로 변환해서 저장합니다. (위 그림 참조)

 

즉, value 라는 char 배열에 문자열이 들어간것이 아니라,

 

char 배열안에 한문자 한문자 들어가 있게 되는 겁니다.

 

value[0] = 49;

value[1] = 50;

 

이런식으로 말이죠.

 

아니? 잠깐!! 왜 1,2 가 들어간게 아닐까요?

 

자 한번더 깊숙히 들어가 봅시다.

 

컴퓨터는 내부적으로 모든 데이터의 표현 및 연산을 2진수로 처리 합니다.

 

코드상에서 겉보기엔 문자를 처리하는것 처럼 보이지만,

 

내부적으로 숫자밖에 인식하지 못합니다.

 

그래서 미국표준협회(ANSI) 에서는 문자표현에 대한 표준을 아스키(ASCII) 코드를 탄생시키게 됩니다.

 

문자 a는 숫자 97, 문자 b 는 숫자 98...

 

이런식으로 약속한것이 아스키코드 입니다.

 

 

 

<그림 3> 아스키코드 표 

 

 

자 그럼 다시 올라가 봅시다.

 

String 객체에 "12" 가 있다면 내부적으로 char 배열에 아래와 같이 저장된다고 했습니다.

 

value[0] = 49;

value[1] = 50;

 

자 이해가 가시나요?

 

12 는 10진수로 저장된게 아닙니다.

 

이미 12는 문자로 인식하고 있기 때문에

 

문자 0 은 10진수 49

문자 1 은 10진수 50

 

이렇게 내부적으로 매핑하여 저장되어 있는겁니다.

 

위 아스키 코드표를 참고해보세요.

 

 

 

이렇게 char a 변수에 문자 a 를 넣고 1을 곱하는 연산이 가능한 이유가 바로 내부적으로

 

10진수로 저장하고 있기 때문에 가능 한겁니다.

 

위 코드를 출력하면 97이 나옵니다. 왜냐하면 10진수 97은 아스키코드 문자로 a 이기 때문 입니다.

(이래서 C를 꼭 먼저 접해 보라는 말이 나오는 이유 입니다.)

 

 

두번째, String 객체는 리터럴 방법으로 생성 하는것과 new 로 생성 한다면? (상수풀에 대한 이해)

 

String 객체는 리터럴("") 로 생성하는 경우 JVM 메모리에 있는 상수풀(Constant Pool)로 들어갑니다.

 

두번째 생성하는 String 객체 역시 처음 생성했던 객체와 동일한 문자열을 가지는 경우 같은 레퍼런스를 참조 하게 됩니다. 

 

String str1 = "ABC";

String str2 = "ABC";

 

즉 내부적으로 intern 을 호출하여 상수풀에 등록된 스트링의 레퍼런스를 가지게 됩니다.

 

intern 메소드는 무엇일까요?

 intern 메소드는 String이 상수풀(Constant Pool)에 등록된 경우 해당 스트링의 주소값을 반환하는 역할을 합니다.

 

그렇기 때문에 위 String 객체를 == 연산하는 경우 같은 주소를 가지는 이유 입니다.

 

 

하지만 new 로 생성되는 경우 어떻게 될까요?

 

String str3 = new String("ABC");

String str4 = new String("ABC");

 

위 str3 == str4 는 서로 false 즉 서로 다른 레퍼런스 입니다.

 

메모리 힙 영역에 서로 다른 객체로 자리매김 하고 있습니다.

 

자 그럼 리터럴로 생성된 String 처럼 str3 와 str4 를 같은 레퍼런스를 가지게 할 수 있을까요?

 

정답은 가능하다 입니다.

 

String str3 = new String("ABC").intern();

String str4 = new String("ABC").intern();

 

위 객체를 == 연산하는 경우 true 가 됩니다.

 

왜냐하면 처음 str3 객체가 생성됨과 동시에 상수풀에 등록이 되고,

str4 객체가 생성이 되면서 ABC 문자열과 동일한 String 객체가 상수풀에 있기 때문에 같은 레퍼런스를 가지게 됩니다.

 

 

 

세번째, String 객체는 바뀌지 않는 변할 수 없는 객체다.

 

많은 사람들이 String 객체를 사용시 변수처럼 사용하곤 합니다.

 

String str4 = "ABC";

str4 += "DEF";

 

간단한 문자열을 합치는 연산을 해봤습니다.

 

첫번째 str4과 두번째 str4 과연 같은 레퍼런스 일까요?

 

정답은, 서로 다른 레퍼런스 를 가지고 있습니다.

 

 

  

 

자 첫번째 그림은 첫번째 소스코드 ABC 문자열만 삽입 했을때 입니다.

그리고 두번째 그림은 DEF 를 합친 순간 입니다.

 

보시는 바와같이 레퍼런스가 변해버렸습니다.

 

결론은 String 객체로  = 연산으로 문자열을 삽입하거나 += 연산으로 문자열을 합치는 행위를 할때는

 

이전에 있던 객체(ABC)는 GC의 대상이되고 새로운 객체(ABCDEF)가 생성되어 집니다.

 

즉, 문자열이 합쳐지는게 아니라 새로운 객체가 생성된다고 보시면 됩니다.

 

왜 이런일이 발생하는 걸까요?

 

이는 앞서 설명해드렸습니다. String 객체는 내부적으로 데이터를 char 배열로 보관한다고 했습니다.

 

하지만 char 배열이 final 로 되어 있기 때문에 데이터를 변경할 수 없습니다.   

 

간단한 예제를 통해 이해해 봅시다.

 

 

final int data 에 100을 정의한 상태에서

 

data 변수에 100을 대입하면 무슨일이 발생할까요?

 

당연히 에러가 발생합니다. 이유는 final 로 변수를 선언해버렸기 때문에 데이터를 변경할 수 없는 상수가 되었기 때문입니다.

 

String 객체 역시 마찬가지 입니다.

 

append 할 수 없는 가장큰 이유가 바로 내부적으로 final 로 char 배열이 선언되어 데이터를 상수로 가지고 있기 때문입니다.

 

(참고로 이거까지 설명하는 자바책은 없더군요. 뭐 누구나 자바SDK 소스를 보시면 금방 눈치챌수 있습니다.)

 

 

반대로 StringBuffer, StringBuilder 은 왜 append 할수 있냐구요?

 

 

<그림> AbstractStringBuilder 추상클래스

 

 

StringBuffer, StringBuilder 클래스는 공통적으로 AbstractStringBuilder 추상클래스를 상속받아 구현되어진 클래스 입니다.

 

AbstractStringBuilder 추상클래스에는 char 배열이 final 로 되어있지 않습니다.

append 메서드 내부적으로 char 배열만 다시 생성해서 메모리공간을 늘리고 기존의 데이터를 복사해서

 

append 한 str 만큼 값을 추가하는 형태이지만,

 

String 객체는 자체를 new 하고 값을 다시 변경하지만,

 

StringBuffer, StringBuilder 객체는 안에 있는 char 배열 데이터만 변경하는 차이 인것 입니다.

 

이제 이해가 되시나요?

 

가장 좋은 방법은 다시 한번 SDK 를 직접 소스코드를 분석해보는것이 좋습니다 ^^

 

더 궁금한점은 답글 주시면 답변 드리겠습니다.