F-lab Java 1주차 / Phase 6 / Unit 6.1 본격 학습 자료 — Phase 6 시작!
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.선수 지식: Phase 4 (JVM 메모리), Phase 5 (GC)
다음 Unit: 6.2 — StringBuilder vs StringBuffer이 Unit의 의미: 자바에서 가장 특별한 클래스 — String.
면접 최단골 (==vsequals,new String()vs literal) 의 정확한 답.
Phase 4-5 의 메모리 이해 위에서 String 의 특수 대우를 풀어냄.
도서관 시스템을 상상해보세요.
→ 이게 String Constant Pool 의 본질.
이름표는 한번 인쇄하면 바꿀 수 없습니다:
→ String 의 불변성 (Immutability) 의 본질.
"String 은 자바에서 가장 특별한 클래스 — 불변 + 공용 풀 (Pool) 으로 메모리 효율과 안전성을 동시에 확보한다."
비유 정리:
| 비유 | 자바 개념 | 의미 |
|---|---|---|
| 공용 도서관 | String Constant Pool | 같은 문자열은 1개만 |
| 이름표 | Immutable String | 한번 만들면 변경 불가 |
| 도서관 카드 | 참조 (Reference) | 같은 책 가리킴 |
| 새 책 인쇄 | new String() | Pool 우회, Heap 에 새로 |
자바에서 가장 많이 쓰는 객체 = String.
// 일상 코드의 90%
String name = "Alice";
String email = "alice@example.com";
String status = "ACTIVE";
log.info("처리 완료: " + result);
만약 모든 String 이 일반 객체처럼 동작했다면?
List<Customer> customers = customerRepository.findAll(); // 1만 명
for (Customer c : customers) {
if (c.getStatus().equals("ACTIVE")) { // "ACTIVE" 매번 새 객체?
// ...
}
}
Pool 없는 세상:
Pool 있는 세상:
// 가상 시나리오 — 만약 String 이 가변이라면
String userId = "alice";
Thread1: userId += "_admin"; // "alice_admin"
Thread2: log.info(userId); // 어떤 값?
가변 String 이라면:
불변 String 이라면:
// 보안 검증 예시
public void connectDB(String url, String username, String password) {
if (isValidUser(username, password)) {
// 검증 통과
db.connect(url, username, password);
}
}
가변 String 이라면:
isValidUser() 호출 후 → 다른 스레드가 password 변경불변 String 이라면:
Map<String, Customer> cache = new HashMap<>();
cache.put("alice", new Customer("Alice"));
// ...
Customer c = cache.get("alice");
가변 String 이라면:
불변 String 이라면:
자바는 두 가지를 동시에 적용:
| Java 버전 | SCP 위치 | 비고 |
|---|---|---|
| Java 1~6 | PermGen | 메모리 제한, OOM 위험 |
| Java 7 | Heap (Old Gen) | GC 대상 ✓ |
| Java 8+ | Heap | PermGen → Metaspace |
Java 7 의 변화 의의:
"String 의 특수 대우 = 자바의 가장 영리한 설계 결정 중 하나."
빈번하게 사용되는 String 의 특성을 인식하고, 불변성 + Pool 두 가지로 메모리/성능/보안 모두 해결. 자바의 디자인 철학 ("개발자가 실수하기 어렵게 + 시스템이 알아서 효율") 의 정수.
// ILIC 코드
public class CustomerService {
public boolean isVipCustomer(Customer customer) {
// ❌ 버그 가능 코드
if (customer.getGrade() == "VIP") {
return true;
}
return false;
}
}
문제:
== 는 참조 비교 (메모리 주소)customer.getGrade() 가 DB 에서 가져온 값이라면 → Pool 의 "VIP" 와 다른 객체== 비교 실패올바른 코드:
if ("VIP".equals(customer.getGrade())) { // ✓ equals 사용
return true;
}
→ String Pool 이해 못하면 이런 버그 발생.
String a = "hello";
String b = "hello";
String c = new String("hello");
System.out.println(a == b); // ?
System.out.println(a == c); // ?
System.out.println(a.equals(c)); // ?
System.out.println(a == c.intern()); // ?
Pool 모르면:
Pool 알면:
a == b: true (Pool 의 같은 객체)a == c: false (c 는 Heap 의 새 객체)a.equals(c): true (값 비교)a == c.intern(): true (intern() 으로 Pool 의 객체 반환)// ❌ 위험한 코드
@RestController
public class ApiController {
@PostMapping("/log")
public void logUserAction(@RequestBody String userInput) {
userInput.intern(); // ❌ 사용자 입력을 Pool 에!
// ...
}
}
문제:
Pool 이해하면:
// 보안 검증
public boolean isValidQuery(String userInput) {
if (userInput.contains("DROP TABLE")) {
return false;
}
return true;
}
가변 String 이었다면:
불변 String 의 안전:
// ❌ 비효율 코드
String result = "";
for (int i = 0; i < 10000; i++) {
result += "item" + i; // 매번 새 String 생성!
}
불변성 모르면:
불변성 알면:
// 항만 코드 처리
List<Cargo> cargos = ...;
for (Cargo c : cargos) {
if (c.getOriginPort() == "BUSAN") { // ❌ 위험!
processBusan(c);
}
}
버그 발생 케이스:
== 가 false → 부산 출발 화물 처리 안 됨올바른 코드:
if ("BUSAN".equals(c.getOriginPort())) { // ✓
processBusan(c);
}
| 시나리오 | Pool 모르면 | Pool 알면 |
|---|---|---|
| equals 버그 | 운영 사고 | 정확한 비교 |
| 면접 질문 | 탈락 | 시니어 답변 |
| intern 오용 | OOM | 신중한 사용 |
| 보안 | 취약점 | 안전 |
| 성능 | O(n²) | StringBuilder |
| 운영 코드 | 간헐적 버그 | 안정 |
→ String 이해는 자바 시니어의 기본.
public final class String {
private final byte[] value; // 실제 문자 데이터
private final byte coder; // 인코딩 (LATIN1 or UTF16)
private int hash; // 캐시된 hashCode
// ... methods
}
핵심 포인트:
1. final 클래스 — 상속 불가
2. private final 필드 — 외부에서 변경 불가
3. 모든 메서드 — 새 String 반환 (수정 X)
String s = "hello";
String s2 = s.toUpperCase(); // s 는 그대로
System.out.println(s); // "hello" (변하지 않음!)
System.out.println(s2); // "HELLO" (새 String 객체)
모든 변환 메서드 는 새 String 반환:
toUpperCase(), toLowerCase()trim(), strip()replace(), concat()substring()| 이점 | 설명 |
|---|---|
| Thread-Safe | 동기화 불필요 |
| HashMap 키 안전 | hashCode 영원히 같음 |
| 보안 | TOCTOU 방지 |
| 캐싱 가능 | 같은 값 = 같은 객체 |
| String Pool 가능 | 공유해도 안전 |
JVM 안의 특별한 영역:
[JVM Heap]
├── Young Generation
├── Old Generation
│ └── String Constant Pool
│ ├── "hello" (객체 1개)
│ ├── "world" (객체 1개)
│ └── "VIP" (객체 1개)
└── Metaspace
String a = "hello"; // 리터럴 → Pool
String b = "hello"; // 같은 리터럴 → Pool 의 같은 객체
String c = new String("hello"); // new → Heap 의 새 객체
메모리 그림:
[Stack]
a (참조)
b (참조) ─── 같은 곳을 가리킴
c (참조)
[Heap]
├── String Constant Pool
│ └── "hello" ← a, b 가 참조
│
└── 일반 Heap
└── "hello" ← c 가 참조 (별도 객체)
검증:
a == b // true (같은 Pool 객체)
a == c // false (다른 객체)
a.equals(c) // true (값 비교)
역할: Heap 객체를 Pool 로 가져오기 (또는 Pool 의 객체 반환)
String a = "hello"; // Pool
String c = new String("hello"); // Heap
String d = c.intern(); // Pool 의 "hello" 반환
System.out.println(a == d); // true
System.out.println(c == d); // false (c 는 여전히 Heap)
동작 원리:
1. c.intern() 호출
2. JVM 이 Pool 에 "hello" 가 있는지 확인
3. 있으면 그 객체 반환 / 없으면 추가 후 반환
== vs equals ⭐⭐⭐ (면접 단골)| 비교 | == | .equals() |
|---|---|---|
| 비교 대상 | 참조 (주소) | 값 (내용) |
| Pool 영향 | 받음 | 안 받음 |
| 권장 | ❌ | ✓ |
String a = "hello";
String b = new String("hello");
a == b // false (다른 객체)
a.equals(b) // true (같은 값)
원칙: String 비교 시 항상 equals() 사용.
String s = "Hello World";
// === 변환 (새 String 반환) ===
s.toUpperCase() // "HELLO WORLD" (새 객체)
s.toLowerCase() // "hello world"
s.trim() // 양쪽 공백 제거
s.replace("o", "0") // 치환
s.substring(0, 5) // "Hello"
s.concat(" !") // "Hello World !"
// === 비교 ===
s.equals("Hello World") // true
s.equalsIgnoreCase("HELLO WORLD") // true
s.compareTo("Hello") // 양수 (사전 순)
// === 검사 ===
s.contains("World") // true
s.startsWith("Hello") // true
s.endsWith("ld") // true
s.isEmpty() // false
s.isBlank() // false (공백만 있어도 true) [Java 11+]
// === 정보 ===
s.length() // 11
s.charAt(0) // 'H'
s.indexOf("World") // 6
String a = "hello"; // 컴파일 시 Pool 에 등록 예약
String b = "hello"; // 같은 리터럴 → 같은 Pool 객체
// 컴파일된 .class 파일에:
// Constants Pool 정보 포함
// → 클래스 로딩 시 String Pool 에 추가
핵심: 리터럴은 클래스 로딩 시점 에 Pool 에 등록.
String c = new String("hello"); // Heap 객체
String d = c.intern(); // Pool 에 등록 (없으면) + 반환
JVM 내부 구현 (HotSpot):
StringTable (HashTable)
├── bucket[0] → "hello" → "world" → ...
├── bucket[1] → "VIP"
├── bucket[2] → ...
└── bucket[N] → ...
기본 크기 (Java 8+): 60013 (소수, 충돌 최소화)
조정: -XX:StringTableSize=N
+ 연산자의 동작String a = "Hello";
String b = "World";
String c = a + " " + b; // 어떻게 동작?
컴파일러 변환 (Java 8 이전):
String c = new StringBuilder()
.append(a)
.append(" ")
.append(b)
.toString();
Java 9+ (Indify):
invokedynamic 으로 더 효율적 처리String a = "Hello" + " " + "World"; // 컴파일 시 폴딩
String b = "Hello World";
System.out.println(a == b); // true!
컴파일러 동작:
"Hello" + " " + "World" → 컴파일 시 "Hello World" 로 변환주의: 변수가 섞이면 폴딩 X
String s = "Hello";
String a = s + " World"; // 런타임 연산
String b = "Hello World";
System.out.println(a == b); // false
문제: ASCII 문자열도 UTF-16 으로 저장 → 메모리 2배 낭비
해결 (Java 9+):
public final class String {
private final byte[] value; // byte[] 로 변경 (Java 8 까지는 char[])
private final byte coder; // LATIN1 (1 byte/char) or UTF16 (2 bytes/char)
}
효과:
시간:
공간:
위험:
Java 6 이전: PermGen → GC 거의 안 됨
Java 7+: Heap → 정상 GC 대상
조건:
실무: 그래도 동적 intern 은 위험.
public class CustomerService {
// ❌ 위험: == 사용
public boolean isVipUnsafe(Customer c) {
return c.getGrade() == "VIP"; // 버그 가능
}
// ✓ 안전: equals 사용
public boolean isVipSafe(Customer c) {
return "VIP".equals(c.getGrade()); // null-safe
}
// ✓ 더 좋은 방법: Enum 사용
public boolean isVipBest(Customer c) {
return c.getGrade() == CustomerGrade.VIP; // Enum 은 == 안전
}
}
enum CustomerGrade {
NORMAL, SILVER, GOLD, VIP
}
팁:
== 비교 안전public class StringPoolDemo {
public static void main(String[] args) {
String a = "hello";
String b = "hello";
String c = new String("hello");
String d = c.intern();
System.out.println("a == b: " + (a == b)); // true
System.out.println("a == c: " + (a == c)); // false
System.out.println("a.equals(c): " + a.equals(c)); // true
System.out.println("a == d: " + (a == d)); // true
System.out.println("c == d: " + (c == d)); // false
// hashCode 확인
System.out.println("a hash: " + System.identityHashCode(a));
System.out.println("c hash: " + System.identityHashCode(c));
}
}
public class CompileTimeFoldingDemo {
public static void main(String[] args) {
String a = "Hello" + " " + "World"; // 컴파일 시 폴딩
String b = "Hello World";
System.out.println(a == b); // true (폴딩됨)
// 변수가 들어가면?
String hello = "Hello";
String c = hello + " World"; // 런타임 연산
System.out.println(b == c); // false (Heap 객체)
System.out.println(b.equals(c)); // true (값은 같음)
}
}
public class LargeStringDemo {
public static void main(String[] args) {
// 1000개의 비슷한 String
List<String> strings = new ArrayList<>();
// ❌ 비효율: 매번 new
for (int i = 0; i < 1000; i++) {
strings.add(new String("ACTIVE")); // 1000개 객체
}
// ✓ 효율: literal 사용
for (int i = 0; i < 1000; i++) {
strings.add("ACTIVE"); // Pool 의 1개 객체 공유
}
// ✓ 동적 데이터를 신중히 intern
Set<String> uniqueDomains = new HashSet<>();
for (Customer c : customers) {
String domain = extractDomain(c.getEmail());
uniqueDomains.add(domain.intern()); // 도메인은 한정적
}
}
}
public class PortService {
// ILIC 의 항만 코드 — 한정된 집합 (~수백 개)
private static final Set<String> KNOWN_PORTS = Set.of(
"BUSAN", "INCHEON", "ULSAN", "TOKYO", "SHANGHAI", "LA"
);
// ✓ Pool 활용 — 안전한 비교
public boolean isKoreanPort(String portCode) {
return "BUSAN".equals(portCode)
|| "INCHEON".equals(portCode)
|| "ULSAN".equals(portCode);
}
// ✓ 더 좋은 방법: Enum
public enum Port {
BUSAN("KR"), INCHEON("KR"), ULSAN("KR"),
TOKYO("JP"), SHANGHAI("CN"), LA("US");
private final String country;
Port(String country) {
this.country = country;
}
public boolean isKorean() {
return "KR".equals(country);
}
}
}
public class StringComparison {
// ❌ NullPointerException 위험
public boolean compareUnsafe(String a, String b) {
return a.equals(b); // a 가 null 이면 NPE!
}
// ✓ 상수가 앞으로
public boolean compareSafe(String input) {
return "ACTIVE".equals(input); // input 이 null 이어도 false 반환
}
// ✓ Objects.equals (null-safe)
public boolean compareModern(String a, String b) {
return Objects.equals(a, b); // 둘 다 null 이면 true, 한쪽만 null 이면 false
}
// ✓ 대소문자 무시
public boolean caseInsensitive(String a, String b) {
return a != null && a.equalsIgnoreCase(b);
}
}
public class LogService {
// ❌ 위험: 사용자 입력 intern
public void logUserActionDangerous(String userInput) {
userInput.intern(); // OOM 위험!
log.info(userInput);
}
// ✓ 안전: intern 안 함
public void logUserAction(String userInput) {
log.info(userInput); // Pool 에 안 넣음
}
// ✓ 한정된 집합만 intern
public void recordEventType(String eventType) {
// 이벤트 타입은 한정적 (수십 개)
Set<String> KNOWN_EVENTS = Set.of("LOGIN", "LOGOUT", "PURCHASE");
if (KNOWN_EVENTS.contains(eventType)) {
eventType = eventType.intern(); // 안전
}
// ...
}
}
public class ConcatBenchmark {
// ❌ O(n²) — 매번 새 String
public String slowConcat(List<String> items) {
String result = "";
for (String item : items) {
result += item; // 매번 새 객체!
}
return result;
}
// ✓ O(n) — StringBuilder
public String fastConcat(List<String> items) {
StringBuilder sb = new StringBuilder();
for (String item : items) {
sb.append(item);
}
return sb.toString();
}
// ✓ Stream + Collectors
public String streamConcat(List<String> items) {
return items.stream()
.collect(Collectors.joining());
}
// 100,000 개 처리 시:
// slowConcat: 수 분 (O(n²))
// fastConcat: ~10ms (O(n))
}
→ Unit 6.2 에서 StringBuilder 깊이 다룸.
== 로 String 비교// ❌ 가장 흔한 버그
if (status == "ACTIVE") { ... }
문제: 객체 출처에 따라 결과 다름.
해결: 항상 equals() 사용.
if ("ACTIVE".equals(status)) { ... } // null-safe
String input = null;
if (input.equals("hello")) { ... } // NullPointerException!
해결 1: 상수를 앞에
if ("hello".equals(input)) { ... } // null 이면 false
해결 2: Objects.equals
if (Objects.equals(input, "hello")) { ... }
@PostMapping("/log")
public void log(@RequestBody String userInput) {
userInput.intern(); // ❌ OOM 위험
}
문제: 무한 다양한 입력 → Pool 폭발.
해결: intern 은 한정된 데이터에만.
// ❌ 비효율
String result = "";
for (int i = 0; i < 100000; i++) {
result += data[i]; // O(n²)
}
해결: StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append(data[i]);
}
String result = sb.toString(); // O(n)
옛날 자바 (~Java 6):
String huge = "..."; // 100MB
String small = huge.substring(0, 10); // 10자
huge = null;
// 그러나 small 이 huge 의 char[] 참조 → 100MB 못 회수!
Java 7+: substring 이 새 char[] 복사 → 해결됨.
byte[] bytes = "한글".getBytes(); // ❌ 플랫폼 기본 charset
String s = new String(bytes); // 다른 플랫폼에서 깨짐
해결:
byte[] bytes = "한글".getBytes(StandardCharsets.UTF_8);
String s = new String(bytes, StandardCharsets.UTF_8);
// ❌ 문자열 상수 남용
public class Constants {
public static final String STATUS_ACTIVE = "ACTIVE";
public static final String STATUS_INACTIVE = "INACTIVE";
public static final String STATUS_PENDING = "PENDING";
}
// 사용
if (Constants.STATUS_ACTIVE.equals(c.getStatus())) { ... }
문제:
해결: Enum
public enum Status {
ACTIVE, INACTIVE, PENDING;
}
if (c.getStatus() == Status.ACTIVE) { ... } // == 안전
[Unit 6.1: String + Constant Pool] ← 지금 여기 ★
↓
[Unit 6.2: StringBuilder vs StringBuffer] (다음)
↓
[Unit 6.3: ArrayList vs LinkedList]
↓
[Unit 6.4: HashMap 내부 구조] ★★★
↓
[Unit 6.5: TreeMap, LinkedHashMap]
| Phase | 연결 |
|---|---|
| 4.1 (JVM 메모리) | Pool 의 위치 (Heap, Old Gen) |
| 4.2 (Pass by Value) | String 참조 전달 |
| 5.1 (GC) | Pool 도 GC 대상 (Java 7+) |
| 5.2 (Heap 구조) | Pool 이 Old Gen 에 위치 |
| 자료구조 | String 활용 |
|---|---|
| HashMap | 키로 자주 사용 — 불변성 필수 |
| HashSet | 중복 제거 — equals + hashCode |
| TreeMap | 정렬 키 — compareTo |
| List | 요소로 자주 사용 |
| 언어 | String 특성 |
|---|---|
| Java | 불변 + Pool |
| C# | 불변 + Pool (Java 와 비슷) |
| Python | 불변 + 일부 작은 string interning |
| JavaScript | 불변 (primitive) |
| C++ | 가변 (std::string) |
| Rust | 불변 (&str) + 가변 (String) |
→ 자바의 선택은 표준적.
| 질문 | 이 Unit 에서의 답 |
|---|---|
== vs equals? | 참조 vs 값 비교 |
new String("a") vs "a"? | Heap vs Pool |
| String 이 왜 불변? | 4가지 이유 (스레드/보안/HashMap/캐싱) |
| intern() 언제 쓰나? | 한정된 데이터, 신중하게 |
| Pool 위치는? | Java 7+ Heap |
같은 Phase:
미래 주차:
1️⃣ String 은 불변 (Immutable) + Pool 로 특수 대우 받는 클래스다.
final클래스 +private final필드로 변경 불가. 모든 변환 메서드는 새 String 반환. 이로써 Thread-Safe, HashMap 키 안전, 보안, 캐싱 가능 4가지 이점 확보. 빈번하게 사용되는 String 의 특성을 인식한 자바의 영리한 설계.2️⃣ String Constant Pool 은 같은 리터럴을 1개 객체로 공유한다.
리터럴 (
"hello") 은 Pool 에 등록 → 같은 리터럴은 같은 객체 공유.new String("hello")는 Heap 에 새 객체.a == b는 참조 비교 → Pool 영향 받음.a.equals(b)는 값 비교 → 항상 안전. Java 7+ 에서 Pool 위치는 Heap (Old Gen) → GC 가능.3️⃣ 실무 원칙 — equals 사용, 동적 데이터 intern X, Enum 활용.
==는 함정 → 항상equals()(또는Objects.equals). 사용자 입력 등 동적 데이터 intern() 금지 (OOM 위험). 한정된 문자열 상수는 Enum 으로 대체 (컴파일 타임 안전 +==비교 가능). 큰 문자열 연결은 StringBuilder (Unit 6.2). ILIC 의 등급/항만 코드 같은 비교에서 이 원칙 지키면 운영 사고 방지.
new String() 의 차이를 메모리 그림으로 그릴 수 있다== 와 equals() 의 차이를 정확히 안다equals 또는 Objects.equals 사용== vs equals 답변 가능String s = "a" vs String s = new String("a") 메모리 그림Q1: String s = "hello" 와 String s = new String("hello") 의 차이를 메모리 그림으로 그려보라.
한 줄 답: 리터럴은 Pool 객체 참조, new 는 Heap 의 새 객체 를 가리킴.
상세 설명:
String a = "hello";
String b = "hello";
메모리 상태:
[Stack]
a (참조) ──┐
b (참조) ──┤
│
[Heap] ▼
├── String Constant Pool
│ └── "hello" ← a, b 둘 다 여기를 가리킴
│
└── (Pool 외부 영역 비어있음)
핵심:
"hello" 가 .class 파일의 Constants Pool 에 기록a == b → trueString c = new String("hello");
String d = new String("hello");
메모리 상태:
[Stack]
c (참조)
d (참조)
[Heap]
├── String Constant Pool
│ └── "hello" ← c, d 가 가리키지 않음!
│
└── 일반 Heap
├── String 객체 #1 ("hello") ← c 가 가리킴
└── String 객체 #2 ("hello") ← d 가 가리킴
핵심:
new 는 무조건 Heap 에 새 객체 생성new 마다 새 객체 → 메모리 낭비c == d → falseString a = "hello"; // Pool
String c = new String("hello"); // Heap
String d = c.intern(); // Pool 의 "hello" 반환
메모리 상태:
[Stack]
a (참조) ─────┐
c (참조) ──────│──┐
d (참조) ─────┤ │
│ │
[Heap] ▼ │
├── String Pool │
│ └── "hello" ← a, d 가 가리킴
│
└── 일반 Heap │
└── "hello" ← c 가 가리킴
검증:
a == c → false (다른 객체)a == d → true (같은 Pool 객체)c == d → false (c 는 Heap, d 는 Pool)a.equals(c) → true (값 비교)Pool = 도서관:
new String() 사용자 = 같은 책 새로 인쇄 (별도 보관)intern() = 자기 책 들고 도서관 가서 같은 책 카드로 교환메모리:
new String 1만 번 = 1만 개 객체비교:
== 는 위치에 따라 결과 다름equals 는 항상 안전권장:
new String("...") 은 거의 사용 안 함String.valueOf() 사용Q2: 왜 String 은 불변 (Immutable) 인가?
한 줄 답: Thread-Safe / HashMap 키 안정성 / 보안 / 캐싱 4가지 이유.
상세 설명:
가변 String 의 문제:
// 가상 코드 (실제 자바 X)
String userId = "alice";
// Thread 1
userId += "_admin";
// Thread 2
log.info("User: " + userId); // "alice" 또는 "alice_admin"?
불변 String 의 안전:
String userId = "alice";
// Thread 1
String adminId = userId + "_admin"; // 새 String
// Thread 2
log.info("User: " + userId); // 항상 "alice"
→ 동기화 없이 안전.
가변 String 의 문제:
Map<String, Customer> cache = new HashMap<>();
String key = "alice";
cache.put(key, alice);
// 가상: key 가 가변이라 변경됨
// key.append("_modified"); // hashCode 변경!
cache.get(key); // null 반환! (잘못된 버킷 검색)
불변 String 의 안전:
String key = "alice"; // hashCode 영원히 같음
cache.put(key, alice);
cache.get("alice"); // 정상 동작
→ HashMap, HashSet 의 정확성 보장.
가변 String 의 위험:
public void connectDB(String url) {
// 1. 검증
if (isValidUrl(url)) {
// 2. 사용 (검증과 사용 사이에 다른 스레드가 url 변경 가능)
connect(url); // 악의적 URL!
}
}
불변 String 의 안전:
불변이라서 가능한 것:
String a = "hello"; // Pool
String b = "hello"; // Pool 의 같은 객체
// a 또는 b 를 통해 "hello" 를 수정한다면?
// → 다른 모든 곳에 영향 → 재앙!
→ 불변이기 때문에 Pool 가능.
public final class String {
private int hash; // 캐시
@Override
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
// 한 번만 계산
h = computeHashCode();
hash = h; // 캐싱
}
return h;
}
}
| 측면 | 가변 String | 불변 String (Java) |
|---|---|---|
| Thread 안전성 | 동기화 필요 | 자동 안전 |
| HashMap 사용 | 위험 | 안전 |
| 보안 | TOCTOU 위험 | 안전 |
| Pool 가능 | X | ✓ |
| hashCode 캐싱 | X | ✓ |
| 메모리 | 절약 가능 | Pool 로 절약 |
| 성능 | 수정 빠름 | 변환 시 새 객체 (느림) |
→ 자바는 안전 + Pool 을 선택. 가변이 필요하면 StringBuilder 사용.