KakaoEnt-Study / Effective-Java-Study

카카오 엔터테인먼트 백엔드 개발자의 생존 전략
1 stars 0 forks source link

[item 36] 비트 필드 대신 EnumSet을 사용하라 #34

Open sean-k1 opened 2 years ago

sean-k1 commented 2 years ago

열거한 값들이 단독이 아닌 집합으로 사용될 경우, 예전에는 각 상수에 서로 다른 2의 거듭제곱 값을 할당한 정수 열거 패턴을 사용했다.

비트 필드 열거 상수 코드

public class Text {
    public static final int STYLE_BOLD = 1 << 0; // 1
    public static final int STYLE_ITALIC = 1 << 1; // 2
    public static final int STYLE_UNDERLINE = 1 << 2; // 4
    public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8

    // 매개변수 styles는 0개 이상의 STYLE_ 상수를 비트별 OR한 값이다.
    public void applyStyles(int styles) {
        // ...
    }
}

비트필드 코드 예제

public class EnumsetExample {
    public static void main(String[] args) {
        Text text = new Text();
        text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
    }
}

위와 같이 비트 별 OR을 사용해 여러 상수를 하나의 집합으로 모을 수 있다. 이렇게 만들어진 집합을 비트 필드(bit field)라 한다.

장점

단점

대안방법

EnumSet - 비트 필드를 대체하는 현대적 기법

Enumset 변경코드

public class Text {
    public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}

    // 어떤 Set을 넘겨도 되나, EnumSet이 가장 좋다.
    public void applyStyle(Set<Style> styles) {
        // ...
    }
}

비트 필드보다 더 나은 대안으로 EnumSet 클래스가 있다. java.util 패키지의 EnumSet 클래스는 열거 타입 상수의 값으로 구성된 집합을 효과적으로 표현해준다. Set 인터페이스를 완벽히 구현하며, 타입 안전하고, 다른 어떤 Set 구현체와도 함께 사용할 수 있다. 또한 EnumSet의 내부는 비트 벡터로 구현되었다. 원소가 총 64개 이하라면, 즉 대부분의 경우에 EnumSet 전체를 long 변수 하나로 표현하여 비트 필드에 비견되는 성능을 보여준다. -> API 작성시 long,int 구분할 필요없음

removeAllretainAll 같은 대량 작업은 (비트 필드를 사용할 때 쓰는 것과 같은) 비트를 효율적으로 처리할 수 있는 산술 연산을 써서 구현했다.

난해한 작업들은 EnumSet이 다 처리해주기 때문에 비트를 직접 다룰 때 겪는 흔한 오류들로부터 해방된다.

Enumset 적용코드

public class EnumsetExample {
    public static void main(String[] args) {
 Text text = new Text();
        text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
    }
}

applyStyles 메서드가 EnumSet<Style>이 아닌 Set<Style>을 받은 것은 다형성 때문이다. 모든 클라이언트가 EnumSet을 건네리라 짐작되는 상황이라도 이왕이면 인터페이스로 받는 게 일반적으로 좋은 습관이다.(ITEM 64)

이렇게 하면 좀 특이한 클라이언트가 다른 Set 구현체를 넘기더라도 처리할 수 있다.

결론


Enumset 정리

팩토리 메서드 생성자 코드

enumset 활용 코드


public class EnumSetTest {

    enum Member{
        Delta,Rizzle,Sean,Maeve,Giro,Acker,Kante,Tony
    }

