String 객체의 생성과 불변성

코코딩딩·2022년 3월 31일
1

JAVA Basic

목록 보기
2/7
post-thumbnail

개요

개발을 하다보면 문자열을 다룰 일이 많습니다.
String 클래스야 말로 java에서 가장 많이 사용되는 클래스가 아닐까 생각합니다.
그런것에 비해서 저는 String에 대해서 무관심하게 사용하고 있었습니다.
이번 기회에 몇 가지 String 클래스에 대해서 알아보려고 합니다.
본 문서에서는 String 객체를 만드는 방법불변성에 대해서 살펴봅니다.

String의 두 가지 생성 방식

java에서 String 객체(문자열)을 생성하는 방법은 여러가지가 있겠지만 대표적으로 2가지로 나눠 볼 수 있을 것 같습니다.

  1. new 키워드를 이용한 생성.
  2. ""(쌍 따옴표)를 이용한 생성.

이 두가지 생성 방식이 어떤 차이를 가지는지 알아 보겠습니다.

두가지 객체 생성 방식의 차이점

new 키워드로 생성된 경우 String object는 heap 메모리에 위치하게 됩니다.
그래서 매번 new 키워드로 생성한 경우 새로운 String Object가 생성됩니다.
그리고 각각의 Object는 동등성이 보장되더라도(같은 문자열), 동일성을 보장하지는 않습니다.
그 의미는 아래 코드로 살펴볼 수 있다.

// 객체 선언
String love = new String("love");// 1)
String love2 = new String("love");// 2)

// 테스트
System.out.println("love == love2 : " + (love == love2));
System.out.println("love.equals(love2) : " + love.equals(love2));

// 결과
love == love2 : false //동일성 체크
love.equals(love2) : true //동등성 체크

위 코드에서 1)과 2)는 같은 문자열 “love”지만 서로 다른 메모리 주소를 가지게 됩니다.

하지만 Literal String의 경우는 다른 생성 메커니즘을 가집니다.

String literalLove = "love"; 
String literalLove2 = "love";

위 코드에서 String객체가 생성되는 메커니즘은 아래와 같습니다.

  1. 변수 literalLove의 문자열이 초기화 될 때 JVM은 Constant Pool에서 "love"를 찾습니다.
  2. Constant Pool에 "love"가 있다면 해당 객체의 주소를 반환합니다.
  3. Constant Pool에 "love"없다면 새로 "love"를 생성 한 후에 해당 객체의 주소를 반환합니다.
  4. 그 결과 두 변수 literalLove, LiteralLove2 은 같은 Heap 메모리 주소를 참조하게 됩니다.

해당 내용을 확인해보기 위해 코드를 작성해보았습니다.

//new 키워드사용한 생성과 쌍따옴표를 사용한 생성

//두 Literal String의 메모리 주소를 비교한다
String literalLove = "love";
String literalLove2 = "love";
System.out.println("literalLove == literalLove2 : " + (literalLove == literalLove2));
System.out.println("literalLove :  " + System.identityHashCode(literalLove));
System.out.println("literalLove2 :  " + System.identityHashCode(literalLove2));

//결과
literalLove == literalLove2 : true
literalLove :  1531333864
literalLove2 :  1531333864

//두 String Object의 메모리 주소를 비교한다.
String love = new String("love");
String love2 = new String("love");
System.out.println("love == love2 : " + (love == love2));
System.out.println("love :  " +System.identityHashCode(love));
System.out.println("love2 :  " +System.identityHashCode(love2));

//결과
love == love2 : false
love :  1468177767
love2 :  434091818

코드에서 보았다 싶이, String 객체를 어떻게 생성하냐에 따라서 메모리를 사용하는 방법이 달라지니 염두해두고 사용하면 좋을 것입니다.
new 키워드를 사용하여 계속해서 같은 문자열을 만들 경우에는 불필요하게 메모리를 사용할 수 있습니다.
하지만 이 부분을 알고 있어 Constant Pool을 사용하면 성능에 이점을 얻을 수 있습니다.
Literal로 생성할 경우는 같은 "love"로 여러번 선언하더라도 항상 같은 객체만을 참조할 뿐입니다.

