안녕하세요! 오늘은 자바 컬렉션 프레임워크의 핵심인 List 인터페이스와 이를 구현한 ArrayList, LinkedList의 특징과 성능 차이를 자세히 알아보겠습니다. 실제 성능 테스트 결과를 바탕으로 각 자료구조의 장단점을 분석하고, 실무에서 어떻게 활용할지 살펴보겠습니다.
컬렉션 프레임워크란?
자바의 컬렉션 프레임워크는 java.util 패키지에 포함된 자료구조 라이브러리로, 데이터를 효율적으로 저장하고 관리하기 위한 클래스와 인터페이스의 집합입니다. 그 중에서도 List 인터페이스는 순서가 있는 데이터 집합을 나타내며, 인덱스를 통한 접근이 가능합니다.
List 인터페이스
List는 순서가 있는 객체 집합을 다루는 인터페이스로, 다음과 같은 주요 기능을 제공합니다:
- 인덱스를 통한 요소 접근
- 요소 추가 및 삭제
- 컬렉션 내 요소 검색
- 중복 요소 허용
ArrayList의 특징
ArrayList는 List 인터페이스를 구현한 클래스로, 내부적으로 배열을 사용합니다:
- 동적 배열 구조: 내부적으로 배열을 사용하지만, 필요에 따라 크기가 자동으로 조정됩니다.
- 인덱스 기반 접근: 배열의 특성상 인덱스를 통한 직접 접근이 가능하여 조회 성능이 뛰어납니다.
- 메모리 효율성: 연속된 메모리 공간을 사용하여 캐시 효율성이 높습니다.
- 데이터 추가/삭제: 데이터 추가 및 삭제 시 요소들을 이동시켜야 하므로 비용이 발생합니다.
// ArrayList 사용 예시
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("A");
arrayList.add("B");
arrayList.add("C");
System.out.println(arrayList.get(1)); // B 출력
LinkedList의 특징
LinkedList는 노드들이 서로 연결된 구조로 되어있는 자료구조입니다:
- 이중 연결 리스트 구조: 각 노드는 자신의 데이터와 이전/다음 노드의 참조를 가집니다.
- 요소 추가/삭제: 노드 간의 참조만 변경하면 되므로, 특히 첫 번째와 마지막 위치에서의 추가/삭제가 효율적입니다.
- 메모리 사용: 각 노드마다 추가적인 참조 데이터를 저장해야 하므로 메모리 사용량이 더 많습니다.
- 접근 성능: 특정 인덱스에 접근하기 위해서는 처음부터 순차적으로 탐색해야 하므로 조회 성능이 상대적으로 낮습니다.
// LinkedList 사용 예시
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("A");
linkedList.add("B");
linkedList.addFirst("Z"); // 첫 번째 위치에 추가
linkedList.addLast("C"); // 마지막 위치에 추가
System.out.println(linkedList); // [Z, A, B, C] 출력
ArrayList vs LinkedList 성능 비교
실제 성능 테스트 결과를 바탕으로 두 자료구조의 성능을 비교해보겠습니다:
작업ArrayListLinkedList
인덱스 접근 (get) | 매우 빠름 (O(1)) | 느림 (O(n)) |
검색 (indexOf) | 빠름 | 느림 |
앞에 추가 (add(0, element)) | 느림 (O(n)) | 매우 빠름 (O(1)) |
중간에 추가 (add(index, element)) | 보통 | 느림 |
끝에 추가 (add(element)) | 대체로 빠름 | 빠름 |
앞에서 삭제 (remove(0)) | 느림 (O(n)) | 매우 빠름 (O(1)) |
중간에서 삭제 (remove(index)) | 보통 | 느림 |
끝에서 삭제 (remove(size-1)) | 빠름 | 빠름 |
이러한 성능 차이는 다음과 같은 코드로 테스트해볼 수 있습니다:
public class MyListPerfornanceTest {
public static void main(String[] args) {
int size = 50_000;
// ArrayList 테스트
System.out.println("== ArrayList 추가 == ");
addFirst(new ArrayList<>(), size);
// LinkedList 테스트
System.out.println("== LinkedList 추가 ==");
addFirst(new LinkedList<>(), size);
// ... 기타 테스트 생략
}
// 앞에 데이터 추가 테스트
public static void addFirst(List<Integer> list, int size) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
list.add(0, i);
}
long endTime = System.currentTimeMillis();
System.out.println("앞에 추가 - 크기: " + size + ", 시간: " + (endTime - startTime) + "ms");
}
// ... 기타 메서드 생략
}
자료구조 선택 가이드라인
테스트 결과와 이론적 배경을 바탕으로 다음과 같은 가이드라인을 제시할 수 있습니다:
ArrayList가 적합한 경우
- 조회가 빈번할 때: 인덱스를 통한 접근이 주로 필요한 경우
- 요소 추가/삭제가 주로 끝에서 이루어질 때
- 메모리 사용량이 중요할 때
- 대용량 데이터를 다룰 때: 캐시 효율성 때문에 성능이 더 좋을 수 있음
LinkedList가 적합한 경우
- 앞이나 끝에서 요소 추가/삭제가 매우 빈번할 때
- 스택이나 큐와 같은 자료구조 구현 시: 양방향 조회 기능이 필요할 때
- 데이터 크기가 자주 변하지만 크기가 그리 크지 않을 때
실무에서의 활용
실무에서는 대부분 ArrayList를 선호하는 경향이 있습니다. 그 이유는 다음과 같습니다:
- 일반적인 성능 우위: 대부분의 사용 사례에서 ArrayList가 더 나은 성능을 보입니다.
- 메모리 효율성: LinkedList는 각 노드마다 추가 참조를 저장해야 하므로 메모리 사용량이 더 많습니다.
- 캐시 지역성: ArrayList는 연속된 메모리 공간을 사용하므로 CPU 캐시를 더 효율적으로 활용할 수 있습니다.
- 단순성: 구현 및 사용 방식이 더 단순하고 직관적입니다.
다만, 요소의 삽입/삭제가 매우 빈번하게 일어나는 특수한 경우에는 LinkedList를 고려해볼 수 있습니다.
실용적인 코드 예시
다음은 ArrayList와 LinkedList를 활용한 간단한 예시입니다:
import java.util.*;
public class ListExample {
public static void main(String[] args) {
// ArrayList 예시 - 주로 조회 작업에 사용
List<String> arrayList = new ArrayList<>();
arrayList.add("Apple");
arrayList.add("Banana");
arrayList.add("Cherry");
// 인덱스로 직접 접근 - ArrayList에 적합
System.out.println("두 번째 과일: " + arrayList.get(1));
// 반복문을 통한 모든 요소 출력
System.out.println("모든 과일 출력 (ArrayList):");
for (String fruit : arrayList) {
System.out.println("- " + fruit);
}
// LinkedList 예시 - 요소 삽입/삭제가 빈번할 때 유용
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("Dog");
linkedList.add("Cat");
// 첫 번째와 마지막 위치에 추가 - LinkedList의 장점
linkedList.addFirst("Bird");
linkedList.addLast("Fish");
System.out.println("\n모든 동물 출력 (LinkedList):");
Iterator<String> iterator = linkedList.iterator();
while (iterator.hasNext()) {
System.out.println("- " + iterator.next());
}
// 큐로 사용 예시
LinkedList<String> queue = new LinkedList<>();
queue.offer("First"); // 큐에 추가
queue.offer("Second");
queue.offer("Third");
System.out.println("\n큐에서 꺼내기:");
while (!queue.isEmpty()) {
System.out.println("- " + queue.poll()); // 큐에서 제거하고 반환
}
}
}
결론
ArrayList와 LinkedList는 각각 고유한 장단점을 가지고 있으며, 상황에 따라 적절한 자료구조를 선택하는 것이 중요합니다.
일반적으로 접근(조회) 작업이 빈번하거나 대용량 데이터를 다룰 때는 ArrayList가 더 효율적이며, 첫 번째 위치에서의 삽입/삭제 작업이 매우 빈번할 때는 LinkedList를 고려해볼 수 있습니다.
하지만 실무에서는 대부분의 경우 ArrayList를 선택하는 것이 안전한 결정이며, 특수한 요구사항이 있을 때에만 LinkedList를 고려하는 것이 좋습니다.
추가 학습 제안
자바 컬렉션 프레임워크에 대한 이해를 더 깊게 하기 위해 다음 주제들을 학습해보세요:
- 다양한 컬렉션 인터페이스(Set, Map 등)
- Iterator와 Iterable 인터페이스
- 컬렉션 관련 유틸리티 클래스(Collections)
- 스트림 API와 컬렉션의 통합
'📚 학습 기록 > Java 기초 & 중급' 카테고리의 다른 글
자바 HashSet 완전 정복: 해시 알고리즘과 충돌 해결 전략 구현하기 (1) | 2025.04.21 |
---|---|
자바 컬렉션 프레임워크: Set과 해시 알고리즘 이해하기 (1) | 2025.04.16 |
ArrayList vs LinkedList: 성능 비교와 의존관계 이해하기 (1) | 2025.04.14 |
자바 LinkedList 완전 정복: 개념과 직접 구현하기 (2) | 2025.04.09 |
ArrayList 완전 정복: 동적 배열 구현과 활용 기법 (1) | 2025.04.09 |