본문 바로가기

Reading/Effective Java

[Effective-Java] Item 62, 63.다른 타입이 적절하다면 문자열 사용을 피하라, 문자열 연결은 느리니 주의하라

참조에 나온 내용을 인지하고 있다는 가정하에 작성해서 두 레퍼런스를 참조했다! [참조1] / [참조2]

 

[Effective-Java] Item 62.다른 타입이 적절하다면 문자열 사용을 피하라

 

문자열 String은 텍스트를 표현하도록 설계되었지만, 워난 흔하며 자바의 좋은 지원때문에 의도하지 않은 용도로 쓰이는 경향이있다.

 

올바르지 않게 사용된 사례


1. 문자열은 다른 값 타입을 대신하기 적합하지 않다.

다양한 입력으로부터 데이터를 받을 때 주로 문자열을 사용하는데, 자연스러워 보이지만 진짜 문자열일 때만 사용하는게 좋다.

기본 타입이든 참조 타입이든 적절한 값 타입이 있다면 그것을 사용하고 없다면 새로 만들자.

 

2. 문자열은 열거 타입을 대신하기 적합하지 않다.

상수를 열거할 땐 문자열보단 열거 타입이 낫다.

 

3. 문자열은 혼합 타입을 대신하기 적합하지 않다.

여러 요소가 혼합된 데이터를 하나의 문자열로 표현하는건 대개 좋지않다.

 

String compoundKey = className + "#" + i.next();

 

className이나 i.next()에 "#"이 쓰였다면 혼란스러운 결과를 초래하고, 각 요소를 개별로 접근하려면 문자열 파싱때문에 느리고, 귀찮고, 오류 가능성도 커진다. 당연하게 equals, toString, compareTo 메소드를 제공할 수 없고 String이 제공하는 기능에 의존해야한다.

이를 대신하기 위해 아이템 24에 나왔던 private 정적 멤버 클래스를 활용하자.

 

4. 문자열은 권한을 표현하기 적합하지 않다.

스레드 지역변수 기능을 설계한다고 가정해보자. 자바 2부터 자바가 기능을 제공했기 때문에, 이전에는 개발자가 직접 구현했어야했다.

종국에는 여러 프로그래머들이 똑같은 설계를 하기 이르렀고, 클라이언트가 제공한 문자열 키로 스레드별 지역변수를 식별한 것이다.

 

public class ThreadLocal{
    private ThreadLocal() { } // 객체 생성 불가
    // 현 스레드의 값을 키로 구분해 저장
    public static void set(String key, Object value);
    // 키가 가리키는 현 스레드의 값을 반환
    public static Object get(String key);
}

 

이 방식은 스레드 구분용 문자열 키가 전역 이름공간에서 공유된다.

new 연산자를 사용할 경우 일반 Heap 영역에 계속해서 새로운 인스턴스가 생성되기 때문에, 리터럴 방식으로 제공했을 것이다.

리터럴 방식은 Heap 영역의 string constant pool에 저장된다(Perm 영역에서 GC의 대상이 되기 위해 자바 7부터 변경됨)

 

이미 존재한다면 그 메모리 주소를 그대로 참조하게 된다. 물론, new 생성자로 만든 String도 intern() 메소드를 호출하면, String constant pool 영역을 먼저 탐색해서 반환하기 때문에 동일한 문자열에 동일한 주소값을 반환할 수도 있다. 아래 코드를 참조하자.

 

String a1 = new String("a");
String a2 = "a";
String a3 = "a";
// a1만 다르고, a1.intern() ~ a3 까지 모두 동일한 주소값을 반환한다.
System.out.println("System.identityHashCode(a1) = " + System.identityHashCode(a1));
System.out.println("System.identityHashCode(a1) = " + System.identityHashCode(a1.intern()));
System.out.println("System.identityHashCode(a1) = " + System.identityHashCode(a2));
System.out.println("System.identityHashCode(a1) = " + System.identityHashCode(a3));

 

