🎯1주차 Unit 6.1 — String 과 String Constant Pool

Psj·2026년 5월 8일

F-lab

목록 보기
41/230

🎯 Unit 6.1 — String 과 String Constant Pool ★★★

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.
면접 최단골 (== vs equals, new String() vs literal) 의 정확한 답.
Phase 4-5 의 메모리 이해 위에서 String 의 특수 대우를 풀어냄.


🌍 1. 세상 속 비유

String Constant Pool = "공용 도서관"

도서관 시스템을 상상해보세요.

시나리오 1 — 도서관 없는 세상 (Pool 없음)

  • 사람마다 자기 책 1권씩 들고 다님
  • 같은 책 읽고 싶으면 → 새로 찍어서 가짐
  • 1000명이 같은 베스트셀러 읽으려면 → 1000권 필요
  • 메모리 낭비 ⚠️

시나리오 2 — 공용 도서관 (Pool 있음)

  • 도서관에 책 1권만 보관
  • 1000명이 같은 책 보려 해도 → 모두 도서관 책 참조
  • 메모리 절약
  • 같은 책 = 같은 책 (식별 가능)

이게 String Constant Pool 의 본질.


String 자체 = "이름표 (immutable)"

이름표는 한번 인쇄하면 바꿀 수 없습니다:

  • "박승제" 이름표를 만들면
  • 그 이름표는 영원히 "박승제"
  • "박승제2" 로 바꾸려면 → 새 이름표 만들어야 함
  • 기존 이름표는 그대로

String 의 불변성 (Immutability) 의 본질.


핵심 한 문장

"String 은 자바에서 가장 특별한 클래스 — 불변 + 공용 풀 (Pool) 으로 메모리 효율과 안전성을 동시에 확보한다."

비유 정리:

비유자바 개념의미
공용 도서관String Constant Pool같은 문자열은 1개만
이름표Immutable String한번 만들면 변경 불가
도서관 카드참조 (Reference)같은 책 가리킴
새 책 인쇄new String()Pool 우회, Heap 에 새로

🔥 2. 탄생 배경

"왜 String 만 특별 대우?" — 빈도 + 위험성

자바에서 가장 많이 쓰는 객체 = String.

// 일상 코드의 90%
String name = "Alice";
String email = "alice@example.com";
String status = "ACTIVE";
log.info("처리 완료: " + result);

만약 모든 String 이 일반 객체처럼 동작했다면?


문제 1: 메모리 폭발

List<Customer> customers = customerRepository.findAll();  // 1만 명

for (Customer c : customers) {
    if (c.getStatus().equals("ACTIVE")) {  // "ACTIVE" 매번 새 객체?
        // ...
    }
}

Pool 없는 세상:

  • "ACTIVE" 리터럴 → 매 반복마다 새 String 객체
  • 1만 번 반복 → 1만 개 String 객체
  • 메모리 낭비 + GC 부담

Pool 있는 세상:

  • "ACTIVE" 리터럴 → Pool 의 1개 String 참조
  • 1만 번 반복 → 1개 String 객체 공유
  • 메모리 절약 + GC 부담 ↓

문제 2: 동시성 문제

// 가상 시나리오 — 만약 String 이 가변이라면
String userId = "alice";
Thread1: userId += "_admin";  // "alice_admin"
Thread2: log.info(userId);    // 어떤 값?

가변 String 이라면:

  • 한 스레드가 수정 → 다른 스레드가 영향
  • 동기화 필요 → 성능 ↓
  • 보안 위험 (인증 우회 등)

불변 String 이라면:

  • 수정 불가 → 항상 같은 값
  • 동기화 불필요
  • 자연스러운 thread-safe

문제 3: 보안 위험

// 보안 검증 예시
public void connectDB(String url, String username, String password) {
    if (isValidUser(username, password)) {
        // 검증 통과
        db.connect(url, username, password);
    }
}

가변 String 이라면:

  • isValidUser() 호출 후 → 다른 스레드가 password 변경
  • → 다른 password 로 connect
  • TOCTOU (Time-Of-Check Time-Of-Use) 보안 취약점

불변 String 이라면:

  • 검증 시점의 값 = 사용 시점의 값
  • 보안 보장

