[Java] String의 불변성(Immutable)

bien·2026년 1월 1일

java

목록 보기
14/14

문자열 다루기

1. String의 불변성 (Immutable)

불변성의 의미

  • 불변성(Immutable)이란 객체의 상태나 값이 생성된 후에는 변경할 수 없는 특성을 말한다.
  • 즉, 문자열(String)은 한 번 생성되면 그 값이 변하지 않는다는 것을 의미한다.
String str = "Java";
str += " Programming";

예를들어 위와 같이 코드를 작성하면, 기존의 "Java"가 변하는 것이 아니다.
메모리(Heap)상에 "Java Programming"이라는 새로운 객체가 생성되고, 변수 str이 새로운 주소값을 가리키게 되는 것이다.

왜 문자열은 불변성일까?

  1. 안전성 (Thread Safety)
    • 문자열은 여러 스레드에서 동시에 사용할 수 있는데, 문자열이 불견 객체이므로 값이 변하지 않는다. 이 덕분에 여러 스레드에서 같은 문자열을 동시에 사용할 때 데이터 경합(data race)나 동기화 문제가 발생하지 않는다.
  2. 메모리 효율성
    • 동일한 값을 가진 문자열이 여러 곳에서 사용되더라도, 자바는 문자열 상수 풀(String Constant Pool)을 사용하여 메모리 중복을 최소화한다. 즉, 같은 값을 가진 문자열 객체는 하나만 만들어지고, 다른 변수들이 이를 참조하게 되므로 메모리 낭비를 줄일 수 있다.
  3. 성능 향상
    • 불변성을 통해 문자열을 안전하게 공유할 수 있기 때문에, 자주 사용되는 문자열에 대해 메모리 할당을 줄여 성능을 향상시킬 수 있다.

intString으로 알아보는 저장방식의 차이

  • int 타입 변수 num의 경우
    • 변수에 할당된 스택 메모리에 값을 바로 저장하고 있기 때문에 10에서 20으로 값을 변경할 경우, 변수가 갖고 있는 메모리에서 값을 변경해버린다.
    • 10이라는 값이 할당되어 있던 메모리 내에서 값을 20으로 변경한 것이기 때문에 이는 가변이라고 할 수 있다.
  • String 타입 변수 str의 경우
    • 문자열 데이터는 스택 메모리에 직접 저장되는 것이 아니라 Heap 영역 중에서 String Constant Pool이라는 곳에 메모리를 할당받아 거기에 값을 저장한다.
    • str은 바로 그 주소값을 참조하게 된다.
    • 실제로 str = "abc" 후에 str = "def"가 실행되어 str이라는 변수가 갖는 참조값이 0x11에서 0x22로 바뀐다고 하더라도 그건 str변수가 갖는 참조값이 변경된 것이지 실제 abc가 저장되어 있는 0x11 주소의 데이터가 바뀌는 것은 아니다.
      • 이렇게 Java에서 String은 불변성을 획득하게 된다.

String Constant Pool이란?

  • 위치: Java 7부터는 Heap 영역 내부에 위치한다. (이전에는 PermGen에 위치하여 크기 제한이 엄격햇으나, 현재는 GC의 관리 대상이 되어 메모리 관리가 유연해졌다.)
  • 작용 원리:
    • 문자열 리터럴을 생성할 때, JVM은 String Constant Pool을 확인한다.
    • 이미 같은 문자열이 있다면 새로 만들지 않고 기존의 주소를 재사용한다.
    • 이를 통해String Pool은 Runtime에서 많은 양의 String 객체를 생성하게 되더라도 많은 양의 메모리 공간을 절약할 수 있습니다.