String은 불변객체?

불변객체는 말그대로 변하지 않는 객체입니다. 그리고 String은 불변객체입니다.
즉 한번 생성되면 String 객체의 문자열 내용을 변경할 수 없다는 것입니다.
그런데 우리는코딩을 하면서 너무 당연하다는 듯 문자열을 자주 변경합니다.
그래서 바꿀 수 없다는 것이 무슨 말인지 이해하지 못할 수 있습니다.

이 내용에 대해서 살펴보기 위해 String class의 concat method를 살펴보면 좋을 것 같습니다.

concat 메소드를 살펴보자

//main method에서
String iLoveJava = "i".concat(" love java");
System.out.print(iLoveJava);

//출력
i love java

//String 클래스의concat method 구현부
public String concat(String str) {
    if (str.isEmpty()) {
        return this;
    }
    if (coder() == str.coder()) {
        byte[] val = this.value;
        byte[] oval = str.value;
        int len = val.length + oval.length;
        byte[] buf = Arrays.copyOf(val, len);
        System.arraycopy(oval, 0, buf, val.length, oval.length);
        return new String(buf, coder);
    }
    int len = length();
    int olen = str.length();
    byte[] buf = StringUTF16.newBytesFor(len + olen);
    getBytes(buf, 0, UTF16);
    str.getBytes(buf, len, UTF16);
    return new String(buf, UTF16);
}

메소드 구현부의 코드 흐름만 파악해보면

  1. 현 객체의 문자열 값인 this.value와 str의 문자열 길이를 합한 크기의 배열 buf을 만들고,
    먼저 this.value의 값부터 복사하여 buf에 넣습니다.
  2. 이후 str의 값을 buf 복사하여 넣습니다. 
  3. buf를 새로운 String객체로 생성하여 리턴합니다.

위 내용으로 볼 때 기존 String 객체에는 따로 수정을 가하지 않고, 두 String 객체를 복사하여 더 큰 byte 배열에 넣고 이를 통해서 새로운 String 객체를 생성한다는 것을 확인 할 수 있습니다.
기본적으로 String은 문자열에 수정이 가해지면 해당 객체의 값을 바꾸는 것이 아닌 수정된 값을 가지는 새로운 객체를 만들고 있습니다

java document도 살펴보자

String에 대한 Java document를 확인해보면 다음과 같은 문구가 있습니다.

Strings are constant;
their values cannot be changed after they are created.
String buffers support mutable strings.
Because String objects are immutable they can be shared.

대충 해석하자면 String 객체는 상수이며, 객체 생성 후 값을 변할 수 없다는 의미입니다.
만약 가변적인 작업이 필요할 경우는 String Buffer(혹은 String Builder) 클래스를 이용하라고 하네요.

왜 수정이 안되나?

String객체는왜 수정이 안되는 걸까요?
알아볼 수록 질문이 늘어나는 군요...
Stirng 클래스에서 문자열 정보를 담고있는 맴버변수를 확인해보겠습니다.

private final byte[] value; // String class의 맴버변수

맴버 변수 value를 보면 private final 키워드가 붙어있음을 확인 할 수 있습니다.
생성자를 통해서 해당 맴버변수는 초기화되며, 외부에서 value변수로 직접 접근할 수 없도록 캡슐화 하였습니다.
또한 final 키워드를 사용하였기에 객체 생성 시 값을 한번 할당하고 이후에는 변경할 수 없게 하였습니다.
그래서 문자열을 연결하려 concat method를 사용할 때 위와 같이 동작할 수 밖에 없어던 것이다.

