Java Collections API에 대해 모르고 있던 5가지 사항, Part 1

Java Collections API에 대해 모르고 있던 5가지 사항, Part 1

Java Collections 사용자 정의 및 확장하기
Ted Neward, Principal, Neward & Associates

요약: Java™ Collections API는 단순히 배열을 대체하는 것에 머물지 않고 그 이상의 기능을 제공함에도 불구하고 시작하기에 적합한 주제입니다. 필자는 Collections의 활용도를 높이는 데 도움이 되는 5가지 팁을 제안하고 있으며, 이 기사에서는 5가지 팁 중에서 가장 중요한 팁인 Java Collections API를 사용자 정의 및 확장하는 방법에 대해 설명합니다.
Java Collections API는 많은 Java 개발자에게 표준 Java 배열 및 이와 관련된 모든 단점을 대체하는 데 필요한 효과적인 대안으로 인식되고 있다. Collections를 주로 ArrayList에 연결해서 사용한다고 해서 문제가 되지는 않지만 Collections의 기능은 훨씬 더 다양하다.

이 시리즈의 정보

Java 프로그래밍에 대해 알고 있다고 생각하는가? 하지만 실제로는 대부분의 개발자가 작업을 수행하기에 충분할 정도만 알고 있을 뿐 Java 플랫폼에 대해서는 자세히 알고 있지 않다. 이 시리즈에서 Ted Neward는 Java 플랫폼의 핵심 기능에 대한 자세한 설명을 통해 까다로운 프로그래밍 과제를 해결하는 데 도움이 되는 알려져 있지 않은 사실을 밝힌다.
이와 마찬가지로 Map(및 종종 사용되는 구현인 HashMap)도 이름/값 또는 키/값 쌍 작업을 수행하는 데 매우 좋은 도구이지만 그렇다고 해서 이러한 익숙한 도구만 사용할 필요는 없다. 올바른 API 또는 올바른 콜렉션을 사용하여 오류가 발생하기 쉬운 수많은 코드를 수정할 수 있다.
Java 프로그래밍과 관련된 주제를 다루는 5가지 사항 시리즈의 두 번째 기사인 이 기사는 Collections에 관한 여러 기사 중 첫 번째 기사이다. 먼저 Array를 List로 변환하는 것과 같은 일상적인 작업을 수행할 수 있는 가장 빠른 방법(그러나 가장 일반적인 방법은 아닐 수 있음)부터 살펴볼 것이다. 그런 다음 사용자 정의 Collections 클래스를 작성하고 Java Collections API를 확장하는 등의 잘 알려져 있지 않은 사항에 대해 설명할 것이다.

1. Collections 트럼프 배열
입문 수준의 Java 개발자라면 1990년대 초 C++ 개발자가 제기한 성능 비판을 잠재우기 위해 배열이 Java 언어에 포함되었다는 사실을 모를 수도 있을 것이다. 그 이후로 많은 시간이 흐른 지금, 일반적으로 배열의 성능은 Java Collections 라이브러리의 성능에 미치지 못하고 있다.
예를 들어, 배열 내용을 문자열로 저장하려면 배열을 반복하면서 내용을 하나의 String으로 연결해야 하는 반면 모든 Collections 구현에는 실용적인 toString() 구현이 있다.
아주 드문 경우를 제외하고는 대부분의 배열을 가능한 빨리 콜렉션으로 변환하는 것이 좋다. 그런 다음 질문을 하나 던져보자. 가장 쉽게 전환할 수 있는 방법은 무엇일까? 그것은 바로 Listing 1처럼 Java Collections API를 사용하는 것이다.

