3. String 클래스

String 클래스 - 기본

자바에서 문자를 다루는 대표적인 타입은 char, String 2가지가 있다.

기본형인 char 는 문자 하나를 다룰 때 사용한다. char 를 사용해서 여러 문자를 나열하려면 char[] 을 사용해야 한다. 하지만 이렇게 char[] 을 직접 다루는 방법은 매우 불편하기 때문에, 자바는 문자열을 매우 편리하게 다룰 수 있는 String 클래스를 제공한다.

String 클래스를 통해 문자열을 생성하는 방법은 2가지가 있다.

  • 리터럴 사용 : "hello"

  • 객체 생성 : new String("hello")

기본적으로 String 은 클래스이다. 기본형 타입(Primitive type) 이 아니다. 따라서 String 을 리터럴로 사용하는 다음의 코드는 뭔가 어색하다.

String str1 = "hello";

문자열은 매우 자주 사용된다. 때문에 편의상 쌍따옴표로 문자열을 감싸면 자바 언어에서 new String("hello") 와 같이 변경해준다. (이 경우 실제로는 성능 최적화를 위해서 문자열 풀을 사용한다)

📌 문자열 리터럴의 시점 별 구체적인 동작 흐름

1. 컴파일 결과 (.class)

  • Constant Pool에 "hello"라는 CONSTANT_String_info 항목 추가됨

  • 바이트코드엔 ldc #2 같은 식으로 "hello"의 인덱스를 참조함

2. 클래스 로딩 시점

  • JVM은 .class에서 Constant Pool을 읽어 Runtime Constant Pool 생성 (이때도 "hello"String 객체가 아닌, UTF-8 바이트만 있는 심볼 상태)