문제 4: HashMap 의 키로 사용

Map<String, Customer> cache = new HashMap<>();
cache.put("alice", new Customer("Alice"));

// ...

Customer c = cache.get("alice");

가변 String 이라면:

  • 키 "alice" 의 hashCode 가 변할 수 있음
  • → put 한 후 키가 바뀌면 → get 시 못 찾음
  • → 메모리 누수

불변 String 이라면:

  • hashCode 영원히 같음
  • HashMap 정상 동작

자바의 답 — 두 가지 해결책

자바는 두 가지를 동시에 적용:

1. 불변성 (Immutability)

  • String 객체는 생성 후 변경 불가
  • 동시성 안전, 보안 안전, hashCode 안정

2. String Constant Pool

  • 같은 문자열 리터럴은 1개의 객체 공유
  • 메모리 효율 ↑

역사적 변화 — Pool 의 위치

Java 버전SCP 위치비고
Java 1~6PermGen메모리 제한, OOM 위험
Java 7Heap (Old Gen)GC 대상 ✓
Java 8+HeapPermGen → Metaspace

Java 7 의 변화 의의:

  • PermGen OOM 위험 해소
  • intern() 호출 시 GC 가능
  • 더 큰 Pool 활용 가능

핵심 통찰

"String 의 특수 대우 = 자바의 가장 영리한 설계 결정 중 하나."

빈번하게 사용되는 String 의 특성을 인식하고, 불변성 + Pool 두 가지로 메모리/성능/보안 모두 해결. 자바의 디자인 철학 ("개발자가 실수하기 어렵게 + 시스템이 알아서 효율") 의 정수.


💣 3. 없으면 생기는 문제

시나리오 1: ILIC 의 등급 비교 버그

// ILIC 코드
public class CustomerService {
    
    public boolean isVipCustomer(Customer customer) {
        // ❌ 버그 가능 코드
        if (customer.getGrade() == "VIP") {
            return true;
        }
        return false;
    }
}

문제:

  • ==참조 비교 (메모리 주소)
  • customer.getGrade() 가 DB 에서 가져온 값이라면 → Pool 의 "VIP" 와 다른 객체
  • == 비교 실패
  • VIP 고객인데 일반 고객으로 처리됨 → 운영 사고

올바른 코드:

if ("VIP".equals(customer.getGrade())) {  // ✓ equals 사용
    return true;
}

→ String Pool 이해 못하면 이런 버그 발생.


시나리오 2: 면접 단골 질문

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 의 객체 반환)

시나리오 3: 메모리 누수 (intern 잘못 사용)

// ❌ 위험한 코드
@RestController
public class ApiController {
    @PostMapping("/log")
    public void logUserAction(@RequestBody String userInput) {
        userInput.intern();  // ❌ 사용자 입력을 Pool 에!
        // ...
    }
}

문제:

  • 사용자 입력 (무한히 다양) 을 모두 Pool 에 적재
  • Pool 크기 폭발 → OutOfMemoryError
  • 옛날 자바에서는 PermGen OOM 단골 원인

Pool 이해하면:

  • intern() 은 신중하게 써야 함
  • 동적 데이터에 적용 X

시나리오 4: SQL Injection 방지 코드

// 보안 검증
public boolean isValidQuery(String userInput) {
    if (userInput.contains("DROP TABLE")) {
        return false;
    }
    return true;
}

가변 String 이었다면:

  • 검증 시점: "SELECT * FROM users"
  • 다른 스레드가 변경: "DROP TABLE users"
  • 사용 시점: 변경된 값 → SQL Injection 가능

불변 String 의 안전:

  • 검증과 사용 시점의 값이 항상 같음
  • 보안 보장

시나리오 5: String 연결의 비효율

// ❌ 비효율 코드
String result = "";
for (int i = 0; i < 10000; i++) {
    result += "item" + i;  // 매번 새 String 생성!
}

불변성 모르면:

  • "그냥 변수에 추가" 라고 생각
  • → 매 반복마다 새 String 생성
  • → 1만 번 = 1만 개 String 객체
  • → O(n²) 시간 복잡도

