[JavaSE]문자열 오브젝트 길이 & 비교법

기타 | 2006. 9. 12. 09:49
Posted by 시반

 

문자열 오브젝트의 길이는?


여러분의 텍스트 스트링의 길이는 얼마나 되는가? 사용자 입력이 데이터 필드 길이 제한 조건에 부합하는지 확인하기 위해선 그 답이 필요하다. 데이터베이스의 텍스트 필드는 입력을 일정한 길이로 제한하는 경우가 많으므로 텍스트를 제출하기 전에 길이를 확인할 필요가 있다. 이유야 어쨌든, 누구든지 이따금씩 텍스트 필드의 길이를 알아야 할 때가 있다. 많은 프로그래머들이 String 오브젝트의 length 메소드를 이용하여 그 정보를 얻으며, 대개의 경우 length 메소드는 올바른 솔루션을 제공한다. 하지만 이것이 String 오브젝트의 길이를 결정하는 유일한 방법은 아니며, 언제나 올바른 것도 아니다.

자바 플랫폼에서 텍스트 길이를 측정하는 데 세 개 이상의 일반적인 방법이 있다.

  1. char 코드 유닛의 수
  2. 문자 또는 코드 포인트의 수
  3. 바이트의 수

char 유닛 수 세기

자바 플랫폼은 유니코드 표준 을 사용하여 문자를 정의하는데, 이 유니코드 표준은 한때 고정 폭(U0000~UFFFF 범위 내의 16비트 값)으로 문자를 정의했다. U 접두사는 유효한 유니코드 문자 값을 16진수로 표시한다. 자바 언어는 편의상 char 타입에 대해 고정 폭 표준을 채택했고, 따라서 char 값은 어떠한 16비트 유니코드 문자라도 표현이 가능했다.

대부분의 프로그래머는 length 메소드에 대해 잘 알고 있고 있을 것이다. 아래의 코드는 예제 문자열에서 char 값의 수를 세는데, 예제 String 오브젝트에는 자바 언어의 \u 표기법으로 정의되는 몇 개의 단순 문자와 기타의 문자가 포함되어 있다는 점에 유의하기 바란다. \u 표기법은 16비트 char 값을 16진수로 정의하며 유니코드 표준에서 사용되는 U 표기법과 유사하다.

private String testString = "abcd\u5B66\uD800\uDF30";
int charCount = testString.length();
System.out.printf("char count: %d\n", charCount);

length 메소드는 String 오브젝트에서 char 값의 수를 센다. 이 예제 코드는 다음을 출력한다.

char count: 7

문자 유닛 수 세기

유니코드 버전 4.0이 UFFFF 이상 되는 상당히 큰 수의 새로운 문자를 정의하면서부터 16비트 char 타입은 더 이상 모든 문자를 표현할 수 없게 되었다. 따라서 자바 플랫폼 J2SE 5.0버전부터는 새로운 유니코드 문자를 16비트 char 값의 쌍(pair)으로 표현하며, 이를 surrogate pair 라고 부른다. 2개의 char 유닛이 U10000~U10FFFF 범위 내의 유니코드 문자를 대리 표현(surrogate representation)하는데, 이 새로운 범위의 문자를 supplementary characters 라고 한다.

여전히 단일 char 값으로 UFFFF까지의 유니코드 값을 표현할 수는 있지만, char 대리 쌍(surrogate pair)만이 추가 문자(supplementary characters)를 표현할 수 있다. 쌍의 최초값 또는 high 값은 UD800~UDBFF 범위에, 그리고 마지막값 또는 low 값은 UDC00~UDFFF 범위에 속한다. 유니코드 표준은 이 두 범위를 대리 쌍을 위한 특수 용도로 배정하고 있으며, 표준은 또한 UFFFF를 벗어나는 대리 쌍과 문자 값 사이의 매핑을 위한 알고리즘을 정의한다. 프로그래머는 대리 쌍을 이용하여 유니코드 표준 내의 어떠한 문자라도 표현할 수 있는데, 이런 특수 용도의 16비트 유닛을 UTF-16 이라고 부르며, 자바 플랫폼은 UTF-16을 사용하여 유니코드 문자를 표현한다. char 타입은 이제 UTF-16 코드 유닛으로, 반드시 완전한 유니코드 문자(코드 포인트)가 아니어도 된다.

