자주 사용하는 컬렉션(Collection) 구현 및 상황별 사용법

kyw·2025년 9월 15일

Java

목록 보기
12/13

프로그래밍에서 데이터를 효율적으로 저장하고 관리하기 위해 컬렉션 프레임워크는 필수적이다. 다양한 상황에 맞춰 최적의 성능을 내기 위해서는 각 컬렉션의 특징을 이해하고 적절한 구현 클래스를 선택하는 것이 중요하다.

1. 리스트(List)

순서가 있는 데이터의 집합으로, 데이터의 중복을 허용합니다. 인덱스를 통해 특정 위치의 데이터에 빠르게 접근할 수 있다.

구현 클래스특징추천 사용 상황
ArrayList- 내부적으로 배열(Array)을 사용하여 데이터 저장
- 인덱스를 통한 데이터 조회(get)가 매우 빠름
- 데이터의 추가/삭제 시, 특히 중간에 삽입/삭제 시 성능 저하 발생 가능 (데이터 이동 필요)
- 데이터 조회가 빈번하게 일어나는 경우
- 데이터의 크기가 예측 가능하고, 중간에 데이터 추가/삭제가 적은 경우
- 스택(Stack)처럼 리스트의 마지막에서만 추가/삭제가 일어나는 경우
LinkedList- 내부적으로 노드(Node)들이 서로 연결된 형태로 데이터 저장
- 데이터의 추가/삭제(add/remove)가 매우 빠름 (연결 정보만 변경)
- 특정 인덱스의 데이터를 조회하려면 처음부터 순차적으로 탐색해야 하므로 조회 성능이 떨어짐
- 데이터의 추가/삭제가 빈번하게 발생하는 경우
- 데이터의 순차적인 접근이 주를 이루는 경우
- 큐(Queue)나 스택(Stack) 자료구조를 구현할 때 (양 끝에서의 추가/삭제가 빠르므로)
Vector- ArrayList와 동일한 구조
- 모든 메소드가 동기화(synchronized) 처리되어 있어 멀티 스레드 환경에서 안전(Thread-safe)
- 동기화 처리로 인해 단일 스레드 환경에서는 ArrayList보다 성능이 떨어짐
- 멀티 스레드 환경에서 순서가 있는 목록을 안전하게 관리해야 할 때
Stack- Vector를 상속받아 구현
- LIFO(Last-In, First-Out) 후입선출 구조
- 재귀 알고리즘, 실행 취소(Undo) 기능, 웹 브라우저의 뒤로 가기/앞으로 가기 기능 구현 시

2. 맵 (Map)

키(Key)와 값(Value)을 하나의 쌍으로 묶어 저장하는 방식. 키는 중복될 수 없으며, 값은 중복이 가능하다. 키를 통해 값을 빠르게 찾아낼 수 있다.

구현 클래스특징추천 사용 상황
HashMap- 가장 일반적으로 사용되는 Map 구현 클래스
- 해싱(Hashing)을 사용하여 키를 기반으로 값을 매우 빠르게 검색 (시간 복잡도: O(1))
- 입력 순서를 보장하지 않음
- null 키와 null 값을 허용
- 순서에 상관없이, 키를 통해 데이터를 빠르게 저장하고 조회하는 것이 중요한 대부분의 경우
LinkedHashMap- HashMap의 특징을 모두 가짐
- 데이터가 입력된 순서를 기억하고, 그 순서대로 순회(iterate) 가능
- 입력된 순서대로 데이터를 처리하거나 출력해야 할 때
- 캐시(Cache) 구현 시 LRU(Least Recently Used) 알고리즘을 적용하기 용이
TreeMap- 키(Key)를 기준으로 자동 정렬되는 Map
- 레드-블랙 트리(Red-Black Tree)로 구현되어 있음
- 정렬로 인해 HashMap보다 데이터 추가/삭제 및 검색 속도가 느릴 수 있음 (시간 복잡도: O(logN))
- 저장된 데이터를 키 순서대로 정렬해야 할 때
- 특정 범위의 데이터를 검색해야 할 때 (예: 특정 점수 이상의 학생 목록 조회)
Hashtable- HashMap과 동일한 구조
- 모든 메소드가 동기화(synchronized) 처리되어 있어 멀티 스레드 환경에서 안전
- null 키와 null 값을 허용하지 않음
- 최근에는 ConcurrentHashMap 사용이 더 권장됨
- (레거시) 멀티 스레드 환경에서 키-값 데이터를 안전하게 관리해야 할 때

3. 셋 (Set)

순서에 상관없이 데이터의 중복을 허용하지 않는 집합. 데이터의 존재 여부를 빠르게 확인하는 데 유용하다.

