Effective Java : 아이템 54. null이 아닌, 빈 컬렉션이나 배열을 반환하라.

    아이템 54. null이 아닌, 빈 컬렉션이나 배열을 반환하라.

    • 빈 컬렉션 대신 null을 반환하면 내부 구현 + 사용하는 쪽에서도 null 처리를 해야하는 번거로움이 발생함. 
    • 번거로움이 발생함에도 프로그램 자체에 아무런 이득이 없으므로 빈 컬렉션 / 배열을 반환하는것이 나음. 
    • 빈 컬렉션을 계속 반환해서 '객체 생성'으로 인해 '성능 문제 발생'이 명확하다면, 불변 객체를 반환하는 방법으로 개선할 수 있음. (대부분은 필요없는 방법임)

    빈 컬렉션을 Null로 특별 취급할 필요가 없다. 

    아래 코드에서 cheessInStock이라는 Collection이 비어있는 경우 null을 반환하는 코드를 볼 수 있다. 그런데 빈 컬렉션을 굳이 'null'로 특별 취급할 필요가 있을까? 빈 컬렉션을 특별 취급할 필요가 없으며, 특별 취급하게 되면 오히려 더 복잡한 코드가 된다. 

    아래에서는 삼항 연산자를 이용했는데, 빈 컬렉션을 null로 특별 취급하지 않았다면 한 줄로 끝날 코드였다. 

    private final List<Cheese> cheesesInStock = new ArrayList<>();
    
    public List<Cheese> getCheeses() {
        return cheesesInStock.isEmpty() ?
                null :
                new ArrayList<>(cheesesInStock);
    }

    문제는 내부 구현 코드에서 그치지 않는다. 빈 컬렉션을 반환하는 메서드를 사용하는 클라이언트에서도 null 처리를 해야한다는 단점이 있다. 아래에서는 cheeses != null을 이용해서 null 체크를 하게 된다. 

    public static void main(String[] args) {
    
        Shop shop = new Shop();
        List<Cheese> cheeses = shop.getCheeses();
        if (cheeses != null && cheeses.contains(Cheese.STILTON)) {
            System.out.println("it's good");
        }
    }

    빈 컬렉션을 특별취급 하지 않게 된다면 오히려 아래처럼 코드가 더 간단해지고, 단단해진다.

    public List<Cheese> getCheeses() {
        return new ArrayList<>(cheesesInStock);
    }
    
    public static void main(String[] args) {
    
        ShopEmptyNotSpecial shop = new ShopEmptyNotSpecial();
        List<Cheese> cheeses = shop.getCheeses();
        if (cheeses.contains(Cheese.STILTON)) {
            System.out.println("it's good");
        }
    }

     


    빈 컬렉션을 반환하는 것은 성능 문제가 될 수 있다? 

    빈 컬렉션은 객체다. 이런 객체가 매번 생성되는 경우라면 만의 하나로 성능 문제가 발생할 수도 있다. (하지만 대부분은 발생하지 않는다). 만약 정말로 빈 컬렉션을 반환해서 성능 문제가 발생하는 경우라면, 해결 방법으로 '빈 불변 컬렉션'을 반환할 수 있다. 이렇게 하면 객체 하나를 계속 반환하기 때문에 '객체가 계속 생성되면서 발생'하는 문제들이 해결된다. 위 코드는 아래처럼 emptyList() 메서드로 바꿀 수 있다.

    public List<Cheese> getCheesesForOptimization() {
        return cheesesInStock.isEmpty() ?
                Collections.emptyList() :
                new ArrayList<>(cheesesInStock);
    }

    emptyList() 메서드를 살펴보면 아래와 같다. 새로운 EmptyList를 불변 객체(public static final)로 하나 만든 다음에 이 객체를 계속 반환한다. 그런데 이 객체는 add, remove 같은 연산이 허용되지 않으므로 진정한 의미의 '불변' 객체다. 따라서 여러 쓰레드가 공유해서 사용해도 큰 문제가 없다. 

    public static final List EMPTY_LIST = new EmptyList<>();
    public static final <T> List<T> emptyList() {
        return (List<T>) EMPTY_LIST;
    }

     

    배열을 사용하는 경우에도 마찬가지로 처리해 볼 수 있다. 아래 코드는 다음을 의미한다.

    • toArray() 메서드는 주어진 배열 EMPTRY_CHEES_ARRAY가 충분히 크면, 그 배열에 컴포넌트를 담아서 반환함. 
    • 충분히 크지 않으면, 새로운 배열을 생성해서 컴포넌트를 담아서 반환함. 
    private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
    public Cheese[] getCheesesWithArray() {
        return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
    }

    위의 경우 EMPTY_CHESSE_ARRAY는 크기가 0인 배열이기 때문에 cheesInStock List에 컴포넌트가 있을 때라면 항상 새로운 배열이 만들어져서 반환된다. 그렇지 않은 경우라면 EMPTY_CHESSE_ARRAY 배열을 이용해서 '어떤 타입'인지 알려주는 타입 토큰처럼 '빈 불변 배열을 재사용' 할 수 있게 된다.

    댓글

    Designed by JB FACTORY