new String() vs String = ""

  • Java에서 String은 조금 특별한 객체인데, 다른 클래스와 마찬가지로 new 키워드와 생성자를 통해 객체를 생성할 수 있을 뿐만 아니라, 생성자 없이 큰 따옴표(")를 통해서도 객체를 만들 수 있기 때문이다.
    • 이때, 두 생성 방식으로 생성된 String 객체에는 차이가 있다.
String s1 = "Cat";
String s2 = "Dog";
String s3 = new String("Cat");
String s4 = new String("Cat");

System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // false
System.out.println(s3 == s4);  // false
  • s1 & s2: String pool의 메모리를 가리키고 있다.
  • s3: String pool이 아닌 일반 객체처럼 힙(Heap) 영역에 할당된 메모리를 가리키고 있다.
  • s1 == s2 :모두 큰 따옴표를 사용해 선언되었으므로 String Pool안에 "Cat"이 할당된 같은 메모리를 가리키게 된다.
  • s1 == s3: s1은 String pool의 메모리를, s3는 Heap영역에 동적으로 할당된 메모리를 가리키기 때문에 서로 다른 메모리를 가리켜 결과로 false를 나타낸다.
  • s3 == s4: 모두 String pol이 아닌 Heap 영역에 동적으로 할당된 메모리를 가리키지만, 각자 본인의 메모리를 별도로 할당받기 때문에 결과는 false를 나타낸다.

String의 hashCode 캐싱

  • hashCode란?
    • hashCode는 객체의 고유한 정수값을 반환하는 메서드이다.
    • 자바에서 hashCode해시 테이블 또는 해시 기반 자료구조(HashMap, HashSet)에서 객체를 빠르게 찾거나 구별하는데 사용된다.
    • hashCode는 객체를 빠르게 비교할 수 있도록 해주는 고유한 숫자를 반환하며, 이 숫자를 기반으로 객체의 저장 위치를 결정할 수 있다.
  • String 객체에서 hashCode 캐싱
    • 자바에서는 String 클래스의 hashCode() 메서드를 호출하면 문자열의 해시 값을 계산해 반환한다. 중요한 점은 String 객체는 불변(immutable) 객체이므로, 한 번 계산된 hashCode 값은 절대 반환하지 않는다. 이 특성을 활용해 자바는 StringhashCode값을 한 번만 계산하고, 그 값을 캐시하여 재사용한다. 이를 통해 성능을 개선할 수 있다.
  • hashCode() 메서드 구현
    • String 클래스의 hashCode()메서드는 문자열의 각 문자를 순차적으로 읽어가면서 그 값을 기반으로 문자열에 대한 고유한 해시 값을 생성한다.
    • String의 hashCode() 메서드 구현 원리:
      • 문자열을 순차적으로 읽으며 각 문자의 아스키 값에 31을 곱한 뒤 더하는 방식
      • 이 방법을 사용하면 문자열이 같으면 해시값이 동일하게 나오게 된다.
    • public int hashCode() {
      	int h = 0;
          for (int i = 0; i < vaule.length; i++) {
          	h = 31 * h + value[i]; // 'value'는 문자열을 저장하는 char 배열
          }
          return h;
      }
  • hashCode 캐싱 과정
    • String은 불변 객체이므로 hashCode 값은 객체가 처음 생성될 때 한 번만 계산되고, 그 이후에는 해시 값을 재계산하지 않고 캐시된 값을 사용한다.
    • String s1 = "hello";
      System.out.println(s1.hashCode()); // "hello"에 대한 해시 값 계산
      System.out.println(s1.hashCode()); // 같은 해시 값 캐시에서 반환
    • 위 코드에서, "hello"라는 문자열에 대해 hashCode를 처음 호출하면 해시 값이 계산된다. 이후 동일한 String 객체에 대해 hashCode를 호출하면 계산된 해시 값이 메모리에서 캐시되어 바로 반환된다. 이로 인해 반복적인 hashCode() 호출에 대한 성능이 향상된다.
  • StringhashCode 캐싱과 HashMap
    • String은 자주 사용되는 객체이므로 HashMap과 같은 자료구조에서 키로 많이 사용된다. 이때, String의 불변성과 hashCode 캐싱 덕분에 해시 충돌이 적고 검색이 빨라지는 효과가 있다. 예를들어, HashMap에서 String을 키로 사용하면 hashCode를 통해 빠르게 위치를 찾고, 저장 위치에 해당하는 값을 빠르게 검색할 수 있다.
  • 캐쉬된 hashCode값의 사용 예시
    • HashMap<String, Integer> map = new HashMap<>();
      map.put("apple", 1);
      map.put("banana", 2);
      
      // 동일한 "apple" 키에 대해 'hashCode'가 이미 캐시되어 있으므로 빠르게 검색됨
      System.out.println(map.get("apple")); // 1
    • 위 코드에서는 apple이라는 문자열을 hashMap의 키로 사용한다. applehashCode값은 첫 번째 호출 시 계산되고, 이후에 캐시된 값을 사용하여 빠르게 검색할 수 있다.

2. StringBuilder: 가변성(Mutable)의 활용

String 클래스는 불변(immutable) 특성을 갖고 있기 때문에 문자열을 변경할때마다 새로운 객체를 생성하게 되며, 이는 성능 저하를 초래할 수 잇다. 이에 비해 StringBuilder는 가변(mutable)한 특성을 갖고 있어, 자주문자열을 변경하거나 조작해야하는 경우 매우 효율적인 클래스다.

핵심 동작원리

  1. Buffer 구조
    • StringBuilder는 내부적으로 char[] 배열(혹은 byte[])을 사용하여 데이터를 저장.
      • 기본적으로 16자리 정도의 공간을 할당받고, 필요에 따라 더 큰 배열로 확장된다.
      • 초기 크기는 변경할 수 있으며, StringBuilder(int capacity)와 같이 생성자를 통해 크기를 지정할 수 있습니다.
    • 이 배열은 고정 크기를 갖지 않으며, 동적으로 크기를 조정할 수 있습니다.
  2. 직접 수정
    • 문자열을 변경할 때마다 새로운 객체를 만들지 않고 기존 배열에 바로 수정을 가한다.
      • append() 메서드처럼 문자열을 추가할 때, StringBuilder는 새로운 문자열을 만들지 않고 기존 배열에 데이터를 추가한다.
    • 이 방식은 String에 비해 메모리 할당과 객체 생성 비용을 크게 줄여주어, 문자열을 반복적으로 수정할 때 효율적이다.
  3. 동적 확장
    • StringBuilder는 문자열이 추가되거나 수정될 때마다 내부 배열이 꽉 차면 문자열의 크기가 두 배로 늘어나는 방식 자동으로 크기가 확장됩니다.

예시: StringBuilder 사용

StringBuilder sb = new StringBuilder();
sb.append("Hello"); // 기존 객체에 문자열 추가
sb.insert(5, ","); // 특정 위치에 삽입 (Hello, World)
sb.delete("World"); // 특정 구간 삭제 (World)

System.out.println(sb.toString());  // 출력: Hello World

위의 예시에서 StringBuilder는 append() 메서드를 사용하여 문자열을 이어붙입니다. 이때 StringBuilder는 기존 배열에 추가하는 방식으로 작업하며, String처럼 매번 새로운 객체를 만들지 않아서 성능상 유리합니다.

String vs StringBuilder 비교 분석

  1. 불변성 vs 가변성
    • String: 불변 객체
      • 문자열을 변경하려면 새로운 String 객체를 생성해야 하므로, 문자열을 자주 수정해야 하는 경우 비효율적
    • StringBuilder: 가변 객체
      • 기존 문자열을 수정할 수 있어 메모리 사용을 최적화하고 성능을 크게 향상시킬 수 있다.
  2. 메모리 관리
    • String: 동일한 문자열이 여러 번 사용될 경우 문자열 상수 풀(String Constant Pool)을 사용하여 메모리 중복을 최소화
    • StringBuilder는 동적 크기 조정을 통해 메모리를 관리하지만, 메모리 낭비가 발생할 수 있다.
  3. 사용 용도
    • String: 문자열을 변경할 필요가 없거나 불변성을 보장해야 하는 경우에 사용한다.
      • 예: 상수 문자열, 비교 연산
    • StringBuilder: 문자열을 자주 조작해야 하는 경우 사용.
      • 예: 반복문 내에서 문자열을 이어 붙이는 경우, 텍스트를 동적으로 생성하는 경우 등에서 유리합니다.
  4. 스레드 안전성
    • String: 불변 객체이므로 스레드 안전(Thread-safe)
      • 여러 스레드에서 동일한 String 객체를 동시에 사용할 수 있습니다.
    • StringBuilder: 가변 객체이기 때문에 스레드 안전하지 않다
      • 여러 스레드에서 동시에 접근할 경우 문제가 발생할 수 있습니다. 스레드 안전성이 필요한 경우 StringBuffer를 사용해야 합니다.

📌 최종 요약

  • 문자열을 자주 수정할 경우 StringBuilder가 성능 면에서 가장 적합하다.
    • 특히 반복문 내에서 문자열을 이어붙일 때 매우 유리하다.
  • 문자열을 자주 변경하지 않고 불변성을 보장하려면 String을 사용해야 한다.
profile
Good Luck!

0개의 댓글