불변성 알면:

  • StringBuilder 사용 (Unit 6.2 주제)
  • → O(n) 시간 복잡도

시나리오 6: ILIC 의 항만 코드 비교

// 항만 코드 처리
List<Cargo> cargos = ...;
for (Cargo c : cargos) {
    if (c.getOriginPort() == "BUSAN") {  // ❌ 위험!
        processBusan(c);
    }
}

버그 발생 케이스:

  • DB 에서 가져온 "BUSAN" 은 Pool 외부 객체
  • == 가 false → 부산 출발 화물 처리 안 됨
  • → 운영 사고

올바른 코드:

if ("BUSAN".equals(c.getOriginPort())) {  // ✓
    processBusan(c);
}

시나리오별 영향도

시나리오Pool 모르면Pool 알면
equals 버그운영 사고정확한 비교
면접 질문탈락시니어 답변
intern 오용OOM신중한 사용
보안취약점안전
성능O(n²)StringBuilder
운영 코드간헐적 버그안정

String 이해는 자바 시니어의 기본.


✅ 4. 해결책 — String 의 두 축

축 1: 불변성 (Immutability)

String 의 내부 구조 (Java 9+)

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 가능공유해도 안전

축 2: String Constant Pool (SCP)

Pool 의 본질

JVM 안의 특별한 영역:

  • 모든 문자열 리터럴이 저장됨
  • 같은 값 = 같은 객체 (1개만)
  • 위치: Heap (Java 7+)
[JVM Heap]
├── Young Generation
├── Old Generation
│   └── String Constant Pool
│       ├── "hello" (객체 1개)
│       ├── "world" (객체 1개)
│       └── "VIP"   (객체 1개)
└── Metaspace

리터럴 vs new String()

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 (값 비교)

intern() 메서드

역할: 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

🏗️ 5. 내부 동작 원리

Pool 의 등록 시점

컴파일 타임 등록

String a = "hello";  // 컴파일 시 Pool 에 등록 예약
String b = "hello";  // 같은 리터럴 → 같은 Pool 객체

// 컴파일된 .class 파일에:
// Constants Pool 정보 포함
// → 클래스 로딩 시 String Pool 에 추가

핵심: 리터럴은 클래스 로딩 시점 에 Pool 에 등록.


런타임 등록 (intern)

String c = new String("hello");  // Heap 객체
String d = c.intern();  // Pool 에 등록 (없으면) + 반환

Pool 의 데이터 구조

JVM 내부 구현 (HotSpot):

  • HashTable 기반
  • 키: String 의 hashCode
  • 값: String 객체 참조
  • → O(1) 조회
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" 로 변환
  • Pool 의 같은 객체 참조

주의: 변수가 섞이면 폴딩 X

String s = "Hello";
String a = s + " World";  // 런타임 연산
String b = "Hello World";

System.out.println(a == b);  // false

Java 9+ 의 Compact Strings

문제: 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)
}

효과:

  • ASCII 만 사용 시 → 메모리 50% 절약
  • 한글 등 비 ASCII 시 → UTF16 (기존과 동일)
  • ILIC 같은 한글/영문 혼용 시스템에서 효과 작을 수 있음

intern() 의 비용

시간:

  • HashTable 조회: O(1) 평균
  • 없으면 추가: O(1)

공간:

  • Pool 에 추가됨 → 메모리 사용
  • Java 7+ 에서는 GC 가능 (Heap 에 있으므로)

위험:

  • 동적 데이터 (사용자 입력 등) intern() → Pool 폭발
  • → OOM 위험

Pool 의 GC

Java 6 이전: PermGen → GC 거의 안 됨
Java 7+: Heap → 정상 GC 대상

조건:

  • Pool 안의 String 도 도달 가능성 (Reachability) 으로 판단
  • 더 이상 참조 없으면 GC 회수
  • → 동적 intern() 도 (이론상) 회수 가능

실무: 그래도 동적 intern 은 위험.


💻 6. 실전 코드 예시

예시 1: ILIC 의 안전한 등급 비교

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
}

:

  • 문자열 상수가 많을수록 → Enum 권장
  • Enum 은 Pool 처럼 동작 + == 비교 안전

