📚 학습 기록/Java 기초 & 중급

자바 컬렉션 프레임워크의 정렬과 유틸리티 클래스 완벽 가이드

zenjoydev 2025. 5. 10. 00:14

안녕하세요! 오늘은 자바의 컬렉션 프레임워크에서 중요한 부분인 정렬Collections 유틸리티에 대해 자세히 알아보겠습니다. 이 개념들은 자바 개발자라면 반드시 알아야 할 기본적인 내용이면서도, 프로젝트에서 자주 활용되는 핵심 요소입니다.

목차

  1. Comparable과 Comparator
  2. 정렬 로직의 이해
  3. Collections 유틸리티 클래스
  4. 가변 vs 불변 컬렉션
  5. 실무에서의 자료구조 선택 가이드
  6. 실전 코드 예제

1. Comparable과 Comparator

객체를 정렬하려면 해당 객체들을 비교할 수 있어야 합니다. 자바에서는 이를 위해 두 가지 인터페이스를 제공합니다.

Comparable 인터페이스

Comparable은 객체의 "자연적인 순서(Natural Ordering)"를 정의합니다. 클래스가 이 인터페이스를 구현하면 객체들을 정렬할 수 있는 기본적인 방법을 제공하게 됩니다.

public class MyUser implements Comparable<MyUser> {
    private String id;
    private int age;
    
    // 생성자 및 getter 생략

    @Override
    public int compareTo(MyUser o) {
        // 나이 기준 오름차순 정렬
        return this.age < o.age ? -1 : (this.age == o.age ? 0 : 1);
    }
}

Comparator 인터페이스

정렬 방식을 지정할 필요가 있을 때는 Arrays.sort() 또는 Collections.sort()의 인수로 Comparator를 넘기면 됩니다. 이 경우 Comparator가 정렬 우선권을 가집니다.

public class IdComparator implements Comparator<MyUser> {
    @Override
    public int compare(MyUser o1, MyUser o2) {
        return o1.getId().compareTo(o2.getId());
    }
}

// 사용 예시
Arrays.sort(users, new IdComparator());

만약 Comparable과 Comparator 둘 다 구현되지 않으면, 비교자에 대해 예외가 발생하여 비교 기능 수행이 불가능합니다.

2. 정렬 로직의 이해

정렬 로직은 현재 객체와 비교 대상 객체의 값을 비교할 때 작동 방식을 이해해야 합니다.

정렬 결과 해석

  • 현재 객체 값 < 비교 객체 값: 음수(-) 반환 → 현재 객체가 앞에 배치
  • 현재 객체 값 == 비교 객체 값: 0 반환 → 동일한 위치
  • 현재 객체 값 > 비교 객체 값: 양수(+) 반환 → 현재 객체가 뒤에 배치

이에 따라 오름차순으로 정렬됩니다. 만약 내림차순 정렬을 원한다면 결과값에 -1을 곱하거나 비교 순서를 바꾸면 됩니다.

Enum 타입 정렬

Enum 타입은 ordinal() 메서드를 통해 선언된 순서값을 반환합니다. 이를 활용하여 Enum 타입도 정렬할 수 있습니다.

아래 카드 게임 예제에서 카드의 정렬 로직을 살펴봅시다:

@Override
public int compareTo(Card o) {
    // 숫자가 다르면 숫자 기준 정렬
    if (this.cardNumber != o.getCardNumber()) {
        return this.cardNumber - o.getCardNumber();
    }
    // 숫자가 같으면 모양(suit) 기준 정렬
    return this.suit.ordinal() - o.suit.ordinal();
}

3. Collections 유틸리티 클래스

Collections 클래스는 컬렉션을 다루는 다양한 정적 메서드를 제공합니다.

주요 메서드

  • Shuffle: 랜덤으로 섞음
  • Sort: 정렬
  • Reverse: 기존 컬렉션을 반대로 정렬해서 반환
  • Of: 불변으로 컬렉션 생성!
  •  
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);

// 최대/최소값 찾기
Integer max = Collections.max(list);
Integer min = Collections.min(list);

// 리스트 섞기
Collections.shuffle(list);

// 정렬
Collections.sort(list);

// 역순 정렬
Collections.reverse(list);

4. 가변 vs 불변 컬렉션

자바에서는 가변(mutable)과 불변(immutable) 컬렉션을 모두 지원합니다.

불변 컬렉션 만들기

가변에서 불변은 Collections.unmodifiable... 메서드를 이용해 가능하며, 가변으로의 전환은 해당 타입의 객체를 새로 만들어 불변의 값을 복사하여 값을 사용할 수 있습니다.

// 불변 리스트 생성
List<Integer> immutableList = List.of(1, 2, 3);

// 가변 리스트로 변환
ArrayList<Integer> mutableList = new ArrayList<>(immutableList);
mutableList.add(4); // 가능

// 다시 불변으로 변환
List<Integer> unmodifiableList = Collections.unmodifiableList(mutableList);
// unmodifiableList.add(5); // 예외 발생

빈 리스트 생성

null 반환보다 빈 리스트 반환이 좋은 상황에 사용합니다.

