본문 바로가기
카테고리 없음

🤔자바의 String은 왜 Immutable로 설계되었을까?

by silvertogold100 2025. 8. 3.
반응형

자바에서는 문자열 데이터를 저장하기 위해 String 클래스 타입을 사용한다.

어떻게 다루느냐에 따라 어플리케이션의 성능이 차이가 난다.

메모리 관점

→ stack 영역이 아닌 객체와 같이 힙에서 문자열 데이터가 생성되고 다뤄진다.

→ String name = "홍길동" → name 이라는 지역 변수는 Stack 영역에 존재하고, "홍길동"이라는 문자열의 위치 값을 가지고 있다. 해당 문자열은 힙 영역에서 생성된다.

String 클래스의 기본적인 특징

기본적으로 자바에서는 String 객체의 값은 변경할 수 없다. (Immutable)

→ 기존의 문자열에 다른 문자열을 더한다면 새로운 문자열 데이터가 생성됨

→ 즉, 문자열 값 자체는 불변이라 변경할 수 없기 때문에 새로운 문자열 데이터 객체를 대입하여 새로운 공간에 값이 할당됨 → 이런 관리 문제를 해결하기 위해 StringBuilder 와 같은 클래스를 제공함.

        String a = "Hello";
        System.out.println(a.hashCode());  // 69609650

        a+= "Java Programming";
        System.out.println(a.hashCode()); // -351927155
        // 새로운 문자열 데이터 a가 생성되어 메모리의 힙 영역에 할당되었음.
        

hashCode() : Object 클래스에 존재하는 메소드로, 객체의 메모리 번지를 이용해서 해시코드를 만들어 리턴하는 메소드

JVM > Heap > String Pool 에 문자열이 저장됨.

🤔자바의 String은 왜 Immutable로 설계되었을까?

  • 보안적인 측면 : 참조 값을 변경하여 어플리케이션 보안 문제가 생길 수 있으므로 불변 처리함
  • 멀티 쓰레드 환경에서 동기화 문제가 발생하지 않는다.
  • JVM에서는 따로 힙모메리 영역 안에 String Constant Pool로 관리하고 있다. 문자열을 상수화하여 다른 변수나 다른 객체들과 공유할수 있도록 제공하고 있다.
  • 스레드 안전성 (Thread Safety):
    • 불변 객체는 여러 스레드에서 동시에 접근해도 상태가 변경될 염려가 없으므로 별도의 동기화 메커니즘이 필요 없습니다. 이는 멀티스레드 환경에서 String 객체를 안전하게 공유할 수 있게 하여 동시성 문제를 크게 줄여줍니다.
    • 만약 String이 가변적이었다면, 한 스레드가 문자열을 수정하는 동안 다른 스레드가 그 문자열을 읽는 경우 예측 불가능한 결과가 발생할 수 있습니다.
  • 보안 (Security):
    • String은 파일 경로, 네트워크 연결 주소, 데이터베이스 연결 정보, 사용자 비밀번호 등 민감한 정보를 저장하는 데 자주 사용됩니다.
    • 만약 String이 가변적이라면, 보안 검증 후에 문자열의 내용이 예기치 않게 변경될 수 있습니다. 예를 들어, 파일 경로가 유효하다고 검증된 후 악성 경로로 변경되어 시스템에 접근하는 공격에 취약해질 수 있습니다. 불변성은 이러한 종류의 공격을 방지하는 데 도움을 줍니다.
  • 성능 및 효율성 (Performance and Efficiency) - 특히 문자열 풀 (String Pool):
    • 자바는 String 리터럴을 위한 특별한 메모리 영역인 "문자열 풀(String Pool)"을 유지합니다. 동일한 내용을 가진 문자열 리터럴이 있을 때, JVM은 새로운 객체를 생성하는 대신 풀에 있는 기존 객체를 재사용합니다.
    • String이 불변이기 때문에 이러한 공유가 가능합니다. 만약 가변적이었다면, 한 곳에서 문자열이 변경되면 다른 곳에서 참조하는 모든 문자열에 영향을 미치게 되어 공유가 불가능하거나 매우 복잡해졌을 것입니다.
    • 불변성은 hashCode() 값이 한 번 계산되면 캐시될 수 있도록 합니다. String은 HashMap의 키 등으로 자주 사용되는데, 이 때 해시 코드를 효율적으로 재사용할 수 있어 성능에 이점이 있습니다.
  • 캐싱 (Caching):
    • String의 해시 코드는 한 번 계산되면 변경될 필요가 없으므로 캐시될 수 있습니다. 이는 String 객체가 HashMap이나 HashSet과 같은 해시 기반 컬렉션의 키로 사용될 때 성능을 향상시킵니다.
  • 보정된 해시 코드 (Hash Code Reliability):
    • String 객체가 Map의 키로 사용될 때, 그 객체의 hashCode() 값은 변경되지 않아야 합니다. 만약 String이 가변적이고 Map에 키로 저장된 후에 내용이 변경된다면, 해당 키를 다시 찾을 수 없게 되는 문제가 발생합니다. 불변성은 이 문제를 방지합니다.
  • 코드 가독성 및 유지보수성 (Code Readability and Maintainability):
    • 불변 객체는 상태가 변하지 않으므로 코드의 흐름을 추적하고 이해하기 더 쉽습니다. 이는 버그를 줄이고 코드의 유지보수성을 높이는 데 기여합니다.

