오늘은 어디로 갈까...

문자열(String) 0x01 본문

낙서

문자열(String) 0x01

剛宇 2009. 2. 19. 12:14
StringUtils 클래스 만들기 위해선 알아야할 것이 무엇일까?
당연 String 클래스이다. 그럼 String 클래스에 대해 알아보도록 하자
String(문자열)은 말 그대로 문자의 집합이다. 내부 소스를 보면 char(문자) 배열로 구성되어진것을 볼 수 있다.
public final class String
    implements java.io.Serializable, Comparable<string>, CharSequence
{
    /** The value is used for character storage. */
    private final char value[];

java(자바)에서 문자 정보는 Unicode Standard v4.0에 근거해서 사용한다.(J2SE API 5.0 문서를 보면 자세히 나와있다.)
Unicode(유니코드)란 전세계의 모든 문자를 컴퓨터에서 일관되게 표현할 수 있게 설계한 표준이라고 보면 된다.
자세한 사항은 http://www.unicode.org/ 에 가보면 알 수 있다.
필자처럼 영어에 거부감이 강한 분들은 http://www.kristalinfo.com/K-Lab/unicode/Unicode_intro-kr.html 에 가서 참조하기 바란다.
(참고로 자바 char는 2바이트이다. 그래서 char c = '한'; 이렇게 선언이 가능하다.)
우리가 주의해서 봐야할 String 클래스의 특성에 대해서 알아보자.

첫번째 특성은 불변(Immutable)형이라는 것이다.
String str = "안녕";
str += "하세요";

'+' 연산을 한번 할때 마다, 처음 선언한 str 변수에 추가되어지는것이 아니라, 새로운 String 인스턴스(instance)가 생성된다.
즉, 첫줄의 str 변수가 가리키는 인스턴스와, 두번째줄의 str 변수가 가리키는 인스턴스가 다르다는 것이다.
그럼 문자열을 수정할때마 새로운 인스턴스가 생성되니, 쓸데 없는 자원(메모리, 성능등) 낭비가 일어나게 된다는 것이다.
그래서 만들어진 것이 StringBuffer와 StringBuilder(1.5 버젼부터 지원)이라는 가변형 문자열 클래스이다.
StringBufffer sBuf = new StrignBuffer("안녕");
sBuf.append("하세요");

이렇게 StringBuffer 를 사용하게 되면, 처음 생성한 인스턴스내에서 문자열이 바뀌게 된다.
즉, String 처럼 새로운 인스턴스를 생성하는게 아니라, 기존에 존재하는 인스턴스 내부의 값이 바뀐다는것이다.
그렇다면, 무엇이 더 효율적일까?
성능적인 관점에서만 본다면 StringBuffer가 String보다 우수하다고 볼 수 있지만, 가독성을 해칠 수 있는 단점도 존재한다.
그러니 적당한 타협선에서, 적당히(?) 사용하면 된다.
그러면 StringBuilder는 무엇일까?
이놈은 1.5부터 생겨났는데 기능은 StringBuffer와 동일하다고 보면 된다.
차이점이라면 동기화를 지원하지 않는다는것이다.
append(String) 메소드를 예를 들면 아래와 같다.
    // StringBuffer.java
    public synchronized StringBuffer append(String str) {
	super.append(str);
        return this;
    }

    // StringBuilder.java
    public StringBuilder append(String str) {
	super.append(str);
        return this;
    }

코드를 보면 StringBuilder 클래스는 synchronized 키워드(keyword)가 없다. 즉 동기화가 안된다는 말씀.
동기화를 지원하지 않기때문에, 멀티 쓰레드(Thread) 환경에서 동시에 호출을 할 경우 엉뚱한 결과가 나올 수도 있다는 것이다.
예를 들어, StringBuilder의 인스턴스를 생성했다고 보자.
서로 다른 2개의 쓰레드에서 동시에, append("안녕하세요"); 를 호출하면 에러가 날 수도 있고, 어떤 값이 나올지 예상할 수 없다는 것이다.
운이 좋으면 "안녕하세요안녕하세요" 란 결과를 돌려주기도 하지만, 그건 어디까지나 확률상에 의존한다는 말이다.
StringBuffer 인스턴스를 생성하여 똑같이 서로 다른 2개의 쓰레드에서 동시에, append("안녕하세요")를 호출하면,
항상 "안녕하세요안녕하세요" 결과를 돌려준다.
그럼 왜??? 동기화를 지원하지 않는 StringBuilder가 만들어졌느냐?
말 그대로 동기화를 하지 않기 때문에, 동기화를 하는 것보다 속도가 빠르다는 것이다.
(이 동기화란 놈이 멀티 쓰레드 환경에서 안정성(?)을 보장해 주지만, 속도를 갉아먹는 무서운 놈이다.)
그래서 동기화가 필요한 경우는 StringBuffer 클래스를, 동기화가 필요 없는 경우는 StringBuilder 클래스를 사용하는게 좋다.
여담으로, StringBuffer와 StringBuilder를 생성할때 capacity를 지정할 수 있는데, 문자열의 길이를 예측할 수 있다면 넣어주는게 성능상 좋다. StringBuffer와 StringBuilder도 내부적으로 char[]를 사용하는데 문자열의 총길이가 capacity보다 크다면 늘려주는 작업을 하게 되므로, 아주 조금~~~의 성능 향상을 기대해볼 수도 있다.


자. 두번째 특성에 대해서 알아보도록하자
String 클래스를 이용하여 문자열을 생성하는 방법은 크게 2가지가 있다.
예를들어, "안녕하세요"라는 문자열을 만들때
String str = "안녕하세요";
String str = new String("안녕하세요");

첫번째 방법처럼 리터럴을 사용하여 문자열을 생성할때에는, 
문자열풀(A Pool of Strings)에서 해당 문자열이 존재하면 그 레퍼런스를 돌려주고, 없다면 문자열을 생성한다음 그 레퍼런스를 돌려준다. 물론 생성된 문자열은 풀에 등록된다.
(이 문자열풀은 클래스와 같은 Heap의 Permanet area(고정 영역)에 생성되며, 해당 프로세스의 종료까지 살아있게 된다.)
String a = "안녕하세요";
String b = "안녕하세요";
System.out.println(a == b);
System.out.println(System.identityHashCode(a));
System.out.println(System.identityHashCode(b));

출력결과:
true
29855319
29855319

a == b는 같은 인스턴스이니 당연히 true가 반환된다.
(등가비교연산자('==') 는 주소값을 비교한다.)

두번째 방법처럼 new로 생성해서 사용할 경우 매번 문자열이 생성된다.
(new로 객체를 생성했으니 Heap의 Young Area에 생성된다.)
String a = new String("안녕하세요");
String b = new String("안녕하세요");
System.out.println(a == b);
System.out.println(System.identityHashCode(a));
System.out.println(System.identityHashCode(b));

출력결과:
false
29855319
5383406

a == b는 다른 인스턴스이니 false가 반환된다.
레퍼런스 주소값이 아니라, 문자열값을 비교하려면 아래처럼 equals() 메소드를 사용하면 된다.
String a = new String("안녕하세요");
String b = new String("안녕하세요");
System.out.println(a == b);
System.out.println(a.equals(b));
System.out.println(System.identityHashCode(a));
System.out.println(System.identityHashCode(b));

자, 심화학습을 해보자.
String a = new String("안녕하세요");
String b = "안녕하세요";
String c = new String("안녕하세요").intern();
System.out.println("a == b      --> " + (a == b));
System.out.println("a.equals(b) --> " + (a.equals(b)));
System.out.println("a.equals(c) --> " + (a.equals(c)));
System.out.println("b == c      --> " + (b == c));
System.out.println("b.equals(c) --> " + (b.equals(c)));

이럴 경우 어떻게 출력될까?
출력결과:
a == b      --> false
a.equals(b) --> true
a.equals(c) --> true
b == c      --> true
b.equals(c) --> true

여기서 주의해볼것이, b == c  즉 같은 레퍼런스를 가지고 있다는 것이다.
String 클래스의 intern() 메소드는, 리터럴을 사용하여 문자열을 생성하는것과 같다. 
문자열풀(A Pool of Strings)에서 해당 문자열이 존재하면 그 레퍼런스를 돌려주고, 없다면 문자열을 생성한다음 그 레퍼런스를 돌려준다는 얘기다.
즉, "안녕하세요" == new String("안녕하세요") 라는 것이다.

좀 더 머리게 아프게 해보자
String a = "안녕";
String b = "안" + "녕";
String c = "안" + "녕".intern();
System.out.println("a == b      --> " + (a == b));
System.out.println("a == c      --> " + (a == c));

출력결과
a == b      --> true
a == c      --> false

왜 이런 결과가 나왔을까?
정확히는 모르겠지만, 추측으로는 자바 컴파일러의 최적화에 의해 "안" + "녕" 이놈이 "안녕" 으로 바껴서 컴파일 되는것 때문에 a와 b가 동일하게 되는게 아닐까....