Listing 1. ArrayToList
import java.util.*;
public class ArrayToList
{    
    public static void main(String[] args)    {        // This gives us nothing good        System.out.println(args);                // Convert args to a List of String        List<String> argList = Arrays.asList(args);                // Print them out        System.out.println(argList);    }}

리턴된 List는 수정할 수 없으므로 새 요소를 추가하려고 하면 UnsupportedOperationException이 발생한다.
그리고 Arrays.asList()는 varargs 매개변수를 사용하여 요소를 List에 추가하므로 이 메소드를 사용하여 new 오브젝트로 구성된 List를 쉽게 작성할 수 있다.

2. 반복은 비효율적이다.
한 콜렉션(특히, 배열을 기반으로 작성된 콜렉션)의 내용을 다른 콜렉션으로 이동하거나 큰 콜렉션에서 작은 오브젝트 콜렉션을 제거하는 경우는 드물지 않게 발생한다.
이러한 경우 콜렉션을 반복하면서 각 요소가 발견될 때마다 해당 요소를 추가 또는 제거하고 싶겠지만 그렇게 하면 안 된다.
이 경우 반복에는 다음과 같은 단점이 있다.
  • 각각의 추가 또는 제거 후 콜렉션의 크기를 조정해야 하므로 비효율적이다.
  • 잠금을 획득하고, 작업을 수행한 후 잠금을 다시 해제할 때마다 심각한 동시성 문제가 발생할 수 있다.
  • 추가 또는 제거가 발생하는 동안 해당 콜렉션 작업을 수행하려는 다른 스레드에 의해 경쟁 조건이 발생한다.
addAll 또는 removeAll을 사용하여 추가 또는 제거할 요소가 포함된 콜렉션을 전달하면 이러한 모든 문제에서 벗어날 수 있다.

3. For 루프로 Iterable 반복하기
Java 5에 추가된 가장 편리한 기능 중 하나인 향상된 for 루프로 인해 Java Collections 작업과 관련된 마지막 난관이 해결되었다.
예전에는 Iterator를 가져온 후 next()를 사용하여 Iterator에 지정된 오브젝트를 가져온 다음 hasNext()를 통해 사용할 수 있는 추가 오브젝트가 있는지 확인하는 과정으로 수동으로 수행해야 했다. 하지만 Java 5부터는 이러한 모든 과정을 자동으로 처리하는 for 루프 변형을 자유롭게 사용할 수 있다.
실제로 이 향상 기능은 Collections뿐만 아니라 Iterable 인터페이스를 구현한 모든 오브젝트에 사용할 수 있다.
Listing 2에서는 Iterator로 사용할 수 있는 Person 오브젝트의 자녀 목록을 만드는 한 가지 방법을 보여 준다. 내부 List에 대한 참조를 전달하는 대신(이 경우, Person 외부의 호출자가 사용자의 가족에 자녀를 추가할 수 있으며, 이러한 상황은 대부분의 부모가 달가워하지 않을 것이다.) Person 유형이 Iterable을 구현한다. 이 방법을 사용하면 향상된 for 루프를 이용하여 자녀를 반복할 수 있다.

Listing 2. 향상된 for 루프: 자녀 표시하기
// Person.javaimport java.util.*;public class Person    implements Iterable<Person>{    public Person(String fn, String ln, int a, Person... kids)    {        this.firstName = fn; this.lastName = ln; this.age = a;        for (Person child : kids)            children.add(child);    }    public String getFirstName() { return this.firstName; }    public String getLastName() { return this.lastName; }    public int getAge() { return this.age; }        public Iterator<Person> iterator() { return children.iterator(); }        public void setFirstName(String value) { this.firstName = value; }    public void setLastName(String value) { this.lastName = value; }    public void setAge(int value) { this.age = value; }        public String toString() {         return "[Person: " +            "firstName=" + firstName + " " +            "lastName=" + lastName + " " +            "age=" + age + "]";    }        private String firstName;    private String lastName;    private int age;    private List<Person> children = new ArrayList<Person>();}// App.javapublic class App{    public static void main(String[] args)    {        Person ted = new Person("Ted", "Neward", 39,            new Person("Michael", "Neward", 16),            new Person("Matthew", "Neward", 10));        // Iterate over the kids        for (Person kid : ted)        {            System.out.println(kid.getFirstName());        }    }}

도메인 모델링에서 Iterable을 사용할 경우 몇 가지 명백한 단점이 있다. 왜냐하면 해당 오브젝트로 구성된 하나의 콜렉션만 iterator() 메소드를 통해 "암묵적으로" 지원될 수 있기 때문이다. 하지만 하위 콜렉션이 명백한 경우에는 Iterable을 사용하여 도메인 유형 관련 프로그래밍을 훨씬 더 쉽고 명백하게 수행할 수 있다.

4. 기존 및 사용자 정의 알고리즘
Collection을 반복해 보았는가? 하지만 역순으로는 어떠한가? 바로 이 점이 기존 Java Collections 알고리즘이 편리한 이유이다.
Listing 2에서는 Person의 하위 항목이 전달된 순서대로 나열된다. 하지만 이제는 역순으로 나열하려고 한다. 각 오브젝트를 새 ArrayList에 역순으로 삽입하는 또 하나의 for 루프를 작성할 수 있지만 서너 번 이후에는 코드가 지루하게 길어진다.
바로 이러한 경우에 Listing 3의 많이 사용되지 않고 있는 알고리즘을 사용할 수 있다.

Listing 3. ReverseIterator 
public class ReverseIterator{    public static void main(String[] args)    {        Person ted = new Person("Ted", "Neward", 39,            new Person("Michael", "Neward", 16),            new Person("Matthew", "Neward", 10));        // Make a copy of the List        List<Person> kids = new ArrayList<Person>(ted.getChildren());        // Reverse it        Collections.reverse(kids);        // Display it        System.out.println(kids);    }}

Collections 클래스에는 이러한 "알고리즘"이 많이 있다. 그리고 이러한 알고리즘은 Collections를 매개변수로 이용하도록 구현되어 있고 전체적으로 콜렉션에 대한 구현 독립적 동작을 제공하는 정적 메소드이다.
더 나아가 Collections 클래스의 알고리즘이 이 우수한 API 설계의 모든 것이 아니라는 점은 분명하다. 예를 들어, 필자는 내용(전달된 Collection의 내용)을 직접 수정하지 않는 메소드를 좋아한다. 그러므로 Listing 4와 같이 사용자 정의 알고리즘을 직접 작성할 수 있다.

Listing 4. 단순해진 ReverseIterator
class MyCollections{    public static <T> List<T> reverse(List<T> src)    {        List<T> results = new ArrayList<T>(src);        Collections.reverse(results);        return results;    }}


5. Collections API 확장하기
위에서 살펴본 사용자 정의된 알고리즘에서는 Java Collections API의 최종 목적지를 잘 보여 준다. 그것은 바로 항상 개발자의 특정 목적에 맞게 이 API를 확장하고 수정할 수 있다는 것이다.
예를 들어, Person 클래스의 자녀 목록을 항상 나이 순으로 정렬해야 한다고 가정하자. 아마도 Collections.sort 메소드를 사용하여 자녀를 반복해서 정렬하는 코드를 작성할 수도 있겠지만 자동으로 정렬해 주는 Collection 클래스가 있다면 더욱 좋을 것이다.
실제로 오브젝트가 Collection에 삽입되는 순서(List의 주요 원리)를 유지할 필요는 없으며 오브젝트를 정렬된 순서로 유지하기만 하면 된다.
java.util 내의 Collection 클래스 중에는 이러한 요구 사항을 충족하는 클래스가 없지만 사용자가 직접 간단하게 작성할 수 있다. 사용자가 Collection이 제공해야 하는 추상 동작을 설명하는 인터페이스를 작성하기만 하면 된다. SortedCollection의 의도는 전적으로 동작에 관한 것이다.

Listing 5. SortedCollection
public interface SortedCollection<E> extends Collection<E>{    public Comparator<E> getComparator();    public void setComparator(Comparator<E> comp);}

이 새 인터페이스의 구현을 작성하는 방법은 많이 복잡하지 않다.

Listing 6. ArraySortedCollection
import java.util.*;public class ArraySortedCollection<E>    implements SortedCollection<E>, Iterable<E>{    private Comparator<E> comparator;    private ArrayList<E> list;            public ArraySortedCollection(Comparator<E> c)    {        this.list = new ArrayList<E>();        this.comparator = c;    }    public ArraySortedCollection(Collection<? extends E> src, Comparator<E> c)    {        this.list = new ArrayList<E>(src);        this.comparator = c;        sortThis();    }    public Comparator<E> getComparator() { return comparator; }    public void setComparator(Comparator<E> cmp) { comparator = cmp; sortThis(); }        public boolean add(E e)    { boolean r = list.add(e); sortThis(); return r; }    public boolean addAll(Collection<? extends E> ec)     { boolean r = list.addAll(ec); sortThis(); return r; }    public boolean remove(Object o)    { boolean r = list.remove(o); sortThis(); return r; }    public boolean removeAll(Collection<?> c)    { boolean r = list.removeAll(c); sortThis(); return r; }    public boolean retainAll(Collection<?> ec)    { boolean r = list.retainAll(ec); sortThis(); return r; }        public void clear() { list.clear(); }    public boolean contains(Object o) { return list.contains(o); }    public boolean containsAll(Collection <?> c) { return list.containsAll(c); }    public boolean isEmpty() { return list.isEmpty(); }    public Iterator<E> iterator() { return list.iterator(); }    public int size() { return list.size(); }    public Object[] toArray() { return list.toArray(); }    public <T> T[] toArray(T[] a) { return list.toArray(a); }        public boolean equals(Object o)    {        if (o == this)            return true;                if (o instanceof ArraySortedCollection)        {            ArraySortedCollection<E> rhs = (ArraySortedCollection<E>)o;            return this.list.equals(rhs.list);        }                return false;    }    public int hashCode()    {        return list.hashCode();    }    public String toString()    {        return list.toString();    }        private void sortThis()    {        Collections.sort(list, comparator);    }}

최적화를 염두에 두지 않고 간단하게 작성해 본 이 구현에는 분명 약간의 리팩토링이 필요하다. 하지만 중요한 점은 Java Collections API의 목적은 콜렉션과 관련된 모든 기능을 제공하려는 것이 아니라는 것이다. 따라서 확장이 필요할 뿐만 아니라 확장을 권장하기도 한다.
java.util.concurrent에 도입된 것과 같은 일부 확장은 "매우 복잡"하기도 하지만 사용자 정의 알고리즘이나 기존 Collection 클래스에 대한 간단한 확장을 작성하는 것처럼 쉬울 수도 있다.
Java Collections API 확장이 어렵게 느껴질 수도 있겠지만 일단 시작하고 나면 생각처럼 어렵지는 않다는 것을 알게 될 것이다.

결론
Java Serialization과 마찬가지로 Java Collections API에도 아직까지 살펴보지 못한 기능이 많이 있다. 5가지 사항 시리즈의 다음 기사에서는 Java Collections API로 수행할 수 있는 5가지 방법을 추가로 설명한다.

출처 : http://www.ibm.com/developerworks/kr/library/j-5things2.html

댓글

가장 많이 본 글