반응형

자바에서 문자열은 가장 많이 쓰이는 타입 중 하나가 아닐까 싶다. 그만큼 적절하게 사용하는 방법을 알아 둔다면 큰 도움이 될 수 있다. 이번에는 어떠한 경우에 문자열 사용을 피해야 하는지와 문자열 연결에서 주의해야 할 점을 알아보자.

 

문자열은 다른 값 타입을 대신하기에 적합하지 않다. 많은 사람이 파일, 네트워크, 키보드 입력으로부터 데이터를 받을 때 주로 문자열을 사용한다. 자연스러워 보이지만, 입력받을 데이터가 진짜 문자열일 때만 그렇게 하는 게 좋다. 받은 데이터가 수치형이라면 int, float, BigInteger 등 적당한 수치 타입으로 변환해야 한다. 예나 아니요 같은 질문의 답이라면 적절한 열거 타입이나 boolean으로 변환해야 한다. 일반화해 이야기하자면 기본 타입이든 참조 타입이든 적절한 값 타입이 있다면 그것을 사용하고 없다면 새로 하나 작성하자. 의외로 지켜지지 않는 경우가 많다. 문자열은 열거 타입을 대신하기에 적합하지 않다. 상수를 열거할 때는 문자열보다는 열거 타입이 월등히 낫다. 문자열은 혼합 타입을 대신하기에 적합하지 않다. 여러 요소가 혼합된 데이터를 하나의 문자열로 표현하는 것은 대체로 좋지 않은 생각이다. 예를 들어 "String compoundKey = className + "#" + i.next();" 와 같은 방식은 단점이 많은 방식이다. 혹여라도 두 요소를 구분해주는 문자 #이 두 요소 중 하나에서 쓰였다면 혼란스러운 결과를 초래한다. 각 요소를 개별로 접근하려면 문자열을 파싱해야 해서 느리고, 귀찮고, 오류 가능성도 커진다. 적절한 equals, toString, compareTo 메서드를 제공할 수 없으며, String이 제공하는 기능에만 의존해야 한다. 그래서 차라리 전용 클래스를 새로 만드는 편이 낫다. 이런 클래스는 보통 private 정적 멤버 클래스로 선언한다. 문자열은 권한을 표현하기에 적합하지 않다. 권한을 문자열로 표현하는 경우가 종종 있다. 예를 들어 스레드 지역변수 기능을 설계한다고 해보자. 그 이름처럼 각 스레드가 자신만의 변수를 갖게 해주는 기능이다. 자바가 이 기능을 지원하기 시작한 때는 자바 2부터로, 그전에는 프로그래머가 직접 구현해야 했다. 그 당시 이 기능을 설계해야 했던 여러 프로그래머가 독립적으로 방법을 모색하다가 종국에는 똑같은 설계에 이르렀다. 바로 클라이언트가 제공한 문자열 키로 스레드별 지역변수를 식별한 것이다. 이 방식의 문제는 스레드 구분용 문자열 키가 전역 이름공간에서 공유된다는 점이다. 이 방식이 의도대로 동작하려면 각 클라이언트가 고유한 키를 제공해야 한다. 그런데 만약 두 클라이언트가 서로 소통하지 못해 같은 키를 쓰기로 결정한다면, 의도치 않게 같은 변수를 공유하게 된다. 결국 두 클라이언트 모두 제대로 기능하지 못할 것이다. 보안도 취약하다. 악의적인 클라이언트라면 의도적으로 같은 키를 사용하여 다른 클라이언트의 값을 가져올 수도 있다. 이 API는 문자열 대신 위조할 수 없는 키를 사용하면 해결된다. 이 키를 권한이라고도 한다. 이렇게 키를 사용하는 방법은 문자열 기반 API의 문제 두 가지를 모두 해결해주지만, 개선할 여지가 있다. 고민해보자. 

문자열에 대한 또 다른 이야기를 해보자. 자주 사용하는 문자열 연산에 관한 이야기이다. 문자열 연결 연산자(+)는 여러 문자열을 하나로 합쳐주는 편리한 수단이다. 그런데 한 줄짜리 출력값 혹은 작고 크기가 고정된 객체의 문자열 표현을 만들 때라면 괜찮지만, 본격적으로 사용하기 시작하면 성능 저하를 감내하기 어렵다. 문자열 연결 연산자로 문자열 n개를 잇는 시간은 n 제곱에 비례한다. 문자열은 불변이라서 두 문자열을 연결할 경우 양쪽의 내용을 모두 복사해야 하므로 성능 저하는 피할 수 없는 결과다. 예를 들어 다음 메서드는 청구서의 품목을 전부 하나의 문자열로 연결해준다.

public String statement() {
	String result = "";
	for (int i = 0; i < numItems(); i++)
		result += lineForItem(i);
	return result;
}

품목이 많을 경우 이 메서드는 심각하게 느려질 수 있다. 성능을 포기하고 싶지 않다면 String 대신 StringBuilder를 사용하자.

 

public String statementWithStringBuilder() {
	StringBuilder sb = new StringBuilder(numItems() * LINE_WIDTH);
	for (int i = 0; i < numItems(); i++)
		sb.append(lineForItem(i));
	return sb.toString();
}

자바 6 이후 문자열 연결 성능을 다방면으로 개선했지만, 이 두 메서드의 성능 차이는 여전히 크다. 품목을 100개로 하고 lineForItem이 길이 80인 문자열을 반환하게 하여 테스트해보니 StringBuilder를 사용했을 때가 6.5배나 빨랐다. statement 메서드의 수행 시간은 품목 수의 제곱이 비례해 늘어나고 StringBuilder를 사용했을 경우는 선형으로 늘어나므로, 품목 수가 늘어날수록 성능 격차도 점점 벌어질 것이다. StringBuilder를 사용한 예제에서 StringBuilder를 선언할 때, 전체 결과를 담기에 충분한 크기로 초기화한 점을 잊지 말자. 하지만 기본값을 사용하더라도 여전히 5.5배나 빠르다.

 

요약하자면,

더 적합한 데이터 타입이 있거나 새로 작성할 수 있다면, 문자열을 쓰고 싶은 유혹을 뿌리치자. 또한 문자열 연결에 대한 원칙은 간단하다. 성능에 신경 써야 한다면 많은 문자열을 연결할 때는 문자열 연결 연산자(+)를 피하자. 대신 StringBuilder의 append 메서드를 사용하라. 문자 배열을 사용하거나 문자열을 연결하지 않고 하나씩 처리하는 방법도 있다.

반응형

+ Recent posts