String 객체는 수정일어나면 항상 새로운 문자열을 만들 수 밖에 없는데 이런 단점을 가지더라도 왜 문자열 객체를 불변성을 보장하는걸까요?
매번 복사하고, 새로운 객체를 만드는 작업을 할바에 그냥 수정할 수 있게 만드는 게 낫지 않았을까요?

java에서는 왜 String을 불변 객체로 만든걸까?

String을 불변객체로 사용함으로 얻는 이점은 다음과 같은 것이 있습니다.
우선 이점에 대해서 살펴봅시다.

  1. 성능 : String Constant Pool을 활용하여 새로운 객체를 생성하지 않고 관리합니다.
    String 객체는 아주 광범위하게 사용합니다. 그러므로 String 객체를 캐싱하거나 재사용 할 수 있으면 heap 메모리의 많은 공간을 절약할 수 있을 것입니다. 이러한 생각에서 String Constant Pool이 만들어지지 않았을까 생각해봅니다.
    String Constant Pool은 JVM에 위치해 있는 특별한 저장공간입니다. String Constant Pool이 동작하는 방식에 대해서는 위 에서 이미 알아본 바가 있습니다.
    이 메모리 공간을 사용함에 있어 주의점도 있습니다.
    보통 쌍따옴표를 사용하여 문자열으 선언하면 이를 JVM이 알아서 String Constant Pool에 저장합니다. 하지만 그외에도 다른 방법을 통해서 String Constant Pool에 문자열을 저장할 수 있습니다.
    아래의 메소드를 사용하면 가능합니다.
    public native String intern();
      

intern 메소드를 사용하면 문자열을 String Constant Pool에 객체를 저장하고 해당 문자열의 메모리주소를 반환합니다.
즉 같은 메모리의 주소를 가지게 됩니다. 이를 통한 이점으로는 equals method가 아닌 ==연산자를 사용하므로 동일성을 파악할 수 있다는 것입니다.
하지만 해당 메소드는 웬만하면 사용하지 않는 것이 좋을 것입니다.
equals보다 ==연산자가 성능이 좋겠지만, 그를 위해 intern 메소드로 매번 문자열을 선언한다면 언젠가 String Constant Pool의 저장공간이 부족하게 될 것이고, GC가 동작하게 될 것입니다.
GC가 동작하면 어플리케이션의 모든 작업은 GC작업이 마무리 될때까지 정지될 것입니다.
이건 좋은 모습은 아닐 것입니다.

  1. 보안 : String 객체가 특정 보안체크 로직을 통과 후에 값이 수정될 가능성을 막을 수 있습니다.
    어떤 문자열의 경우 어플리케이션 전반에 걸쳐서 보안에 민감할 수 있고, JVM Class Loader 또한 광범위 하게 문자열을 사용합니다. 문자열 객체가 수정이 가능하다면 특정 유효성, 보안 검사를 통과한 후 해당 문자열이 수정될 수 있다는 것을 의미하죠. 이는 아주 큰 보약취약점이 될 것입니다.

  2. 동시성 : String 객체는 불변성이 보장되므로 thread-safe합니다. 동시성 이야기를 하자면 내용이 길어지기에 이는 ‘동시성’관련 문서에서 다뤄야 할 것 같습니다.

마무리

이렇게 String class의 선언방식에 따른 메모리 사용이나, 불변성과 그로 얻는 이점 등을 살펴보았습니다. 이 문서를 작성하면서 많이 반성하게 되었습니다. 제가 사용하는 것에 대해서 깊게 알지 못했던 것과, 조사하면서 이해하기 힘든 부분이나, 깊이있는 부분들이 참 많다는 걸 알았습니다.
물론 호기심을 채워가는 과정이 재미있었고, 이렇게 문서로 남기니 이론과 개념이 이전보다 훨씬 정리 되었습니다.

💡 참조

1개의 댓글

comment-user-thumbnail
2022년 4월 1일

좋은 글 감사합니다 ㅎㅎ

답글 달기