String은 왜 불변일까: hashCode 캐싱과 HashMap 설계까지 한 번에 정리

revo·2026년 2월 9일

자바

목록 보기
8/30


String을 배우다 보면 “불변이다”, “hashCode를 캐싱한다”, “HashMap의 key로 안전하다” 같은 말들을 연속으로 듣게 된다.
문제는 이 말들이 각각 따로 설명되면 전혀 와닿지 않는다는 것이다.

나 역시

  • hashCode는 Map에서만 쓰는 개념 같았고 왜 equals와 같이 써야 하는지 감이 없었고 String이 왜 여기까지 끌려오는지도 이해가 안 됐다.

이 글은 String의 불변성 → hashCode 캐싱 → HashMap 설계가 하나의 논리로 어떻게 연결되는지를 정리한 글이다.


hashCode는 Map의 기능이 아니다

먼저 가장 큰 오해부터 정리해야 한다.

hashCode는 HashMap의 기능이 아니다.

hashCode()Object 클래스에 정의된 메서드다.
즉, 모든 객체가 태어날 때부터 가지고 있는 성질이다.

class Object {
    public int hashCode() { ... }
}

HashMap은:

  • hashCode를 만드는 주체가 아니라
  • 이미 존재하는 hashCode를 이용하는 자료구조다.

이 관점을 놓치면 “Map을 쓰니까 hashCode가 필요한 거 아닌가?”라는 오해가 생긴다.


HashMap은 key의 hashCode만 사용한다

Map에서 hashCode가 사용되는 대상은 항상 key다.

map.put(key, value);

내부 흐름은 단순하다.

  1. key.hashCode() 호출
  2. 이 값으로 버킷 위치 결정
  3. 같은 버킷 안에서 equals()로 최종 비교

value의 hashCode는 이 과정에 전혀 관여하지 않는다.

그래서 다음 코드는:

map.get(1).hashCode();
  • Map 동작과는 무관하고
  • 단순히 "one"이라는 String 객체의 hashCode를 출력한 것일 뿐이다.

이 지점에서 많이 붕 뜬다.
보고 있는 숫자가 Map이 쓰는 값이 아니기 때문이다.


hashCode는 “고정값”이 아니라 “고정된 규칙의 결과”다

String이나 Integer의 hashCode를 여러 번 출력해보면 항상 같은 값이 나온다.

"apple".hashCode(); // 항상 동일
1.hashCode();       // 항상 동일

이 때문에 “이미 정해진 값 같다”는 느낌을 받기 쉽다.
하지만 정확한 표현은 이거다.

hashCode는 고정된 공식으로 계산되며, 입력이 같으면 결과가 항상 같도록 설계된 값이다.

  • String → 문자열 내용 기반 계산
  • Integer → 숫자 값 그대로 반환
  • 불변 객체이기 때문에 결과가 변하지 않는다

String의 hashCode 캐싱이란 무엇인가

String의 hashCode() 구현을 보면, 계산 결과를 내부 필드에 저장해 두고 재사용한다.

의미는 단순하다.

한 번 계산한 hashCode를 다시 계산하지 않는다.

왜 이런 최적화가 가능할까?

  • String은 불변 객체
  • 문자열 내용이 절대 바뀌지 않는다
  • hashCode 결과도 절대 바뀌지 않는다

그래서:

  • 첫 호출은 계산
  • 이후 호출은 O(1)

String이 Map의 key로 자주 쓰이기 때문에 이 캐싱은 성능상 필수에 가깝다.


만약 String이 가변이었다면?

이제 핵심 질문이다.

만약 String이 가변 객체였다면, hashCode 캐싱은 왜 위험해질까?

가정해보자.

String s = "abc";
int h1 = s.hashCode(); // "abc" 기준으로 계산 후 캐싱

이후 문자열이 바뀐다면?

s.setCharAt(0, 'x'); // "xbc" (가변이라고 가정)

이 상태에서 다시 hashCode()를 호출하면:

  • 캐시된 값 반환
  • 여전히 "abc" 기준 hashCode

즉,

  • 객체의 현재 상태와 hashCode가 불일치한다.

이게 HashMap에서 왜 치명적인가

Map<String, String> map = new HashMap<>();
map.put(s, "value");
  • 저장 시: "abc" 기준 hashCode로 버킷 결정
  • 이후 문자열 변경
  • 조회 시: 과거 hashCode + 현재 equals 비교

결과는?

  • 같은 객체인데도 찾지 못함
  • 에러도 안 나고
  • 디버깅도 어려움

이건 단순한 버그가 아니라 자료구조의 전제를 깨뜨리는 상태다.

그래서 자바에는 이런 강한 규칙이 있다.

equals가 true면 hashCode도 반드시 같아야 한다.

그리고 이 규칙을 지키기 위해 String은 불변 객체로 설계되었다.


그래서 String은 HashMap의 “완벽한 key”다

String은:

  • 불변 객체
  • 내용 기반 equals
  • 내용 기반 hashCode
  • hashCode 캐싱 가능

즉,

  • 상태가 바뀌지 않고 hashCode와 equals가 항상 일관된다

정리

  • hashCode는 객체의 성질이지 Map의 기능이 아니다
  • HashMap은 key의 hashCode만 사용한다
  • String은 불변이기 때문에 hashCode 캐싱이 가능하다
  • 가변 객체에서 hashCode 캐싱은 구조를 망가뜨린다
  • 그래서 가변 객체는 key로 부적합하다

0개의 댓글