    public static void main(String[] args) {
        // 1. allOf() : 열거형 Member 모든 요소를 반환
        EnumSet es = EnumSet.allOf(Member.class);
        EnumSet es2 = EnumSet.copyOf(es); // es를 복사
        System.out.println("EnumSet Day: "+ es2);

        // 2. noneOf() : Member 비우기
        es2 = EnumSet.noneOf(Member.class);
        System.out.println("Member를 비우기 => "+ es2);

        // 3. of() : 지정한 요소를 반환
        es = EnumSet.of(Member.Rizzle, Member.Sean);
        System.out.println("리즐과 션만 => "+ es);

        // 4. comeplementOf() : 지정된 요소를 제외한 나머지를 반환
        es2 = EnumSet.complementOf(es);
        System.out.println("리즐 션 제외 모두 => "+ es2);

        // 5. range() : 지정된 구간 내의 요소만 반환
        es2 = EnumSet.range(Member.Rizzle, Member.Kante);
        System.out.println("리즐부터 캉테까지 => "+ es2);

        //6. 불변 Enumset 만들기 add 불가
        Set<Member> unmodifiableSetMembers = Collections.unmodifiableSet(EnumSet.of(Member.Rizzle, Member.Sean));
        System.out.println("unmodifiableSetMembers = " + unmodifiableSetMembers);
        unmodifiableSetMembers.add(Member.Maeve);

    }
}

saintbeller96 commented 2 years ago

션이 책 내용은 너무 잘 정리해주셔서, 비트 필드와 EnumSet이 실제로 사용되고 있는 예시를 가져왔습니다.

비트 필드 사용

Java Stream API의 Spliterator는 스트림에서 반복 및 분할 동작을 제어하는 역할을 수행한다.

Spliterator에서는 특성(characteristics)을 정의할 때 비트 필드를 사용했다.

public interface Spliterator<T> {
    ...
    public static final int ORDERED    = 0x00000010; //16, 순서를 보장한다.
    public static final int DISTINCT   = 0x00000001; //1, 스트림 내부의 원소는 유일하다.
    public static final int SORTED     = 0x00000004; //4, 스트림이 정렬되어 있다.
    public static final int SIZED      = 0x00000040; //64, 스트림의 크기를 알고 있다.
    public static final int NONNULL    = 0x00000100; //256, 스트림 내부 원소는 non-null이다.
    public static final int IMMUTABLE  = 0x00000400; //1024, 원소들은 불변한다.
    public static final int CONCURRENT = 0x00001000; //4096, 원소들은 변경될 수 있다.
    public static final int SUBSIZED   = 0x00004000; //16384, 분할된 스트림의 크기를 알고 있다.
    ...
}

대표적인 사용 예시로, 리스트에서 생성한 Spliterator의 특성을 제공할 때 비트 연산을 사용한다.

public abstract class AbstractList<> {
    ...
    static final class RandomAccessSpliterator<E> implements Spliterator<E> {
        ...
        public int characteristics() {
        //리스트 타입의 스트림은 순서를 보장하고, 스트림의 크기를 알고 있는 특징을 가진다.
                return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
        }
    }
}

EnumSet 사용

하지만 스트림의 종단 연산 결과를 수집하는 역할을 수행하는 Collector에서는 EnumSet을 사용해 특성을 정의한다.

public interface Collector<T, A, R> {
    ...
    enum Characteristics {
    //병렬 또는 동시 처리를 지원함
        CONCURRENT,
    //스트림의 결과가 순서를 보장하지 않음
        UNORDERED,
    // Indicates that the finisher function is the identity function and can be elided.
    // If set, it must be the case that an unchecked cast from A to R will succeed.
        IDENTITY_FINISH 
    }
}

Collector의 동반 클래스인 Collectors에서 이 EnumSet을 사용해 Collector의 특성을 표현한다.

이때, 불변성을 보장하기 위해 unmodifiableSet을 사용했다.

public final class Collectors {

    static final Set<Collector.Characteristics> CH_CONCURRENT_ID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.CONCURRENT,
                                                     Collector.Characteristics.UNORDERED,
                                                     Collector.Characteristics.IDENTITY_FINISH));
    static final Set<Collector.Characteristics> CH_CONCURRENT_NOID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.CONCURRENT,
                                                     Collector.Characteristics.UNORDERED));
    static final Set<Collector.Characteristics> CH_ID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
    static final Set<Collector.Characteristics> CH_UNORDERED_ID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED,
                                                     Collector.Characteristics.IDENTITY_FINISH));
    static final Set<Collector.Characteristics> CH_NOID = Collections.emptySet();
    static final Set<Collector.Characteristics> CH_UNORDERED_NOID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED));
    ...
}