예시 2: Pool 동작 시각적 확인

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));
    }
}

예시 3: 컴파일 타임 폴딩의 함정

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 (값은 같음)
    }
}

예시 4: 큰 문자열의 메모리

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());  // 도메인은 한정적
        }
    }
}

예시 5: ILIC 의 항만 코드 처리

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);
        }
    }
}

예시 6: String 비교의 best practice

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);
    }
}

예시 7: ILIC 의 동적 데이터 처리

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();  // 안전
        }
        // ...
    }
}

예시 8: String concat 효율 비교

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 깊이 다룸.


⚠️ 7. 주의사항 & 흔한 실수

실수 1: == 로 String 비교

// ❌ 가장 흔한 버그
if (status == "ACTIVE") { ... }

문제: 객체 출처에 따라 결과 다름.

해결: 항상 equals() 사용.

if ("ACTIVE".equals(status)) { ... }  // null-safe

실수 2: equals 호출 시 NPE

String input = null;
if (input.equals("hello")) { ... }  // NullPointerException!

해결 1: 상수를 앞에

if ("hello".equals(input)) { ... }  // null 이면 false

해결 2: Objects.equals

if (Objects.equals(input, "hello")) { ... }

실수 3: 동적 데이터 intern

@PostMapping("/log")
public void log(@RequestBody String userInput) {
    userInput.intern();  // ❌ OOM 위험
}

문제: 무한 다양한 입력 → Pool 폭발.

해결: intern 은 한정된 데이터에만.


실수 4: String + 반복

// ❌ 비효율
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)

실수 5: substring 의 메모리 누수 (Java 6 이전)

옛날 자바 (~Java 6):

String huge = "...";  // 100MB
String small = huge.substring(0, 10);  // 10자
huge = null;
// 그러나 small 이 huge 의 char[] 참조 → 100MB 못 회수!

Java 7+: substring 이 새 char[] 복사 → 해결됨.


실수 6: charset 무시

byte[] bytes = "한글".getBytes();  // ❌ 플랫폼 기본 charset
String s = new String(bytes);  // 다른 플랫폼에서 깨짐

해결:

byte[] bytes = "한글".getBytes(StandardCharsets.UTF_8);
String s = new String(bytes, StandardCharsets.UTF_8);

실수 7: 문자열 상수 vs Enum

// ❌ 문자열 상수 남용
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())) { ... }

문제:

  • 컴파일 타임 검증 X (오타 가능)
  • 새 상태 추가 시 흩어진 처리

해결: Enum

public enum Status {
    ACTIVE, INACTIVE, PENDING;
}

if (c.getStatus() == Status.ACTIVE) { ... }  // == 안전

🔗 8. 연관 개념 맵

Phase 6 (데이터 다루기) 에서의 위치