length 메소드는 단지 char 유닛만을 세기 때문에 추가 문자는 셀 수 없다. 다행히도 J2SE 5.0 API에는 codePointCount(int beginIndex, int endIndex) 라는 새로운 String 메소드가 추가되었다. 이 메소드는 두 인덱스 사이에 얼마나 많은 유니코드 포인트(문자)가 있는지 알려주는데, 인덱스 값은 코드 유닛 또는 char 위치를 참조한다. 식 endIndex - beginIndex 의 값은 length 메소드가 제공하는 값과 동일하며, 이 둘의 차이는 codePointCount 메소드가 반환하는 값과 항상 동일하지는 않다. 텍스트에 대리 쌍이 포함되어 있는 경우에는 길이 수가 확실히 달라진다. 대리 쌍은 하나 또는 두 개의 char 유닛으로 된 단일 문자 코드 포인트를 정의한다.

하나의 문자열에 얼마나 많은 유니코드 문자 코드 포인트가 있는지 알아보려면 codePointCount 메소드를 사용하도록 한다.

private String testString = "abcd\u5B66\uD800\uDF30";
int charCount = testString.length();
int characterCount = testString.codePointCount(0, charCount);
System.out.printf("character count: %d\n", characterCount);

이 예제는 다음 내용을 출력한다.

character count: 6

testString 변수에는 두 개의 흥미로운 문자가 포함되어 있는데, 그것은 '배움(learning)'이라는 뜻의 일본어 문자와 GOTHIC LETTER AHSA 라는 이름의 문자이다. 일본어 문자는 유니코드 코드 포인트 U5B66을 사용하며, 이는 동일한 16진수 char 값 \u5B66을 가진다. 고딕체 글자의 코드 포인트는 U10330이고, UTF-16에서 고딕체 글자는 대리 쌍 \uD800\uDF30이다. 이 쌍은 단일 유니코드 코드 포인트를 표현하므로 전체 문자열의 문자 코드 포인트 수는 7이 아니라 6이 된다.

바이트 수 세기

String 안에는 얼마나 많은 바이트가 있을까? 그 대답은 사용되는 바이트 기반 문자 세트에 의해 결정된다. 바이트 수를 알아보려는 이유 중 하나는 데이터베이스의 문자열 길이 제한 조건을 충족하기 위해서이다. getBytes 메소드는 유니코드 문자를 바이트 기반 인코딩으로 변환하며, 이는 byte[] 를 반환한다. UTF-8 은 바이트 기반 인코딩의 일종이지만, 모든 유니코드 코드 포인트를 정확하게 표현할 수 있다는 점에서 대부분의 다른 바이트 기반 인코딩과는 구별된다.

다음 코드는 텍스트를 byte 값의 배열로 변환한다.