왜 서로 다를까?

SpliteratorCollector 모두 Java 8에서 도입된 Stream API를 지원하기 위해 제공된다.

하지만 왜 Spliterator은 비트 필드를 사용하고 Collector에서는 EnumSet을 사용할까?

스택 오버플로우를 뒤져보다가 관련된 질문을 찾을 수 있었다.

놀랍게도 이 질문의 답변 중 하나에서 이펙티브 자바 저자인 조슈아 블로크가 포함된 openjdk 개발자들이 주고 받은 메일 목록을 찾았다.

(참고로 Java5에서 EnumSet을 설계한 사람이 바로 조슈아 블로크다)

여기서 Spliterator에서 EnumSet이 아닌 비트 필드를 사용한 이유를 어느정도 알 수 있었다(완전히 이해하기엔 내용이 너무 어려웠다😢).

Spliterator에서 비트 필드를 사용한 이유

결론부터 말하면 Spliterator에서 불변성과 성능 모두를 고려해야 했기 때문에 비트 필드를 사용했다.

일단 EnumSet을 사용하면 불변성을 보장할 수 없다.

대부분의 Spliterator는 매번 동일한 특성을 반환하지만(위 AbstractList 참고) EnumSet을 사용하면 정적이면서 불변하는 특성(상수)로 만들 수 없다.

class MySpliterator { ...
   static final EnumSet<Characteristics>  cs = EnumSet.of(...);
   EnumSet<Characteristics>  characteristics() return cs; }
}//EnumSet이 불변성을 보장하지 못함

따라서 spliterator를 생성할 때마다 해당 spliterator의 특성을 표현하는 EnumSet을 새로 생성해야 하고 이는 오버헤드로 이어진다.

스트림은 사용할 때마다 새롭게 생성해야 하기 때문에, 반복문의 성능에 가깝게 스트림을 구현하기 위해선 이런 오버헤드도 고려해야 한다.

따라서 int로 표현한 비트 필드가 요즘 시대에 어울리지 않는 것은 사실이지만, 더 나은 대안이 없기 때문에 EnumSet 대신 비트 필드를 사용했다고 한다.

그럼 Collector도 스트림과 함께 사용되지만 왜 비트 필드가 아닌 EnumSet을 사용했을까?

Collector 의 특성은 총 3가지다. 이 특성을 사용해 표현할 수 있는 조합의 최대 개수는 많아 봤자 8개밖에 안된다.

그래서 Collector의 동반 클래스인 Collectors에서는 Collector가 가질 수 있는 모든 특성 조합을 미리 생성해 제공해준다.

public final class Collectors {
    //불변성을 보장하기 위해 unmodifiableSet을 사용했다.
    static final Set<Collector.Characteristics> CH_CONCURRENT_ID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.CONCURRENT,
                                                     Collector.Characteristics.UNORDERED,
                                                     Collector.Characteristics.IDENTITY_FINISH));
    static final Set<Collector.Characteristics> CH_CONCURRENT_NOID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.CONCURRENT,
                                                     Collector.Characteristics.UNORDERED));
    static final Set<Collector.Characteristics> CH_ID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
    static final Set<Collector.Characteristics> CH_UNORDERED_ID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED,
                                                     Collector.Characteristics.IDENTITY_FINISH));
    static final Set<Collector.Characteristics> CH_NOID = Collections.emptySet();
    static final Set<Collector.Characteristics> CH_UNORDERED_NOID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED));
    ...
}

Collector의 구현체는 미리 정의된 특성 조합을 그대로 가져다 사용할 수 있다.

하지만 Splitertor이 가질 수 있는 특성 조합은 256가지이다. 따라서, Collector처럼 모든 특성 조합을 미리 생성해 놓을 수 없다.