티스토리 뷰

반응형

ordinal 메서드 대신 인스턴스 필드를 사용하라

대부분의 열거 타입 상수는 자연스럽게 하나의 정숫값에 대응한다. 그리고 ordinal은 몇 번째 위치인지 반환해주는 메서드이다. 하지만 이 메서드는 치명적인 단점이 존재한다.

public enum BadOrdinal {
    
    SOLO, DUET, TRIO, QUARTET, QUINTET, SEXTET,
    SEPTET, OCTET, NONET, DECTET;
    
    public int numberOfMusician() { return ordinal() + 1; }
}

동작은 하지만 유지보수에 정말 좋지 않은 코드이다. 상수 선언 순서를 바꾸는 순간 오동작하며 복합 4중주도 똑같은 8명이지만 이미 8중주 상수가 존재하기 때문에 추가할 수 없다. 

 

해결책은 열거 타입 상수에 연결된 값은 ordinal 메서드로 얻지말고 인스턴스 필드에 저장하면 간편해진다.

public enum GoodInstance {

    SOLO(1), DUET(2), TRIO(3), QUARTET(4), 
    QUINTET(5), SEXTET(6), SEPTET(7),
    OCTET(8), NONET(9), DECTET(10);
    
    private final int numberOfMusicians;

    GoodInstance(int numberOfMusicians) {
        this.numberOfMusicians = numberOfMusicians;
    }
    
    public int numberOfMusicians() { return numberOfMusicians; }
}

Enum의 API 문서를 보면 ordinal에 대해 이렇게 쓰여 있다. "대부분 프로그래머는 이 메서드를 쓸 일이 없다."

이 메서드는 EnumSet과 EnumMap 같이 열거 타입 기반의 범용 자료구조에 쓸 목적으로 설계 되었다. 이 목적이 아니면 쓰지 말자.

 

비트 필드 대신 EnumSet을 사용하자

열거한 값이 주로 집합으로 사용될 경우 비트 필드 열거 패턴을 사용했다. (서로 다른 2의 거듭제곱 값을 할당한 정수 열거 패턴)

public class Text {

    public static final int STYLE_BOLD =          1 << 0;
    public static final int STYLE_ITALIC =        1 << 1;
    public static final int STYLE_UNDERLINE =     1 << 2;
    public static final int STYLE_STRIKETHROUGH = 1 << 3;

    public void applyStyles(int styles) { ... }
}

다음과 같은 식으로 필드를 사용하면 비트별 OR를 사용하여 여러 상수를 하나의 집합으로 모을 수 있고, 이렇게 만들어진 집합을 비트 필드라 한다.

 

하지만 비트 필드를 사용하면 여러 문제가 존재한다. 만약 비트 값이 그대로 출력되면 해석하기가 어렵고 몇 비트가 필요한지 미리 예측하여 int 나 long을 선택해야한다.

 

이러한 문제를 해결하기 위해 보통 EnumSet을 사용하는데 이 경우 내부는 비트 벡터로 구현되어있고 원소가 총 64개 이하라면 EnumSet 전체를 long 변수 하나로 표현하여 비트 필드에 비견되는 성능을 보여준다. removeAll과 retainAll ㄱㅌ은 대량 작업은 비트를 효율적으로 처리할 수 있는 산술 연산을 써서 구현했다.

 

그러면서도 이러한 작업을 EnumSet이 대신 해주면서 비트를 다룰 때 흔히 겪는 오류로 부터 해방된다.

public class TextWithEnumSet {
    
    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
    
    public void applyStyles(Set<Style> styles) {
        
    }
    
    // text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
}

applyStyles 메서드가 EnumSet이 아닌 Set을 받은 이유는 모든 클라이언트가 EnumSet을 건네리라 짐작이 되어도 이왕이면 인터페이스로 받는 것이 좋은 습관이다. 다른 Set 구현체를 넘기더라도 처리할 수 있으니 더욱 안전하다.

 

ordinal 인덱싱 대신 EnumMap

ordinal 메서드로 얻는 값을 배열이나 맵의 인덱스로 사용할 수 있지만 아까도 언급했듯이 그리 좋은 선택이 아니다. 대신 EnumMap이라는 것이 존재하는데 내부적으로 배열을 사용하기에 컬렉션의 타입 안정성과 배열의 속도를 모두 갖는다. 즉 컬렉션의 장점과 배열의 장점을 모두 갖고 싶으면 EnumMap을 고려하자

 

코드를 보자

public class plantWithOrdinal {
    