🤔자바의 문자열은 왜 두 가지 생성 방식을 지원할까?

1. 문자열 리터럴 방식 (String Literal)

예시: String s = "Hello";

동작 방식:

  • JVM은 String 리터럴을 만나면 먼저 **문자열 상수 풀(String Constant Pool)**이라는 특별한 메모리 영역을 확인합니다.
  • 만약 풀에 동일한 내용을 가진 문자열이 이미 존재한다면, 새로운 객체를 생성하지 않고 풀에 있는 기존 객체의 참조를 반환합니다.
  • 만약 풀에 동일한 내용의 문자열이 없다면, 새로운 String 객체를 풀에 생성하고 그 참조를 반환합니다.

장점:

  • 메모리 효율성: 동일한 문자열에 대해 여러 번 객체를 생성하지 않고 재사용하므로 메모리 사용을 최적화할 수 있습니다.
  • 성능: 기존 객체를 재사용할 수 있어 객체 생성 오버헤드를 줄입니다.
  • 간결성: 코드가 더 간결하고 읽기 쉽습니다.

주요 사용 목적:

  • 대부분의 경우 문자열을 선언하고 초기화할 때 사용합니다.
  • 동일한 문자열이 여러 번 사용될 가능성이 있는 경우에 특히 효율적입니다.

2. new 연산자를 통한 생성 방식

예시: String s = new String("Hello");

동작 방식:

  • new 연산자를 사용하면 문자열 상수 풀을 확인하지 않고, 힙(Heap) 메모리 영역에 항상 새로운 String 객체를 생성합니다.
  • 만약 new String("Hello")와 같이 리터럴을 인자로 전달하면, "Hello" 리터럴은 문자열 상수 풀에 생성되거나 재사용될 수 있지만, new String()에 의해 별개의 새로운 "Hello" 객체가 힙에 생성됩니다.

장점:

  • 명시적인 새로운 객체 생성: 항상 새로운 String 객체를 생성해야 할 필요가 있을 때 사용합니다. 예를 들어, 두 문자열의 내용이 같더라도 메모리상에서 완전히 다른 객체로 존재해야 함을 보장해야 할 때 유용합니다.

단점:

  • 메모리 비효율성: 동일한 내용의 문자열이라도 new를 사용할 때마다 새로운 객체가 생성되므로 메모리 사용량이 늘어날 수 있습니다.
  • 성능 저하: 객체 생성 오버헤드가 발생합니다.

주요 사용 목적:

  • 두 문자열 객체가 내용이 같더라도 서로 다른 메모리 주소를 가지도록 강제해야 할 때 (예: 특정 객체 동일성 비교가 중요한 경우).
  • 매우 드물지만, 문자열 상수 풀에 너무 많은 문자열이 쌓여 메모리 문제가 발생할 가능성을 피하고 싶을 때 (일반적인 애플리케이션에서는 거의 고려되지 않음).
  • 자바가 두 가지 문자열 생성 방식을 제공하는 것은 개발자가 특정 시나리오에 따라 메모리 사용과 성능을 최적화할 수 있도록 유연성을 주기 위함입니다. 대부분의 경우 문자열 리터럴 방식이 효율성과 간결성 면에서 더 권장되며, new String()은 특정 목적(예: 명시적으로 새로운 객체 생성 보장)을 위해 제한적으로 사용됩니다.

결론:

자바가 두 가지 문자열 생성 방식을 제공하는 것은 개발자가 특정 시나리오에 따라 메모리 사용과 성능을 최적화할 수 있도록 유연성을 주기 위함입니다. 대부분의 경우 문자열 리터럴 방식이 효율성과 간결성 면에서 더 권장되며, new String()은 특정 목적(예: 명시적으로 새로운 객체 생성 보장)을 위해 제한적으로 사용됩니다.