그렇다면 문자열로 제공되는 키는 고유하지 않다는 뜻이 되는데, 클라이언트 간에 커뮤니케이션 미스로 같은 키를 쓰기로 결정한다면 의도치 않게 같은 변수를 공유하게 되고, 두 클라이언트 모두 제대로 기능하지 못하게 된다. 악의적 클라이언트라면 의도적으로 같은 키를 사용해 다른 클라이언트의 값을 가져올 수 있게되기에 보안에 상당히 취약해진다.

 

이 API는 문자열 대신 위조할 수 없는 키를 사용하면 해결되고, 이 키를 권한이라고도 한다.

 

public class ThreadLocal{
    private ThreadLocal() { } // 객체 생성 불가
    public static class Key { // 권한
        Key() { }
    }
    public static Key getKey() {
        return new Key();
    }
    public static void set(Key key, Object value);
    public static Object get(Key key);
}

 

이 방법은 문자열 기반 API의 문제를 모두 해결해주지만 set, get이 정적 메소드일 이유가 없으니 Key 클래스의 인스턴스 메소드로 바꿀 수 있다. 그러면 Key는 더 이상 스레드 지역변수를 구분하기 위한 키가 아니라 그 자체가 스레드 지역변수가 되기 때문에 결국 지금의 ThreadLocal은 별달리 하는 일이 없어져 중첩 클래스 Key의 이름을 ThreadLocal로 변경하면 된다.

 

뿐만아니라, get으로 얻은 Object를 실제 타입으로 형변환해야해서, 타입안전하게 만들기 어렵다.

하지만 ThreadLocal을 매개변수화 타입으로 선언하면 간단히 해결되며, 최종적으로 아래 코드와 같이 만들 수 있다.

 

public class ThreadLocal<T> {
    public ThreadLocal();
    public void set(T value);
    public T get();
}

 

이렇게 자바 2부터 제공하는 java.lang.ThreadLocal과 흡사해지고, 문자열 기반 API 문제 해결과 키 기반 API보다 나은 성능을 가진다.

실제 사용되는 ThreadLocal의 구조를 간단하게 살펴보자.

 

 

현재 이해하는데 필요한 부분이라고 생각되는 내용을 가져왔다.

내부적으로 get 혹은 set을 할 때, 현재 Thread를 먼저 불러왔고, Thread는 ThreadLocalMap을 필드로 갖는다. 따라서 getMap을 통해서 현재 ThreadLocalMap을 가져오고, 값의 여부에 따라 로직이 결정되고, 값이 있다면 타입으로 형변환을 거쳐서 반환함을 알 수 있다.

 

 

[Effective-Java] Item 63.문자열 연결은 느리니 주의하라

한 줄짜리 출력값 혹은 작고 크기가 고정된 객체의 문자열 표현을 만들때면 괜찮지만 본격적으로 사용하기 시작하면 성능 저하가 발생한다.

문자열 연결 연산자로 문자열 n개를 잇는 시간은 n^2에 비례한다. 자세한 내용은 제일 상단에 첨부한 레퍼런스를 확인하기!

 

문자열은 불변이기에 두 문자열을 연결할 경우 양쪽 내용을 모두 복사해야해 성능 저하는 피할 수 없다.

성능 저하를 줄이려면 String으로 연결하는 대신 StringBuilder를 사용하자. 만약 동기화를 제공해야 한다면 StringBuffer를 사용하자.

그럼 성능을 비교해보자.

 

@Test
public void stringConcat() {
	String s = "";
	for(int i=0;i<100_000;i++){
		s+= i+"hello";
	}
}

@Test
public void stringBuilder() {
	StringBuilder sb = new StringBuilder();
	for(int i=0;i<100_000;i++){
		sb.append(i).append("hello");
	}
}

 

위 테스트의 결과는 내 노트북에서 761배나 빨랐다 ,.,.,.! 

 

 

성능에 신경써야 한다면 많은 문자열을 연결할 때 문자열 연결 연산을 피하고 StringBuilder나 StringBuffer를 이용하자.

혹은 문자 배열을 사용하거나, 문자열을 연결하지 않고 하나씩 처리하는 방법도 있다.

 

 

 

* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.