자바에서 문자열을 다룰 때 가장 많이 사용하는 타입은 단연 String
입니다. 하지만 여러분은 다음과 같은 코드에서 의문을 가져본 적 없으신가요?
String s1 = "Cat";
String s2 = "Cat";
String s3 = new String("Cat");
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
위 코드에서 s1 == s2
는 true
이고 s1 == s3
는 false
라는 결과가 나옵니다. 도대체 무슨 일이 일어난 걸까요?
이번 글에서는 Java에서 String
이 어떻게 동작하고, 왜 불변(Immutable)하게 설계되었는지, 그리고 메모리 구조, 성능, 보안 측면에서 어떤 이점이 있는지 하나씩 정리해보겠습니다.
1. String 객체는 불변(Immutable)하다
Java에서 String
은 불변 객체(Immutable Object)입니다. 즉, 한 번 생성된 문자열은 절대 변경될 수 없습니다.
1.1 내부 구조
내부 구조부터 살펴보면 Java 9 이상에서는 String
클래스의 핵심 멤버는 다음과 같습니다.
@Stable
private final byte[] value;
private final byte coder; // LATIN1 or UTF16
private int hash;
value
: 문자열 데이터를 저장하는 byte 배열이며,final
로 선언되어 있어 재할당 불가coder
: 인코딩 방식 (LATIN1 또는 UTF16)을 나타냄hash
:hashCode()
결과를 캐싱해두는 필드
참고로 Java 8까지는 char[] value
형태였으나, Java 9부터는 메모리 최적화를 위해 byte[] + coder
방식으로 변경되었습니다.
2. String이 불변이면 좋은 점이 뭐야?
단순히 "안 바뀌니까 안전해요!" 수준이 아닙니다. 불변성이 있기에 String
은 Java에서 여러 측면에서 핵심적인 역할을 할 수 있습니다.
2.1 String Pool을 통한 메모리 절약
String a = "hello";
String b = "hello";
System.out.println(a == b); // true
"hello"
는 컴파일 시점에 String Constant Pool에 저장되고, 동일한 리터럴 사용 시 같은 객체를 재사용합니다.- 메모리 낭비 없이 문자열을 공유할 수 있는 이유는 바로 String이 불변이기 때문입니다.
- 만약
String
이 변경 가능했다면, 공유된 객체가 예상치 못하게 변경될 위험이 생겼을 겁니다.
2.2 멀티스레드 환경에서 안전 (Thread-safe)
- 불변 객체는 상태가 변하지 않기 때문에 동기화 없이도 안전하게 사용할 수 있습니다.
String
은 Java에서 매우 자주 쓰이므로, 성능적으로도 큰 이점입니다.
2.3 해시코드 캐싱
private int hash;
String
의hashCode()
는 한 번 계산되면 내부 필드(hash
)에 캐싱됩니다.- 불변 객체이기에 이후에도 항상 동일한 해시값을 반환합니다.
HashMap
,HashSet
등에서 키로 사용 시 매우 효율적입니다.
2.4 보안 측면에서도 안전
String
을 수정할 수 없다면, 비밀번호나 토큰 같은 민감 정보가 실수로 수정되거나 변조될 위험이 없습니다.- 보안 라이브러리에서
String
을 자주 사용하는 이유이기도 합니다.
3. 리터럴과 new String의 차이
3.1 리터럴로 생성한 String
String first = "hello";
String second = "hello";
System.out.println(first == second); // true
- 두 객체는 같은 String Pool 내 주소를 참조
- 메모리를 절약하며, 내부적으로 하나의 객체만 존재
3.2 new String으로 생성한 String
String third = new String("hello");
System.out.println(first == third); // false
third
는 Heap 영역에 별도로 생성된 새로운 객체- Pool에 존재하는
"hello"
와는 주소가 다름
3.3 intern() 메서드로 Pool 강제 참조
String pooled = third.intern();
System.out.println(first == pooled); // true
intern()
메서드는 Pool에 해당 문자열이 존재하면 그 참조값을 반환하고- 없으면 Pool에 추가하고 참조를 반환합니다
📌 예제: 실제 해시코드 비교
String first = "hello"; // 리터럴
String second = new String("hello"); // 생성자
String third = second.intern(); // intern() 사용
System.out.println(System.identityHashCode(first)); // ex) 998325
System.out.println(System.identityHashCode(second)); // ex) 1423451
System.out.println(System.identityHashCode(third)); // ex) 998325
first
와third
는 같은 Pool 객체를 참조 → 같은 주소second
는 새로운 Heap 객체 → 주소 다름
4. 이미지를 통해 직관적으로 이해하기
다음 이미지는 String
객체가 생성되는 방식과 메모리 내 구조, 객체 비교 결과를 아주 잘 보여줍니다.
4.1 String s1 = “Cat”
- 이 코드는
"Cat"
이라는 문자열 리터럴을 생성합니다. - Java는 컴파일 시점에
"Cat"
을 String Constant Pool에 저장합니다. - 그리고
s1
은 그 Pool에 있는"Cat"
객체를 참조하게 됩니다.
4.2 String s2 = “Cat”
s2
역시 같은 리터럴"Cat"
을 사용하므로, String Pool에 같은 문자열이 이미 존재하는지 확인합니다."Cat"
이 존재하므로 기존 객체를 재사용하며,s2
도 동일한"Cat"
객체를 참조합니다.
결과: s1 == s2
는 true → 두 참조 변수는 동일한 메모리 주소의 같은 객체를 가리키고 있기 때문입니다.
4.3 String s3 = new String(”Cat”)
- 이번엔 생성자
new
를 사용하여String
객체를 생성했습니다. - 이 방식은 항상 Heap 영역에 새로운 객체를 생성합니다.
- 내부적으로는 여전히 String Pool에 있는
"Cat"
을 참고하지만,
→ 그 내용을 기반으로 새로운 객체를 복사 생성합니다.
결과: s1 == s3
는 false → s1
은 String Pool의 "Cat"
객체를, s3
는 Heap에 새로 만들어진 "Cat"
객체를 가리키므로 참조 주소가 다릅니다.
4.4 정리 요약
변수 | 생성 방식 | 메모리 위치 | 객체 공유 여부 | == 비교 |
---|---|---|---|---|
s1 |
리터럴 | String Pool | O (Pool 공유) | — |
s2 |
리터럴 | String Pool | O (Pool 공유) | s1 == s2 → true |
s3 |
new 생성자 |
Heap | X (새 객체 생성) | s1 == s3 → false |
참고) .equals()는 값 비교!
==
는 주소(참조) 비교이고, .equals()
는 값(내용) 비교입니다.
System.out.println(s1 == s3); // false
System.out.println(s1.equals(s3)); // true
따라서 문자열의 내용이 같은지를 확인하려면 무조건 .equals()
를 사용해야 합니다.
5. 결론
Java의 String
은 단순한 문자열이 아닙니다. 불변(Immutable)하게 설계된 이유는 다음과 같은 구조적 이점 때문입니다.
- 메모리 최적화: String Pool을 활용해 동일 문자열을 공유
- 스레드 안전성: 동기화 없이도 안정적으로 사용 가능
- 성능 최적화: 해시코드 캐싱, 불필요한 객체 생성을 줄임
- 보안 강화: 민감 정보 보호에 유리
또한 리터럴과 new
방식의 차이를 이해함으로써, 메모리 관리와 성능까지도 고려한 코드 작성이 가능합니다.
'BackEnd > JAVA' 카테고리의 다른 글
[JAVA] equals()와 hashCode() 완벽 정리 (1) | 2025.02.24 |
---|---|
[JAVA] 익명 클래스 (Anonymous Class) 정리 (1) | 2025.02.20 |
[JAVA] Lombok의 @UtilityClass 어노테이션 정리 (1) | 2025.02.13 |
[JAVA] 얕은 복사 (Shallow Copy) VS 깊은 복사 (Deep Copy) (2) | 2025.02.06 |
[JAVA] 와일드카드 (Generic Wildcards) 정리 (2) | 2025.02.05 |