byte[] utf8 = null;
int byteCount = 0;
try {
  utf8 = str.getBytes("UTF-8");
  byteCount = utf8.length;
} catch (UnsupportedEncodingException ex) {
  ex.printStackTrace();

System.out.printf("UTF-8 Byte Count: %d\n", byteCount);

타깃 문자 세트가 생성될 바이트의 수를 결정한다. UTF-8 인코딩은 단일 유니코드 코드 포인트를 1~4개의 8비트 코드 유닛(=1 바이트)으로 변환한다. 문자 a , b , c , d 는 모두 4 바이트만을 요구하는 한편, 일본어 문자(한국어도 같음)는 3 바이트로 변환된다. 또한 고딕체 글자는 4 바이트를 차지한다. 결국 다음의 결과가 나오는 것을 알 수 있다.

UTF-8 Byte Count: 11


그림 1. 무엇을 세느냐에 따라 문자열의 길이가 달라진다.

요약

추가 문자를 사용하지 않으면 length codePointCount 의 반환 값 간에는 차이점이 없다고 할 수 있다. 하지만 UFFFF 범위를 넘어서는 문자를 사용할 경우에는 길이를 결정하는 다양한 방법에 관해 알아야 할 필요가 있다. 예컨대 미국에서 한국으로 제품을 발송할 경우에는 십중팔구 length codePointCount 가 서로 다른 값을 반환하는 상황이 발생한다. 데이터베이스의 문자 세트 인코딩과 일부 직렬화 포맷은 UTF-8을 최선의 방법으로 권장되고 있는데, 이 경우에도 역시 텍스트 길이는 달라진다. 결국 길이를 어떻게 활용할 것인지에 따라 다양한 측정 옵션을 사용할 수 있다.

추가 정보

본 테크 팁 주제에 관한 자세한 내용을 보려면 다음 리소스를 참조할 것(영문)

 

 

문자열 오브젝트 비교법


String 오브젝트는 다양한 방법으로 비교가 가능하며 그 결과 또한 종종 차이가 날 수 있다. 결과의 정확성은 주로 어떤 유형의 비교가 요구되느냐에 의해 좌우된다. 일반적인 비교 기법으로는 다음과 같은 것들이 있다.

  • == 연산자에 의한 비교.
  • String 오브젝트의 equals 메소드에 의한 비교.
  • String 오브젝트의 compareTo 메소드에 의한 비교.
  • Collator 오브젝트에 의한 비교.

== 연산자에 의한 비교

== 연산자는 String 오브젝트 레퍼런스에 사용되는데, 2개의 String 변수가 메모리 내의 동일한 오브젝트를 가리킬 경우 비교값으로 true를 리턴한다 . 그렇지 않은 경우, 텍스트가 동일한 문자 값을 가지는지 여부에 관계없이 비교값으로 false 를 리턴한다. == 연산자는 실제 char 데이터를 비교하지 않는데, 이 점을 간과할 경우 작업자는 그 후의 코드에 'The strings are unequal(문자열이 동일하지 않다)' 라고 나타나는 것에 당황해 할 수도 있다.

String name1 = "Michèle";
String name2 = new String("Michèle");
if (name1 == name2) {
  System.out.println("The strings are equal.");
} else {
  System.out.println("The strings are unequal.");

자바 플랫폼은 문자열 리터럴 및 상수를 위한 내부 풀을 생성한다. 정확하게 동일한 char 값을 가지는 문자열 리터럴과 상수는 풀 내에 단 한번 존재하게 된다. 또한 String 리터럴 및 상수를 동일 char 값과 비교하면 그 결과가 항상 동일하다는 것을 알 수 있다.

equals 메소드에 의한 비교

equals 메소드는 두 문자열의 실제 char 내용을 비교하는데, 이 메소드는 2개의 String 오브젝트가 동일한 값을 가지는 char 데이터를 보유할 경우 true 를 반환한다. 다음의 코드 예제에서는 'The strings are equal(문자열이 동일하다)' 이 출력된다.

String name1 = "Michèle";
String name2 = new String("Michèle");
if (name1.equals(name2) {
  System.out.println("The strings are equal.");
} else {
  System.out.println("The strings are unequal.");

compareTo 메소드에 의한 비교

compareTo 메소드는 equals 메소드와 유사하게 char 값을 비교한다. 아울러, 이 메소드는 자체 String 오브젝트가 인수 문자열에 선행할 경우 음의 정수를 반환하고, 문자열이 동일할 경우에는 0을 반환한다. 반면에 오브젝트가 인수 문자열 뒤에 올 경우에는 양의 정수를 반환한다. compareTo 메소드는 cat hat 에 선행한다는 것을 나타낸다. 이 비교에서 알아두어야 할 가장 중요한 점은 메소드가 char 값을 축어적으로 비교한다는 점인데, 가령 메소드는 cat 의 'c' 값이 hat 의 'h'보다 적은 숫자 값을 가진다고 판단한다.

String w1 = "cat";
String w2 = "hat";
int comparison = w1.compareTo(w2);
if (comparison < 0) {
  System.out.printf("%s < %s\n", w1, w2);
} else {
  System.out.printf("%s < %s\n", w2, w1);

위의 코드 예제는 compareTo 메소드의 동작을 보여주며 cat < hat 을 출력한다. 우리가 예상한 결과이긴 한데, 그렇다면 이 경우 어떤 결함과 문제점이 생길 수 있는가?

오류 산출

사전을 이용할 때처럼 텍스트를 자연어로서 비교하려 하면 문제가 발생하게 된다. String 클래스는 자연어의 관점에서 텍스트를 비교할 능력을 가지고 있지 않은데, 이 클래스의 equals compareTo 메소드는 문자열의 개별 char 값을 비교한다. name1 의 인덱스 n 에 해당하는 char 값과 name2 의 인덱스 n 에 해당하는 char 값이 양 문자열의 모든 n 에 대해서 동일할 경우 equals 메소드는 true 값을 리턴한다.

동일한 compareTo 메소드에 대해 cat Hat 의 비교를 요구할 경우 혼란스런 결과가 산출된다. 일반적으로 사람들은 대문자 여부와 관계없이 cat Hat 보다 앞선다는 것을 알고 있다. 하지만, compareTo 메소드는 Hat < cat 으로 표시하는데, 이유인 즉 유니코드 문자표에서는 대문자가 소문자에 선행하기 때문에 이런 결과가 나오는 것이다. ASCII 문자표에서도 같은 순서가 적용된다. 따라서 분명한 것은 애플리케이션 사용자에게 정렬된 텍스트를 제시하고자 할 경우 이런 순서가 항상 바람직한 것은 아니라는 점이다.

문자열의 동일성을 판단하려고 할 때에도 잠재적인 문제점이 나타날 수 있다. 즉, 텍스트는 복수의 내부 표현을 가질 수 있는데, 가령 'Michèle'이라는 불어 이름에는 유니코드 문자 시퀀스 M i c h è l e 이 포함된다. 하지만, 시퀀스 M i c h e ` l e 을 사용할 수도 있다. 두 번째는 '시퀀스 조합'('e' '`')을 사용하여 'è'를 표시하는 경우이다. 유니코드를 이해하는 그래픽 시스템은 이 두 표현의 내부 문자 시퀀스가 약간 다르더라도 동일하게 보이도록 디스플레이할 것이고, String 오브젝트의 단순한(simplistic) equals 메소드는 이 두 문자열이 다른 텍스트를 가지는 것으로 표시한다. 즉, 이들은 사전적으로(lexicographically) 동일한 것이 아니라 언어학적으로(linguistically) 동일한 것이다.

아래의 코드 단편으로는 'The strings are unequal.'이 나타난다. equals compareTo 메소드 모두 이들 문자열의 언어학적 동일성을 이해하지 못한다.

String name1 = "Michèle";
String name2 = "Miche\u0300le"; //U0300 is the COMBINING GRAVE ACCENT
if (name1.equals(name2)) {
  System.out.println("The strings are equal.");
} else {
  System.out.println("The strings are unequal.");

이름들의 목록을 정렬하려고 할 경우, 문자열의 compareTo 메소드는 거의 십중팔구 잘못된 결과를 표시하게 된다. 특정 이름을 검색하고자 할 경우, 사용자가 시퀀스 조합을 입력하거나 데이터베이스가 사용자의 입력과 다르게 데이터를 정규화(normalize)하면 이번에도 equals 메소드는 엉뚱한 결과를 표시한다. 문제는, 자연어 정렬 또는 검색을 수행할 때마다 문자열의 단순한 비교가 잘못된 결과를 표시한다는 것인데, 이런 연산을 수행하려면 단순한 char 값 비교보다는 더 강력한 무언가가 필요하다.

Collator 사용하기

java.text.Collator 클래스는 자연어 비교 기능을 제공하는데, 자연어 비교는 주로 특정 서체의 문자들에 대한 동일성과 순서를 결정하는 지역 고유의(locale-specific) 규칙에 의존한다.

Collator 오브젝트는 사전에서 'cat'이 'Hat'보다 먼저 나온다는 사실을 이해하므로 collator 비교를 이용할 경우 다음 코드는 cat < Hat 을 출력하게 된다.

Collator collator = Collator.getInstance(new Locale("en", "US"));
int comparison = collator.compare("cat", "Hat");
if (comparison < 0) {
  System.out.printf("%s < %s\n", "cat", "Hat");
} else {
  System.out.printf("%s < %s\n", "Hat", "cat" );

collator는 경우에 따라(대개는 자연어 처리가 중요한 경우) 문자 시퀀스 M i c h è l e M i c h e ` l e 과 동일하다는 것을 알고 있다.

다음 비교에서는 Collator 오브젝트가 사용되는데, 이는 시퀀스 조합을 인식하여 두 문자열이 동일한 것으로 판단한다. 따라서 ' The strings are equal.'이라고 나타난다.

Collator collator = Collator.getInstance(Locale.US);
String name1 = "Michèle";
String name2 = "Miche\u0300le";
int comparison = collator.compare(name1, name2);
if (comparison == 0) {
  System.out.println("The strings are equal.");
} else {
  System.out.println("The string are unequal.");

Collator 오브젝트는 심지어 몇 단계로 구분하여 문자 차이를 이해할 수 있다. 가령 e d 는 2개의 서로 다른 글자인데, 이런 차이는 '1차적' 차이이다. 한편 글자 e è 도 역시 다르지만, 그 차이는 '2차적' 특성을 띤다 할 수 있다. 또한 Collator 인스턴스 구성 방식에 따라 단어 'Michèle'과 'Michele'을 동일하게 간주할 수도 있다. 다음 코드는 'The strings are equal.' 을 출력한다.

Collator collator = Collator.getInstance(Locale.US);
collator.setStrength(Collator.PRIMARY);
int comparison = collator.compare("Michèle", "Michele");
if (comparison == 0) {
  System.out.println("The strings are equal.");
} else {
  System.out.println("The string are unequal.");

요약

어떤 경우에 equals 메소드가 == 연산자보다 적절한지 한 번 생각보기 바란다. 또한, 텍스트에 순서를 매겨야 할 경우 Collator 오브젝트의 자연어 비교가 필요한지 여부에 대해서도 생각해보라. 다양하고 미묘한 비교의 차이점을 살펴보고 나면, 경우에 따라 잘못된 API를 사용해 왔다는 사실을 발견하게 될지도 모른다. 이렇게 차이점을 알게 되고 나면 애플리케이션과 고객을 위해 올바른 선택을 하는 데 큰 도움이 될 수 있을 것이다.

추가 정보

본 테크 팁의 주제에 관한 자세한 내용을 보려면 다음 리소스를 참조할 것(영문)

 

출처 : JavaSE 테크팁 아카이브

'기타' 카테고리의 다른 글

VMWare로 Fedora6 설치하기 02  (0) 2007.03.10
VMWare로 Fedora Core6 설치하기 01  (0) 2007.03.10
CHM 파일이 열리지 않을 때  (0) 2006.11.09
자바 2.0의 시대  (0) 2006.08.25
window update 정품인증 회피(3월1일 이후)  (0) 2006.05.08
 
블로그 이미지

시반

시반(詩伴)이란 함께 시를 짓는 벗이란 뜻을 가지고 있습니다. 함께 나눌수 있는 그런 공간이길 바라며...

카테고리

분류 전체보기 (233)
개발 이야기 (73)
WEB2.0 (57)
DB2 (24)
MySQL (6)
오라클 (26)
기타 (44)
취미 (0)
잡담 (2)