CS 이론 정리

객체 생성의 모든 것 (1) - 문자열

외계나무 2024. 11. 8. 09:19

Brief Summary

기본적으로 객체 생성은 [선언 + 초기화] 작업을 종합해서 이야기한다. 따라서, 흔히 보는 객체 생성 코드는 이런 모양이다.
A a = new A();

이 코드의 의미를 따져보면 이렇다.
A a: A 타입의 객체를 a라는 레퍼런스 변수로 가리키는데,
new A(): A 타입 크기의 메모리를 할당함. (+ 생성자 메서드 실행)

이렇게, 객체를 생성할 때 사용하는 new 연산자는 메모리의 Heap 영역에 객체가 사용할 공간을 할당해주고 그 주소를 반환한 후 생성자를 실행한다. 여기서 포인트는 new가 메모리 공간을 할당해준다는 점이다. 그렇지만 모든 선언 - 초기화 작업에 new가 들어가는가? int i = 0을 힐끔 쳐다보자. new는 코빼기도 보이지 않는다. 그렇다. 앞선 볼드체는 수정되어야 한다. new의 포인트는 그것이 객체의 메모리 공간을 할당해준다는 점이다.

참고로, 객체는 값이 변수 자체에 할당되지 않고 따로 저장된 후에 변수에는 객체의 주소값이 할당된다. 10억을 객체로 선언했다면 우리 손에는 10억이 아니라 금고의 위치만 쥐어져 있는 것이다...

1. 문자열 생성과 String Pool

자바에서 String 문자열은 기본적으로 String Pool 혹은 String Constant Pool이라고 불리는 Heap 영역의 어드메에 저장된다. 이는 자원을 절약하기 위해서인데, 0과 1로 이루어진 컴퓨터의 세계에서 숫자도 아닌 문자열이 얼마나 복잡할지 생각해보면 썩 괜찮은 조치처럼 보인다. (이걸 포함해서 자바가 다른 자료형보다 문자열을 우대하고 있는 흔적을 곳곳에서 볼 수 있다. 그게 싫으냐 하면, 자바 개발자로서 나도 문자열을 편애하기 때문에 아무 불만이 없다.)

그런데, 자바를 조금이라도 배운 사람들은 모두 알다시피, 문자열은 == 연산자로 동일성 비교를 할 수 없다. 정확하게는 자바는 문자열에 대한 동일성 비교를 권하지 않는다. 대신 equals()를 사용해 동등성 비교를 권장한다. 앞서 본 것처럼 자바가 String Pool에 저장된 문자열 객체를 재활용한다면 당연히 같은 객체일텐데, 왜일까?

흔히 문자열 객체를 생성할 때는 앞선 new 연산자가 끼어들 틈이 없다. 우리는 보통 이렇게 선언하기 때문이다. String str = "the string" 보았는가 자바의 문자열 우대를? primitive 타입도 아닌데 냅다 선언하고 값 할당해도 오냐오냐 해주는 저 모습을? 농담은 이쯤하고, 의문이 생기지 않는가? 앞서 new 연산자가 중요한 이유는 그게 객체의 메모리 공간을 할당하기 때문이라고 했는데, 왜 여기는 new가 없는데도 문자열 객체의 값이 메모리에 저장되고 그 주소값이 변수에 할당되는지 말이다.

2. 문자열 리터럴

String name = "Harry" 이 방식을 문자열 리터럴(Literal)이라고 한다. 기본 데이터 타입, 즉 primitive 타입에 값을 할당하는 방식을 리터럴이라고 부르기 때문이다. (literally, 말 그대로, 변수가 그 자체로 해당 값을 갖고 있다는 뜻이다) 이렇게 문자열 리터럴이 생성되면 앞서 설명한 바와 같이 해당 값이 String Pool에 저장되고 변수에는 그 주소를 할당한다. 혹은, String Pool에 해당 문자열이 이미 있다면 그 객체의 주소를 할당한다. 이렇게 만들어진 경우에는 복수의 변수가 같은 문자열 객체의 주소를 가지게 되기 때문에 동등할 뿐만 아니라 동일하다.

하지만 문자열 객체 생성에는 이런 방법도 있다. String name = new String("Joy"); 이렇게 new 연산자를 쓰는 순간 자바는 이 문자열을 그냥 객체의 하나라고만 인식하고 다른 객체들과 마찬가지로 적당히 Heap 영역에 방 만들어주고 변수에는 방 열쇠를 쥐어준다. 우대해준다는데 굳이 집어던지고 new의 손을 잡은 녀석까지 섬세하게 붙잡아 String Pool에 넣어주기에는 컴파일러가 너무 바빴던 것이다.

사실 이렇게 분리된 문자열 저장 방식은 String Pool이 원래는 Perm 영역이라는 고대의 메모리에 저장되었기 때문인데... Perm 영역은 자바 7 이전에 존재했다는 미지의 아틀란티스로서 GC 대상에서 제외되는 메타 데이터의 영역이었다고 한다. 그러나 이제는 Heap 영역에 소속되었기 때문에 Heap의 규칙에 따라 참조를 잃고나면 GC에 의해 반환처리된다. 오오 빛바랜 영광이여...

하여튼, new로 생성하는 방식을 동적으로 생성한다고 표현한다. 리터럴과 구분되며, 이렇게 리터럴로 생성한 문자열과 동적으로 생성한 문자열 중에 중복된 값이 생기더라도 자바 VM은 이를 따로 관리하지 않는다. 메모리 문제가 아예 없을 수는 없어서 자바 9에서는 이것을 약간 개선했다는 소문이 있지만, 근본적인 방식은 변하지 않았다.

정리

: 위에서는 굳이 표현하지 않았지만 String Constant Pool의 constant는 상수를 의미한다. 즉, 리터럴로 생성되는 문자열은 상수의 성격을 가지고 있는 것이다. 그래서 흔히 자바의 문자열은 Immutable(불변)하다고 한다. 그러나 문자열은 중복도 많은 만큼 수정도 잦을 수밖에 없고 그래서 StringBuilder와 StringBuffer라는 보조바퀴 한 쌍이 데굴데굴 굴러가는데, 이건 언젠가 따로 자세히 다뤄보기로 하고 넘어가자.

오늘은 문자열을 중심으로 객체를 생성하는 방법과 new 연산자에 대해 알아보았다. 이어지는 포스트에서는 힌트만 던지고 넘어간 '동적'이라는 표현에 대해 다루며 그 특성을 활용한 자바가 사랑하는 다형성까지 맛보기 해본다.


참고:
paki1019/Java-String-객체와-String-리터럴 와 이 포스트가 참고한 다른 포스트들...
minthug94/변수Variable와-상수Constant-그리고-리터럴Literal