[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-5 와의 연결

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:

  • 6.2 (StringBuilder) — 가변 String
  • 6.4 (HashMap) — String 의 hashCode 활용

미래 주차:

  • 동시성 (4주차) — String 의 thread-safe 특성 활용
  • 데이터 처리 (Stream API) — String 변환

📝 9. 핵심 요약 — 3줄 정리

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 의 등급/항만 코드 같은 비교에서 이 원칙 지키면 운영 사고 방지.


🎓 학습 자기 점검

기본 이해

  • String 이 왜 불변인지 4가지 이유를 설명할 수 있다
  • String Constant Pool 의 위치를 안다 (Heap, Old Gen)
  • 리터럴과 new String() 의 차이를 메모리 그림으로 그릴 수 있다
  • ==equals() 의 차이를 정확히 안다

실전 적용

  • String 비교 시 equals 또는 Objects.equals 사용
  • null-safe 한 String 비교를 작성할 수 있다
  • intern() 의 위험을 알고 신중하게 사용한다
  • 문자열 상수를 Enum 으로 대체할 시점을 안다

면접 대비 (5분 답변)

  • == vs equals 답변 가능
  • String s = "a" vs String s = new String("a") 메모리 그림
  • 왜 String 이 불변인지 답변 가능
  • intern() 의 동작과 위험 답변 가능

자기 점검 질문 답변

Q1: String s = "hello"String s = new String("hello") 의 차이를 메모리 그림으로 그려보라.

한 줄 답: 리터럴은 Pool 객체 참조, newHeap 의 새 객체 를 가리킴.

상세 설명:

Case 1: 리터럴

String a = "hello";
String b = "hello";

메모리 상태:

[Stack]
  a (참조) ──┐
  b (참조) ──┤
            │
[Heap]      ▼
├── String Constant Pool
│   └── "hello"  ← a, b 둘 다 여기를 가리킴
│
└── (Pool 외부 영역 비어있음)

핵심:

  • 컴파일 시 "hello" 가 .class 파일의 Constants Pool 에 기록
  • 클래스 로딩 시 String Pool 에 등록
  • 같은 리터럴은 같은 객체 공유
  • a == b → true

Case 2: new String()

String 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 에 새 객체 생성
  • 인자로 받은 "hello" 는 Pool 에 등록되지만, 그 객체는 참조하지 않음
  • new 마다 새 객체 → 메모리 낭비
  • c == d → false

Case 3: 혼합

String 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 == cfalse (다른 객체)
  • a == dtrue (같은 Pool 객체)
  • c == dfalse (c 는 Heap, d 는 Pool)
  • a.equals(c)true (값 비교)

시각적 비유

Pool = 도서관:

  • 리터럴 사용자 = 도서관 카드 (같은 책 공유)
  • new String() 사용자 = 같은 책 새로 인쇄 (별도 보관)
  • intern() = 자기 책 들고 도서관 가서 같은 책 카드로 교환

실무 영향

메모리:

  • 리터럴 1만 번 = 1개 객체
  • new String 1만 번 = 1만 개 객체

비교:

  • == 는 위치에 따라 결과 다름
  • equals 는 항상 안전

권장:

  • new String("...") 은 거의 사용 안 함
  • 리터럴 또는 String.valueOf() 사용

Q2: 왜 String 은 불변 (Immutable) 인가?

한 줄 답: Thread-Safe / HashMap 키 안정성 / 보안 / 캐싱 4가지 이유.

상세 설명:

이유 1: Thread-Safe

가변 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"

동기화 없이 안전.


이유 2: HashMap 키의 hashCode 안정

가변 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 의 정확성 보장.


이유 3: 보안 — TOCTOU 방지

가변 String 의 위험:

public void connectDB(String url) {
    // 1. 검증
    if (isValidUrl(url)) {
        // 2. 사용 (검증과 사용 사이에 다른 스레드가 url 변경 가능)
        connect(url);  // 악의적 URL!
    }
}

불변 String 의 안전:

  • 검증 시점의 값 = 사용 시점의 값
  • TOCTOU (Time-Of-Check Time-Of-Use) 공격 불가능

이유 4: 캐싱 가능 (Pool)

불변이라서 가능한 것:

  • 같은 값의 String 을 하나의 객체로 공유 가능 (Pool)
  • 가변이라면 공유 시 한 곳에서 수정 → 다른 곳도 영향
String a = "hello";  // Pool
String b = "hello";  // Pool 의 같은 객체

// a 또는 b 를 통해 "hello" 를 수정한다면?
// → 다른 모든 곳에 영향 → 재앙!

불변이기 때문에 Pool 가능.


이유 5 (보너스): hashCode 캐싱

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;
    }
}
  • 불변이므로 hashCode 한 번 계산 후 캐싱 가능
  • HashMap 등에서 재사용 시 성능 ↑

종합 — 자바의 디자인 결정

측면가변 String불변 String (Java)
Thread 안전성동기화 필요자동 안전
HashMap 사용위험안전
보안TOCTOU 위험안전
Pool 가능X
hashCode 캐싱X
메모리절약 가능Pool 로 절약
성능수정 빠름변환 시 새 객체 (느림)

→ 자바는 안전 + Pool 을 선택. 가변이 필요하면 StringBuilder 사용.


다음 Unit으로

  • StringBuilder vs StringBuffer 학습 준비 완료
  • 가변 String 이 왜 필요한지 궁금하다
  • 동시성과 String 의 관계를 만날 준비 완료
profile
Software Developer

0개의 댓글