String is immutable
Java의 String은 immutable하다. Immutable은 불변이라는 뜻으로, Java의 String은 변하지 않는다는 것을 의미한다. String이 변하지 않는다는 것에 의아하다고 느껴질 수 있다. 아래 코드를 보면
String s1 = "Jack";
System.out.println(s1);
s1 = "John";
System.out.println(s1);
분명 s1의 값을 변경했고, 그 값이 변경되었음을 확인할 수 있다. 그런데 문자열이 immutable 하다는 것은 무슨 의미인가.
String is immutable이라는 것은 String type의 instance의 값이 불변이라는 것이다. 또한, Java의 String 타입은 기본형(primitive type)이 아닌 참조형(reference type)이다.
즉, 위 코드에서 "Jack"과 "John" 문자열은 String 타입의 인스턴스로서 JVM의 어딘가에 생성되어 존재하며, 우리는 s1이라는 변수에 해당 String 타입의 인스턴스를 할당할 뿐, String 타입 인스턴스를 변경하는 것이 아니다.

Java에서 인스턴스를 생성하는 방법은 new 키워드를 통해 생성자를 호출하는 것이다. 하지만 위 코드에서 "John"이라는 문자열은 어떻게 생성된 것일까. Java에서 String 인스턴스를 생성하는 방법은 두가지가 있으며, 하나는 생성자를 통한 방법이며(new String("string")), 다른 방법은 "를 통해 생성하는 Literal String 생성 방법이다.
Literal String의 생성 매커니즘은 다음과 같다.
1. JVM이 Constant Pool에서 생성하고자 하는 문자열을 찾는다.
2-1. Constant Pool에 해당 문자열이 있다면, 그 instance의 주소를 반환한다.
2-2. 해당 문자열을 찾지 못한다면, 새로 String instance를 생성하고 그 instance의 주소를 반환한다.
즉 아래 코드의
String str1 = "bkkmw";
String str2 = "bkkmw";
System.out.println(str1 == str2);
실행 결과는 true이다. 즉, str1과 str2는 동일한 인스턴스이다.
생성자를 통해 생성된 여러 String instance는 같은 문자열일 수 있으나, 같은 instance가 아니다.
즉 아래 코드의
String str1 = new String("bkkmw");
String str2 = new String("bkkmw");
System.out.println(str1 == str2);
실행 결과는 false이다. 즉, str1과 str2는 같은 인스턴스가 아니다.
그럼 위 코드에서 str1과 str2가 같은 인스턴스가 아닌, 같은 문자열인지 비교하는 방법은 없는가?(없을리가) 두 문자열의 내용이 같은지 확인하기 위해서는 그 값을 하나씩 비교하면 될 것이다. 어렵지는 않지만, 머리 아프다(귀찮다).
그래서 Java의 String class에는 equals() method가 구현되어 있다. 쉽게 말해서
String str1 = new String("bkkmw");
String str2 = new String("bkkmw");
System.out.println(str1.equals(str2));
위 코드의 실행 결과는 true이다.
String class의 equals() method를 구현한 방식을 보면 아래와 같다.
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
우선 같은 instance인지 비교하고, 아닐 경우 String class의 인스턴스인지 확인하고, 각 character를 비교하는 방식이다.
Java에서 String이 같은지 비교할 때는 문자열.eqauls(문자열)을 통해 비교하면 된다.
한가지 더 작성하자면, 문자열 비교에 있어서 유의해야 할 점이 있는데, String class의 equals() 메소드를 통해 비교를 한다는 것은, 비교에 있어서 string instance가 존재해야 한다는 것이다. 즉,
str1.equals("bkkmw");
위 코드가 동작하기 위해서는 str1 변수가 참조하는 값이 String이어야 한다는 것이다.
무슨 당연한 소린가 싶겠지만, 실제 프로젝트를 하다보면 상황이 생각대로만 돌아갈리가 절대 없다. 즉, 위 코드에서 str1을 String 타입으로 선언했더라도, 그 값이 String 객체를 가르키고 있다 보장할 수 없다. 즉, 로직이 내가 생각한 것과 다르게 흘러갈 수 있다는 것이다.
str1변수가 다른 객체를 참조하고 있다면, 위 코드가 동작하지 않는 것은 아니다. 오히려 false라는 결과가 의도한 결과일 것이다. String class가 아닌데 equals() method를 호출할 수 있냐? 그렇다. equals() method는 Java의 모든 class의 상위에 있는 Object class에서도 구현 된 메소드이기에, str1이 참조하는 객체가 어떤 타입이라도 코드는 동작할 것이고, 문자열이 아니라면 당연히 false를 반환할 것이기에 코드는 의도한대로 동작할 것이다.
그럼 뭐가 문제길래 이렇게 길게 글을 쓰는걸까
str1이 객체의 주소를 참조하지 않고있을 수 있다. 즉, null일 수 있다는 것이다. Spring으로 backend 개발하다보면 종종 있는 일이다. Client에서 내가 생각한 데이터만 보낼리 없다.
str1의 값이 null일 경우, .eqauls() method를 호출할 수 없다. 즉, NPE가 발생할 것이다.
그럼 어떻게 하냐. 정확한 정답은 없겠지만, 아래와 같이 작성하는 것을 추천한다.
"bkkmw".equals(str1);
적어도 NPE는 피할 수 있다.
다시 원래 쓰고자 했던 주제로 돌아왔다. Java는 Literal String을 String constant pool을 통해 관리한다. 이를 통해 얻을 수 있는 장점은, 매번 String을 생성하고 메모리를 관리하는 것이 아닌, 동일한 문자열은 하나의 인스턴스를 참조하도록 하여 메모리를 절약할 수 있는 것이다. 프로젝트를 하다보면 자주 사용되는 같은 문자열이 꽤나 있었다.
그럼 String이 불변인 것 과 메모리 절약은 무슨 관련이 있을까. String constant pool을 통해 메모리를 절약하는 구조를 구현할 수 있는 이유는 String이 불변이기 때문이다. 여러 String 타입의 변수들이 하나의 String을 참조하고 있을 때, String이 immutable하지 않다면 수정 시 모든 값이 변하게 되는 문제가 발생한다.
그 외에도 thread-safe 등의 많은 이점이 있지만, 가장 큰 장점은 메모리 절약이 아닐까 생각한다.