멘토링

왜 자바 String은 불변인가?

langsamUndStetig 2022. 6. 13. 16:05

String 객체는 불변의 성질을 갖고 있다. 

아래의 코드에서 str은 처음에 "Hi"라는 값을 가진 String 객체의 참조값을 가지고 있다. str에 concat 메소드를 사용하여 " Kim"을 추가하였고, 그것을 다시 str에 할당했다. String은 불변의 성질을 갖고 있기 때문에, 객체를 가리키던 참조값이 변경되었음을 알 수 있다. 저 코드에 있는 String 객체의 수는 "Hi", " Kim", 그리고 "Hi Kim" 으로 총 3개이다. 

중요한 것은 String의 객체는 불변이지만, 그 객체를 가리키던 변수는 참조값을 변경할 수 있다는 것이다. 

String str = "Hi";
String st1 = "Hi";
System.out.println(str.hashCode()); // 2337
System.out.println(st1.hashCode()); // 2337
str=str.concat(" Kim"); 
System.out.println(str.hashCode()); // -2135669968

 

왜 자바에서 String은 불변의 성질을 갖고 있는가?

 

- String이 불변이 아니라면 String pool은 존재할 수 없다. 많은 힙 영역은 JRE(Java Runtime Environemnt, 자바 실행환경)에 저장되어 있다. 동일한 String 변수가 풀에 있는 한 개 보다 많은 String 변수에 참조될 수 있다. 또한 String interning은 String이 불변이 아닐 경우 불가능하다. 

*String interning이란: String.intern() 메서드를 말한다.intern() 메서드는 String 객체의 완전한 복사본을 만들어 String 상수 풀에 저장한다. 만약 이미 String 상수 풀에 존재한다면, 새로운 String 객체는 생성되지 않고, 이미 존재하는 객체를 가리킨다. 다른 것으론, 예를 들어 String s와 t가 있을 때, s.intern() == t.intern()가 true면 s.equals(t)도 true다. 이 둘의 관계는 필요충분 조건이다. 

** String 리터럴과 new String()의 차이점

- String 리터럴은 String t = "Hi"; 이고, String s = new String("Hi")로 선언하는 것이다. 

이 둘의 차이점은 new를 사용할 경우 무조건적으로 객체를 새로 생성한다는 것이다. 리터럴로 선언 할 경우, 위의 사진을 보면 알겠지만 String s ="Hi";라고 새로 선언하더라도 기존의 t가 가리키던 객체를 가리키게 된다.

그래서 q에 intern 메서드를 사용하여 r이라는 새로운 String 타입 변수에 할당할 경우, r은 기존의 t와 s가 가리키던 상수 풀에 있던 "Hi"를 가리키게 된다.

    String t = "Hi";
    String s = "Hi";
    String q = new String("Hi");
    String u = new String("Hi");
    System.out.println(t.equals(s)); // true
    System.out.println(t==s); //true
    System.out.println(t==q); // false
    System.out.println(q==u); // false
    System.out.println(t.equals(q)); // true
    String r = q.intern(); // String 객체를 가리키지 않고, 
                           // 상수 풀에 있는 "Hi"값을 가리키게 했다.
                           // 만약 상수 풀에 없더라면 새로 생성이되었을 것이다.
                           // 따라서 주소값을 비교할 경우 참이 나오게 된다.
    System.out.println(r == t); // true
    System.out.println(r ==q);  // false

- String을 불변으로 하지 않으면, 심각한 보안 문제를 야기할 수 있다. String이 불변이 아니라면, 해커들이 참조되어진 값을 변경하여 보안 이슈를 일으킬 수 있다. 그러나 String은 불변이어서 이로부터 안전하다.

 

- String은 멀티 쓰레딩으로부터 안전하다. 여러 쓰레드들이 한 개의 String 객체에 접근할 경우 동기화 문제가 발생할 수 있으나, String은 불변하기 때문에 안전하다.

 

-ClassLoader를 이용해 올바른 클래스를 로딩할 때 안전하다. 

 

 

String과 메모리 사용에 관한 중요한 사실들

String t = "Hi";  //String 리터럴을 사용하여 "Hi" 객체를 String 상수 풀에 만들고, 참조값을 t에 할당한다.
t.concat(" Hi Hi"); // VM이 "Hi Hi Hi"라는 새로운 String 객체를 만들었지만, 
// 참조값을 핟당받은 변수가 없다. 따라서 String 객체에 도달할 수 없게 되었다.
System.out.println("t refers to " + t); // t refers to Hi

 t.concat로 생성된 "Hi Hi Hi"는 어떻게 되었을까? 

저 String 객체는 존재하지만, 주소값을 저장하는 변수가 없기 때문에 잃어버린 것으로 여겨진다.

이러한 String 객체들은 메모리에 존재하고, 프로그래밍 언어는 메모리를 효율적으로 관리하고자 한다. 

서비스가 커질수록 String 리터럴은 많은 메모리 영역을 차지하며, 중복을 일으킬 수도 있다. 자바를 효율적으로 만들기 위해서, JVM은 "String 상수 풀"이라는 특별한 메모리 영역을 만들었다. 

 

컴파일러가 String literal을 볼 때, 풀에 있는 String을 찾아본다. 일치 하는 값을 찾으면, 새로운 literal에 대한 참조가 기존에 풀에 있는 String으로 바로 연결되고, 새로운 String 객체는 생성되지 않는다. 그래서 이미 있는 String 객체는 한 개 이상의 참조변수를 손쉽게 가지게 된다. 만약 String이 불변이 아닐 경우, 여러 참조 변수가 가리키는 String 객체를 한 참조 변수에서 임의로 변경한다면, 나머지 참조변수들은 원치 않아도 가리키던 객체의 값이 바뀌게 된다. 

 

또한 Java 7 전까진 자바 상수 풀은 고정된 크기를 가진 PermGen 영역에 위치해 있었고, garbage collection에 적합하지 않았다. PermGen에 저장되었으 땐, 너무 많은 String을 interning 할 경우 OutOfMemory error가 발생했다. 그러나 Java7부턴 힙영역에 위치해 있기 때문에, garbage collection에 적합해졌고, OutOfMemory 발생 확률이 적어졌다. 왜냐하면 참조되지 않은 String들은 풀에서 지워지게 되기 때문이다.

 

따라서 이러한 이유들 때문에 String 클래스는 final 클래스로 설정되어 있다. 왜냐하면 이러한 String의 특성을 overriding하면 안되기 때문이다.

 

참고 자료

https://www.baeldung.com/java-string-immutable

https://www.baeldung.com/string/intern

https://www.geeksforgeeks.org/java-string-is-immutable-what-exactly-is-the-meaning/