3. 실행 시점 (ldc #2)

  • JVM이 RCP의 2번 인덱스를 참조

  • "hello"라는 심볼릭 문자열 리터럴을 intern pool에 조회

    • 없으면 new String("hello") 수행 후 intern pool에 등록

    • 있으면 intern pool에 등록된 Heap 객체를 사용

📌 String Constant Pool (Intern Pool)

  • intern pool은 Java의 Heap 영역에 존재하며, 동일한 문자열 리터럴이 여러 번 생성되는 것을 방지합니다.

  • "hello"처럼 문자열 리터럴은 자동으로 intern 처리되지만, new String("hello")와 같이 명시적으로 객체를 생성한 경우에는 intern pool에 등록되지 않습니다.

  • Java 6까지는 intern pool이 PermGen에 존재했지만, Java 7부터는 Heap으로 이동하여 메모리 유연성이 향상되었습니다.

참고 : "hello" == "hello"true인데,new String("hello") == "hello"는 왜 false 일까?

  • 실제로 Java 7 부터 문자열 리터럴과 문제열 객체는 힙 공간에서 관리된다. (때문에, 같은 공간에 저장되는 것으로 생각할 수 있다)

  • 하지만, 문자열 리터럴의 경우 JVM 이 Intern Pool 이라는 논리적인(!) 공간에서 관리한다.

  • 때문에, 문자열 리터럴의 경우 == (동일성 비교) 를 해도 Intern Pool 에서 캐시된 내용을 비교하기 떄문에, true 가 나온다.

String 클래스 구조

String 클래스 구조는 대략 다음과 같이 생겼다.

속성(필드)

여기에는 String 의 실제 문자열 값이 보관된다. 문자 데이터 자체는 char[] 에 보관된다.

String 클래스는 개발자가 직접 다루기 불편한 char[] 을 내부에 감추고 String 클래스를 사용하는 개발자가 편리하게 문자열을 다룰 수 있도록 다양한 기능을 제공한다. 그리고 메서드 제공을 넘어서 자바 언어 차원에서도 여러 편의 문법을 제공한다.

참고 - 자바 9 이후 String 클래스 변경 사항

자바 9 부터 String 클래스에서 char[] 대신에 byte[] 를 사용한다.

자바에서 문자 하나를 표현하는 char 는 2byte 를 사용한다. 그런데 영어, 숫자는 보통 1byte 로 표현이 가능하다. 그래서 단순 영어, 숫자로만 표현된 경우 1byte 인 Latin-1 인코딩을 사용하고 그렇지 않은 경우 2byte 인 UTF-16 인코딩을 사용한다. 따라서 메모리를 더욱더 효율적으로 사용할 수 있도록 변경되었다. (객체 생성 시점에 해당 인코딩 타입을 체크한다)

예를 들어서 "가나다라" 문자열을 String 타입으로 저장하면 크기가 8인 byte 배열에 저장된다.

기능(메서드)

너무 많아서 패스..

필요할 때 알아서 찾아쓰자;;

String 클래스와 참조형

String 은 클래스이다. 따라서 기본형이 아니라 참조형이다.

참조형은 변수에 계산할 수 있는 값이 들어있는 것이 아니라, 레퍼런스 값이 들어있다. 따라서 원칙적으로는 + 같은 연산자를 사용할 수 없다.

  • 자바에서 문자열을 더할 때는 String 이 제공하는 concat() 과 같은 메서드를 사용해야 한다.

  • 하지만, 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 + 연산을 제공한다.

    • C++ 은 연산자 오버로딩이 존재하기 때문에, + 연산이 가능하도록 할 수 있지만 자바는 아니다..

String 클래스 - 비교

String 클래스 비교할 때는 == 비교가 아니라 항상 equals() 비교를 해야한다.

  • 동일성(Identity) : == 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인

  • 동등성(Equality) : equals() 메서드를 사용하여 두 객체가 논리적으로 같은지 확인

  • str1, str2 는 기본적으로 레퍼런스 값이 다르기 때문에, 동일성 비교를 했을 때 false 를 반환한다.

  • String 클래스는 내부 문자열 값을 비교하도록 equals() 메서드를 재정의 해두었다.

  • String str3 = "hello" 와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해서 문자열 풀을 사용한다.

  • 자바가 실행되는 시점(런타임) 에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 만들어둔다. 이때 같은 문자열이 있으면 만들지 않는다.

  • String str3 = "hello" 와 같이 문자열 리터럴을 사용하면 문자열 풀에서 "hello" 라는 문자를 가진 String 인스턴스를 찾는다. 그리고 찾은 인스턴스의 참조 (x003) 을 반환한다.

  • String str4 = "hello" 의 경우 "hello" 문자열 리터럴을 사용하므로 문자열 풀에서 str3 과 같은 x003 참조를 사용한다.

  • 문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에, 성능도 최적화 할 수 있다.

    • 참고로 문자열 풀은 힙 영역을 사용한다. 그리고 문자열 풀에서 문자를 찾을 때는 해시 알고리즘을 사용하기 때문에, 매우 빠른 속도로 원하는 String 인스턴스를 찾을 수 있다.

  • 따라서 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로, ==(동일성) 비교에 성공한다.

주의할 점

그렇다면 문자열 리터럴을 사용하면 == 비교를 하고, new String() 을 직접 사용하는 경우에만 equals() 비교를 사용하면 되지 않을까?

정답은 아니다..

개발을 하다보면 협업을 하게 되는데, 다른 개발자가 내 메서드를 사용할 때 메서드에서 문자열 리터럴을 사용하는지 new String() 을 사용하는지는 구분할 수 없다..

때문에! 안전하게 equals() 를 사용하는 것이 좋다!

String 클래스 - 불변 객체

String 은 불변 객체이다. 따라서 변경이 필요한 경우 기존 값을 변경하지 않고 새로운 결과를 만들어서 반환한다.

  • String.concat() 은 내부에서 새로운 String 객체를 만들어서 반환한다.

  • 따라서 불변과 기존 객체의 값을 유지한다.

String 이 불변으로 설계된 이유

String 이 불변으로 설계된 이유는 앞서 불변 객체에서 배운 내용에 추가로 다음과 같은 이유도 있다.

  • String 은 리터럴의 겨우 자바 내부에서 문자열 풀을 통해서 최적화한다.

  • 만약 String 내부의 값을 변경할 수 있다면(불변이 아니라면), 기존에 문자열 풀에서 같은 문자를 참조하는 변수의 모든 문자가 함께 변경되어 버리는 문제가 발생한다.

  • 위 그림의 경우 str3 이 참조하는 문자를 변경하면 str4 의 문자도 함께 변경되는 사이트 이펙트 문제가 발생한다.

  • 하지만, String 클래스는 불변으로 이러한 사이드 이펙트 문제를 고려하지 않아도 된다.

StringBuilder - 가변 String

불변인 String 클래스의 단점

불변인 String 클래스에도 단점이 있다. 아래 예를 보면 두 문자를 더하는 경우 다음과 같이 작동한다. (실제 동작하는 코드는아니다)

더 많은 문자를 더하는 경우를 생각해보자.

  • 이 경우 총 3개의 String 클래스가 추가로 생성된다.

  • 하지만, 중간에 더하기 위해서 만들어진 String 클래스는 생성 되었다가 바로 GC 에 의해서 버려지고 최종적으로 new String("ABCD") 만 사용된다.

불변인 String 클래스의 단점은 문자를 더하거나 변경할 때 마다 새로운 객체를 생성해야 한다는 점이다. 문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고, GC 를 해야 한다. 결과적으로 컴퓨터의 CPU, 메모리 자원을 더 많이 사용하게 된다. 그리고 문자열 크기가 클수록 문자열을 더 자주 변경할수록 시스템 자원을 많이 소모하게 된다.

StringBuilder

이 문제를 해결하는 방법은 단순하다. 바로 불변이 아닌 가변 String 이 존재하면 된다. 가변은 내부의 값을 바로 변경하면 되기 때문에, 새로운 객체를 생성할 필요가 없다. 따라서 성능과 메모리 사용면에서 불변보다 효율적이다.

이런 문제를 해결하기 위해 자바는 StringBuilder 라는 가변 String 을 제공한다. 물론 가변의 경우 사이드 이펙트에 주의해서 사용해야 한다.

StringBuilder 는 내부에 final 이 아닌 변경할 수 있는 byte[] 을 가지고 있다.

가변(Mutable) vs 불변(Immutable)

  • String 은 불변하다. 즉, 한번 생성되면 그 내용을 변경할 수 없다. 따라서 문자열에 변화를 주려고 할 때마다 새로운 String 객체가 생성되고, 기존 객체는 버려진다. 이 과정에서 메모리와 처리 시간을 더 많이 소모한다.

  • 반면에 StringBuilder 는 가변적이다. 하나의 StringBuilder 객체 안에서 문자열을 추가, 삭제 수정할 수 있으며, 이때마다 새로운 객체를 생성하지 않는다. 이로 인해 메모리 사용을 줄이고 성능을 향상시킬 수 있다.

  • 단, 사이드 이펙트를 주의해야 한다.

  • StringBuilder 는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String 으로 변환하는 것이 좋다.

String 최적화

자바의 String 최적화

자바 컴파일러는 다음과 같이 문자열 리터럴을 더하는 부분을 자동으로 합쳐준다.

최적화1. 문자열 리터럴 최적화

컴파일 전

컴파일 후

따라서 런타임에 별도의 문자열 결합 연산을 수행하지 않기 때문에, 성능이 향상된다.

최적화2. String 변수 최적화

문자열 변수의 경우 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에, 단순하게 합칠 수 없다.

이런 경우 다음과 같이 최적화를 수행한다.(최적화 방식은 자바 버전에 따라서 달라진다)

이렇듯 자바가 최적화를 처리해주기 때문에 지금처럼 간단한 경우에는 StringBuilder 를 사용하지 않아도 된다. 대신 문자열 더하기 연산(+) 을 사용하면 충분하다.

String 최적화가 어려운 경우

다음과 같이 문자열을 루프 안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않는다.

왜냐하면 대략 다음과 같이 최적화가 되기 때문이다.(최적화 방식은 자바 버전에 따라서 다르다)

반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 한다. 반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정된다. 이런 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없다. 따라서, 이런 상황에서는 최적화가 어렵다.

StringBuilder 는 물론이고, 아마도 대략 반복 횟수인 100,000번의 String 객체를 생성했을 것이다.

이럴 때는 직접 StringBuilder 를 사용해주면 된다.

StringBuilder 를 직접 사용하는 것이 더 좋은 경우

  • 반복문에서 반복해서 문자를 연결할 때

  • 조건문을 통해 동적으로 문자열을 조합할 때

  • 복잡한 문자열의 특정 부분을 변경해야 할 때

  • 매우 긴 대용량 문자열을 다룰 때

Last updated