구현 클래스특징추천 사용 상황
HashSet- 가장 일반적으로 사용되는 Set 구현 클래스
- 해싱(Hashing)을 사용하여 데이터의 추가/삭제 및 검색 속도가 매우 빠름 (시간 복잡도: O(1))
- 저장된 순서를 보장하지 않음
- 데이터의 중복을 제거하고, 순서에 상관없이 저장/조회할 때
- 특정 데이터가 컬렉션 안에 존재하는지 빠르게 확인해야 할 때
LinkedHashSet- HashSet의 특징을 모두 가짐
- 데이터가 입력된 순서를 기억하고, 그 순서대로 순회(iterate) 가능
- 입력된 순서대로 중복 없는 데이터를 관리해야 할 때
TreeSet- 데이터를 기준으로 자동 정렬되는 Set
- 레드-블랙 트리(Red-Black Tree)로 구현되어 있음
- 정렬로 인해 HashSet보다 데이터 추가/삭제 및 검색 속도가 느릴 수 있음 (시간 복잡도: O(logN))
- 중복 없는 데이터를 항상 정렬된 상태로 유지해야 할 때
- 특정 범위의 데이터를 검색해야 할 때 (예: 특정 점수 이상의 학생 목록 조회)

4. 큐 (Queue)

데이터를 일시적으로 저장하고, 먼저 들어온 데이터가 먼저 나가는 FIFO(First-In, First-Out) 선입선출 구조를 가짐. 데이터 처리 순서가 중요할 때 유용하게 사용된다.

구현 클래스특징추천 사용 상황
LinkedList- Queue 인터페이스를 구현하고 있어 큐 자료구조로 활용 가능
- 데이터 추가/삭제가 빨라 큐의 동작 방식에 적합
- 일반적인 FIFO 방식의 큐를 구현할 때
PriorityQueue- 저장된 데이터 중에서 우선순위가 가장 높은 데이터가 먼저 출력되는 큐
- 내부적으로 힙(Heap) 자료구조를 사용
- 데이터를 넣는 순서와 상관없이, 우선순위에 따라 정렬됨
- 작업 스케줄링(우선순위가 높은 작업 먼저 처리)
- 네트워크 트래픽 관리(긴급한 데이터 먼저 전송)
- 다익스트라(Dijkstra)와 같은 그래프 최단 경로 탐색 알고리즘

자주 사용하는 컬렉션을 그림과 표로 한눈에 정리하자면 다음과 같다.

컬렉션 순회 방법

  • for each문 사용

가장 보편적이고 가독성이 좋은 표준 순회 방식이다. 내부적으로 Iterator를 사용하지만 훨씬 간결하게 표현할 수 있다.

List<Object> items = new ArrayList<>();
items.add("Hello, World!"); // String 타입
items.add(123);              // Integer 타입
items.add(new User("Java")); // User 타입

for (Object item : items) {
    // 현재 item 변수는 Object 타입이므로, toString(), equals() 등 Object의 메서드만 호출 가능
    System.out.println("타입: " + item.getClass().getSimpleName() + ", 값: " + item.toString());

    // 특정 타입의 메서드를 호출하려면 반드시 타입 확인 및 강제 형변환이 필요하다.
    if (item instanceof String s) {
        System.out.println("  -> 문자열 길이: " + s.length());
    } else if (item instanceof Integer i) {
        System.out.println("  -> 정수 값의 두 배: " + (i * 2));
    } else if (item instanceof User u) {
        System.out.println("  -> 사용자 이름: " + u.name());
    }


  • Iterator 사용(반복자)

Iterator는 자바 컬렉션 프레임워크의 표준 순회 방식이다. 모든 Collection 인터페이스의 구현체는 iterator() 메서드를 통해 Iterator 객체를 반환한다.

hasNext(): 다음에 가져올 요소가 있는지 확인한다 (boolean 반환).
next(): 다음 요소를 가져온다.
remove(): next()로 가져온 요소를 컬렉션에서 안전하게 제거한다. 반복문 실행 중 요소를 삭제할 수 있는 유일하고 안전한 방법이다.

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

Set<String> set = new HashSet<>();
set.add("Apple");
set.add("Banana");
set.add("Cherry");

Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
    String fruit = iterator.next();
    System.out.println(fruit);

    // "Banana" 요소를 안전하게 제거
    if ("Banana".equals(fruit)) {
        iterator.remove();
    }
}
System.out.println("삭제 후 Set: " + set); // [Apple, Cherry]
  • Stream API 사용

단순 순회를 넘어, 컬렉션의 데이터를 처리하는 데 중점을 둔 기능이다. 데이터를 필터링, 매핑, 정렬하는 등 다양한 중간 연산과 최종 연산을 조합하여 복잡한 작업을 간결하게 처리할 수 있다.

데이터 처리 로직을 선언형으로 작성하여 가독성이 높다.
내부 반복을 통해 병렬(parallel) 처리가 용이하다.
filter(), map(), sorted() 등 강력하고 유용한 중간 연산을 제공한다.

import java.util.ArrayList;
import java.util.List;

List<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("avocado");
list.add("cherry");

// 단순 순회
list.stream().forEach(System.out::println);

// 'a'로 시작하는 단어만 필터링하여 대문자로 변환 후 출력
list.stream()
    .filter(s -> s.startsWith("a")) // 필터링 (중간 연산)
    .map(String::toUpperCase)      // 매핑 (중간 연산)
    .forEach(System.out::println); // 최종 연산
profile
作心百日

0개의 댓글