// 불변 빈 리스트
List<Object> emptyList = Collections.emptyList();
List<Object> emptyList2 = List.of();

5. 실무에서의 자료구조 선택 가이드

자료구조 선택은 매우 중요합니다. 상황에 맞는 자료구조를 선택하면 성능과 가독성을 모두 얻을 수 있습니다.

자료구조 선택 가이드

  • 순서가 중요하고 중복이 허용될 때: List > ArrayList
  • 중복이 허용되지 않고 순서가 중요하지 않을 때: Set > HashSet
  • 키-값 쌍으로 데이터를 저장할 때: Map > HashMap
  • FIFO(선입선출) 구조가 필요할 때: Queue > ArrayDeque

순서가 중요하고 중복이 허용될 때는 List가 적합하고, 대량의 데이터 삽입과 추가가 빈번하다면 LinkedList가 더 적합합니다. 단, 일반적인 경우는 ArrayList가 더 좋은 선택입니다.

6. 실전 코드 예제

지금까지 배운 내용을 실제 코드에 적용해보겠습니다.

카드 게임 예제 (Comparable 구현)

public class Card implements Comparable<Card> {
    private CardSignature suit = drawRandomCardSuit();
    private int cardNumber = drawRandomCardNumber();

    // 생략...

    @Override
    public int compareTo(Card o) {
        // 카드 번호가 다르면 번호 기준 정렬
        if (this.cardNumber != o.getCardNumber()) {
            return this.cardNumber - o.getCardNumber();
        }
        // 카드 번호가 같으면 카드 모양 기준 정렬
        return this.suit.ordinal() - o.suit.ordinal();
    }

    @Override
    public String toString() {
        return cardNumber + "(" + suit.symbol + ")";
    }

    private enum CardSignature {
        SPADE("\u2660"), HEART("\u2665"), DIAMOND("\u2666"), CLOVER("\u2663");
        
        private String symbol;
        
        CardSignature(String symbol) {
            this.symbol = symbol;
        }
        
        public String getSymbol() {
            return symbol;
        }
    }
}

Collections 클래스 활용 예제

public class CollectionsSortMain {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);

        Integer max = Collections.max(list);
        Integer min = Collections.min(list);

        System.out.println("max = " + max);
        System.out.println("min = " + min);

        System.out.println("list = " + list);
        Collections.shuffle(list);
        System.out.println("Shuffle list = " + list);
        Collections.sort(list);
        System.out.println("Sort list = " + list);
        Collections.reverse(list);
        System.out.println("reverse list = " + list);
    }
}

불변 컬렉션 예제

public class ImmutableMain {
    public static void main(String[] args) {
        // 불변 리스트 생성
        List<Integer> list = List.of(1, 2, 3);

        // 가변 리스트
        ArrayList<Integer> mutableList = new ArrayList<>(list);
        mutableList.add(4);
        System.out.println("mutableList = " + mutableList);
        System.out.println("mutableList Class = " + mutableList.getClass());

        // 불변 리스트
        Collection<Integer> unmodifiableList = Collections.unmodifiableCollection(mutableList);
        System.out.println("unmodifiableList Class = " + unmodifiableList.getClass());
    }
}

Comparator 활용 예제

public class SortMain3 {
    public static void main(String[] args) {
        MyUser myUser1 = new MyUser("a", 30);
        MyUser myUser2 = new MyUser("b", 20);
        MyUser myUser3 = new MyUser("c", 10);

        MyUser[] array = {myUser1, myUser2, myUser3};
        System.out.println("기본 데이터");
        System.out.println(Arrays.toString(array));

        System.out.println("Comparable 기본 정렬");
        Arrays.sort(array); // MyUser의 compareTo 메서드 사용
        System.out.println(Arrays.toString(array));

        System.out.println("IdComparator 정렬");
        Arrays.sort(array, new IdComparator());
        System.out.println(Arrays.toString(array));

        System.out.println("IdComparator().reversed() 정렬");
        Arrays.sort(array, new IdComparator().reversed());
        System.out.println(Arrays.toString(array));
    }
}

결론

자바의 컬렉션 프레임워크는 객체 정렬을 위한 Comparable과 Comparator 인터페이스, 그리고 다양한 컬렉션 조작을 위한 Collections 유틸리티 클래스를 제공합니다. 이러한 기능들을 잘 활용하면 더 효율적이고 가독성 높은 코드를 작성할 수 있습니다.

정렬은 내가 커스텀 작성이 가능하므로, 정렬 로직을 내가 구현해야 합니다. 기본 정렬 메서드를 오버라이드 하면 Comparable의 기본 정렬을 사용하고, 별도의 정렬 방식이 필요할 때는 Comparator를 활용하면 됩니다.

여러분도 이 개념들을 활용해 보셨나요? 어떤 경우에 어떤 방식의 정렬이 더 효과적이었는지, 혹은 어떤 자료구조가 특정 상황에서 더 유용했는지 경험을 공유해주세요! 함께 배우고 성장하는 개발자 커뮤니티를 만들어가요! 😊