public class InternEx {
    public static void main(String[] args) {
        String str1 = "Hello";
        String str2 = new String(new char[]{'H','e','l','l','o'}).intern();

        System.out.println(str1 == str2);
        // 해당 코드의 의미는 무엇인가?
        // intern 메소드의 기능을 확인하는 코드로 이미 literal constant pool에 존재하는 문자열인지 확인하고, 존재한다면 그 주소값을 str2에 할당해줌
        // 그러므로 true를 반환함.

        String str3 = new String("Hi");
        String str4 = "Hi";

        str3 = str3.intern();
        System.out.println(str3 == str4); // true

    }
}

intern() 메소드

  • intern() : 자바에서 문자열을 최적화하여 관리하기 위한 메소드
  • 문자열을 리터럴로 선언할 경우 내부적으로 String의 intern() 메소드가 호출이 된다.
  • 해당 리터럴이 문자열 상수 풀(Literal constant pool)안에 존재하는지 확인,
  • 풀에 존재한다면: 풀에 있는 해당 문자열 객체의 참조(주소)를 반환합니다. 즉, 현재 객체는 버려지고 풀에 있는 기존 객체를 재사용합니다.
  • 풀에 존재하지 않는다면: 현재 String 객체를 문자열 상수 풀에 추가하고, 이 객체의 참조를 반환합니다.
public class InternEx {
    public static void main(String[] args) {
        String str1 = "Hello";
        String str2 = new String(new char[]{'H','e','l','l','o'}).intern();
        System.out.println(str1 == str2);
        // 해당 코드의 의미는 무엇인가?
        // intern 메소드의 기능을 확인하는 코드로 이미 literal constant pool에 존재하는 문자열인지 확인하고, 존재한다면 그 주소값을 str2에 할당해줌
        // 그러므로 true를 반환함.

        String str3 = new String("Hi");
        String str4 = "Hi";

        str3 = str3.intern();
        System.out.println(str3 == str4); // true

    }
}

자주 활용되는 메소드

  • charAt(), length(), replace(), substring() , indexOf() , contains() , split()
  • Class Character의 메소드
    • isUpperCase() : 대문자인지 소문자인지
    • isDigit() : 숫자인지 아닌지

StringBuffer 클래스

  • + 연산자를 이용하여 String 인스턴스의 문자열을 결합하면, 내용이 합쳐진 새로운 String 인스턴스가 생성됨.
  • 문자열을 많이 결합하면 할수록 메모리 낭비, 속도도 느려지는 단점이 있다.
  • StringBuffer 클래스는 클래스 내부에 버퍼(buffer. 데이터를 임시로 저장하는 메모리)라는 독립적인 공간을 가지고 있어 문자열을 바로 바로 추가할 수 있어 공간의 낭비 없이 문자열 연산(추가, 수정, 삭제) 속도를 빠르게 처리할 수 있다.
  • 문자열의 수정이 빈번한 경우에 사용하는 것이 적합하며, 네트워크 상에서 데이터를 송수신하는 경우는 불변타입을 사용하는 것이 적합하다. (Ex. json 타입)
        StringBuffer sb1 = new StringBuffer();
        sb1.append("hello");
        sb1.append(" ");
        sb1.append("Java programming");
        sb1.append("!!!");

        String result1 = sb1.toString();
        System.out.println(result1);

    String str = "abcdefg";
        StringBuffer sb = new StringBuffer(str);
        System.out.println("초기 상태 : " + sb);

        // StringBuffer를 String 타입으로 변환
        System.out.println("초기 상태 : " + sb.toString());
        // str문자열에서 cd를 출력하세요
        System.out.println("문자열 추출 :" + sb.substring(2,4));

        //str문자열에 "2" 추가하세요,
        System.out.println("문자 추가 : "+sb.insert(2, "추가"));

        // sb에서 문자열을 삭제
        System.out.println("문자 삭제 : " +sb.delete(2,4));

        // sb에 문자 붙이기 : append()
        System.out.println("문자 붙이기 : "+sb.append("hijl"));

        // sb의 길이 : length()
        System.out.println("문자열의 길이 : "+sb.length());

        // buffer 용량
        System.out.println("용량 : " + sb.capacity());

        // 문자열의 역순 : reverse()
        System.out.println("문자열의 역순 : "+sb.reverse());

        // 현상태 문자열 확인
        System.out.println("마지막 상태 체크: " + sb);
반응형