    enum LifeCycle { ANNUL, PERENNIAL, BIENNIAL }
    
    final String name;
    final LifeCycle lifeCycle;

    public plantWithOrdinal(String name, LifeCycle lifeCycle) {
        this.name = name;
        this.lifeCycle = lifeCycle;
    }

    @Override
    public String toString() {
        return name;
    }
}

이제 정원에 심은 식물들을 배열 하나로 관리하고 이들을 생애주기 별로 묶어 총 3개의 집합으로 만들어 정원을 한바퀴 돌며 각 식물을 해당 집합에 넣어보자 이 때 ordinal을 사용하면

public class PlantApp {

    public static void main(String[] args) {
        @SuppressWarnings("unchecked")
        Set<Plant>[] plantLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
        Plant[] garden = {new Plant("tree", Plant.LifeCycle.ANNUL),
                new Plant("flower", Plant.LifeCycle.BIENNIAL), new Plant("grass", Plant.LifeCycle.PERENNIAL)};

        for (int i = 0; i < plantLifeCycle.length; i++) {
            plantLifeCycle[i] = new HashSet<>();
        }

        for (Plant p : garden) {
            plantLifeCycle[p.lifeCycle.ordinal()].add(p);
        }

        for (int i = 0; i < plantLifeCycle.length; i++) {
            System.out.printf("%s : %s%n", Plant.LifeCycle.values()[i], plantLifeCycle[i]);
        }
    }
}

동작은 하겠지만 문제가 한가득인 코드다. 배열은 제네릭과 호환되지 않아 형변환을 해야하고 비검사성 경고가 나올 것이다. 배열은 각 인덱스의 의미를 몰라 출력 결과에 직접 레이블을 달아야 한다.

정확한 정수 값을 사용한다는 것을 개발자가 직접 보증해야한다.

 

-> 정수는 열거 타입과 다르게 타입 안전하지 않기 때문이다.

 

그렇다면 EnumMap을 사용하면 이를 해결할 수 있을까?

ublic class PlantAppWithEnumMap {

    public static void main(String[] args) {
        Map<Plant.LifeCycle, Set<Plant>> plantLifeCycle = new EnumMap<>(Plant.LifeCycle.class);

        Plant[] garden = {new Plant("tree", Plant.LifeCycle.ANNUL),
                new Plant("flower", Plant.LifeCycle.BIENNIAL), new Plant("grass", Plant.LifeCycle.PERENNIAL)};

        for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
            plantLifeCycle.put(lc, new HashSet<>());
        }

        for (Plant p : garden) {
            plantLifeCycle.get(p.lifeCycle).add(p);
        }

        System.out.println(plantLifeCycle);
    }
}

훨씬 코드가 간결하고 성능도 비슷하다. 안전하지 않은 형변환이 사라졌고 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공하여 직접 레이블을 달 일도 없다 EnumMap은 내부의 구현 방식을 안으로 숨겨서 배열을 사용하여 성능이 비슷하다. 

 

여기서 EnumMap의 생성자가 받는 키 타입의 class 객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공한다. 만약 자바 8 이상이라면 스트림을 사용해 코드를 더 리펙토링 할 수 있다.

public class PlantAppWithEnumMap {

    public static void main(String[] args) {
        Plant[] garden = {new Plant("tree", Plant.LifeCycle.ANNUL),
                new Plant("flower", Plant.LifeCycle.BIENNIAL),
                new Plant("grass", Plant.LifeCycle.PERENNIAL)};

        System.out.println(Arrays.stream(garden)
                .collect(groupingBy(p -> p.lifeCycle,
                        () -> new EnumMap<>(Plant.LifeCycle.class), toSet())));
    }
}

훨씬 간결해졌다. 하지만 주의할 점은 이 코드는 EnumMap만 사용했을 경우와는 다르게 동작한다. 스트림을 이용한 방식은 내용이 없는 카테코리에 대해서는 값을 만들지 않는다. 

 

즉 열거타입 LifeCycle에 3개의 상수가 있다고 할 때 만일 이 중 2개에 해당하는 식물들만 있다면 결과로 나온 EnumMap에는 두 가지 그룹에 대해서만 만든다.

 

자 우린 EnumMap과 Set을 사용하는 경우 ordinal() 메서드에 위험성에 대해 알아보았다. 유용한 열거 타입을 잘 알고 써야 문제가 없다고 필자는 생각한다. 잘 알고 사용해서 문제를 해결하는 개발자가 되자!!

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함