본문 바로가기

Java

String 클래스는 왜 불변으로 설계되었을까?


Heap Space 절약

String은 가장 많이 사용되는 객체이다. 10만 개의 String 객체를 사용한다고 할 때, 전부 JVM의 Heap Space에 저장된다면 메모리가 남아나질 않을 것이다.

자바에서는 가장 많이 사용되는 String 객체를 불변으로 설계하고 문자열 리터럴과 Java String Pool을 사용해서 Heap Space를 절약하고자 했다.

String a = "Hello World";
String b = "Hello World";

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

"Hello World" 처럼 ""를 활용해 문자열을 만드는 것을 문자열 리터럴이라고 한다. identity 비교와 equality 비교를 공부한 사람이라면 위 출력 결과를 false라고 예상할 수 있다.

하지만 위 출력문은 적어놓은 것처럼 true가 출력된다. 그 이유가 뭘까? 아래의 그림을 살펴보자.

앞서 말했던 것처럼 String 객체는 가장 많이 사용되기 때문에 문자열 리터럴과 Java String Pool을 사용해서 Heap Space를 절약하고자 했다.

Java String Pool은 JVM이 문자열을 Heap Space에 저장하는 특수 메모리 영역이다. String 객체는 불변 객체이기 때문에 JVM은 Java String Pool에 여러 개의 리터럴 문자열의 복사본을 하나만 저장하여 할당된 메모리양을 최적화할 수 있다. 이 과정을 interning 이라고 한다.

String 객체의 값이 바뀔 수 있다면 String Pool에 복사본을 저장해서 두 개의 객체를 같은 곳을 바라보게 할 수 없을 것이다. 같은 문자열 리터럴을 바라보는 String 객체 중 한 객체의 값이 언제 변할지 모르기 때문이다.

만약 같은 문자열을 가지고 있지만 각자 다른 주소 값을 참조하고 싶다면 new String("Hello World")와 같이 String 객체를 생성하면 된다.

결론적으로 String 객체를 불변 객체로 설계함으로써 Java String Pool 이라는 JVM 특수한 메모리 영역을 통해 메모리 최적화를 할 수 있게 된 것이다.


보안

문자열은 사용자 이름, 비밀번호, 네트워크 연결 URL 등 정보의 민감한 부분을 저장하는 데 널리 사용된다. 따라서 String 객체는 보안이 굉장히 중요하다고 볼 수 있다.

아래의 간단한 예제를 살펴보자.

void updateNickname(long id, String nickname) {
    // 여러 가지 작업
    ...
    userService.updateNickname(id, nickname);

}

String 객체가 불변이 아니라면 userService.updateNickname 메서드를 호출하기 전 여러가지 작업을 실행할 때 파라미터로 넘어왔던 nickname의 값이 외부에서 바뀔 수 있다.

String 객체를 불변으로 설계함으로써 결과의 영향을 미칠 가능성을 최소화했다고 볼 수 있다. 이는 String 객체뿐 아니라 불변 객체의 장점이기도 하다. 또 다른 파라미터인 long id는 값 자체가 넘어오기 때문에 외부에서 값이 바뀔 확률이 없다.

String 객체 뿐만 아니라 기본 유형의 Wrapper Class(ava.lang.Integer, Java.lang.Byte, Java.lang.Character, Java.lang.Short, Java.lang.Boolean, Java.lang.Long, Java.lang.Double, Java.lang.Float)도 불변이라는 사실을 기억하자.


동기화

불변성은 멀티 스레드 환경에서 서로 다른 스레드가 접근해도 변경되지 않기 때문에 자동으로 문자열 스레드를 안전하게 만든다.

따라서 불변 객체는 동시에 실행되는 여러 스레드에서 공유될 수 있다. 또한 스레드가 값을 변경하면 같은 값을 수정하는 대신 새 문자열이 Java String Pool에 생성되기 때문에 스레드로부터 안전하다.

이 또한 불변 객체의 공통 장점이지 않을까 싶다.


hashCode 캐싱

String 객체는 HashMap, HashSet 등 해시 구현에도 널리 사용된다. HashMap에서 같은 키인지 확인할 때 사용하는 메서드는 Object 객체의 hashCode 메서드이다. HashSet 또한 내부적으로 HashMap을 사용하기 때문에 마찬가지이다.

String 객체는 불변 객체이기 때문에 hashCode값을 저장해뒀다가 캐싱할 수 있다. String 클래스 내부를 살펴보면 int hash 라는 필드로 직접 해시 값을 가지고 있다.

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    /** The offset is the first index of the storage that is used. */
    private final int offset;
    /** The count is the number of characters in the String. */
    private final int count;   
    /** Cache the hash code for the string */
    private int hash; // Default to 0
    ...

따라서 hashCode 메서드를 호출할 때 캐싱을 통해 항상 같은 값을 반환한다. 이는 String 객체가 불변이기 때문에 가능한 것이다.

결론적으로 hash 값을 사용하는 HashMap과 같은 컬렉션에 String 객체를 넣어서 사용할 때 성능이 향상된다. 변경 가능한 객체의 경우에는 매번 hashCode 메서드를 호출해서 hash 값을 비교해야 한다.


결론

String 객체가 불변 객체로 설계됨으로써 Java String Pool을 활용해 메모리 절약을 할 수 있었고 String 객체로 hash 값을 사용하는 컬렉션을 사용할 때 성능을 향상 시킬 수 있었다. 또한 String 객체는 대부분의 경우에 안전하다.

String 객체는 왜 불변 객체로 만들었을까에 대한 단순한 의문으로 시작해서 학습 그리고 글을 작성하면서 자바 언어 디자이너가 얼마나 많이 고민해서 String 클래스를 설계했는지 느낄 수 있었던 유익한 시간이었다.


참고 자료


'Java' 카테고리의 다른 글

자바 제네릭(Generics) 기초  (0) 2020.11.08
Collection.forEach와 Stream.forEach는 뭐가 다를까?  (1) 2020.09.29
자바 빌드 도구  (0) 2020.09.11
Java Collections Framework  (0) 2020.09.04
자바 반복문 알고 쓰자!  (0) 2020.09.01