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

자바 컬렉션 프레임워크 완전 정복: ArrayList vs LinkedList 실전 비교

zenjoydev 2025. 4. 15. 18:33

안녕하세요! 오늘은 자바 컬렉션 프레임워크의 핵심인 List 인터페이스와 이를 구현한 ArrayList, LinkedList의 특징과 성능 차이를 자세히 알아보겠습니다. 실제 성능 테스트 결과를 바탕으로 각 자료구조의 장단점을 분석하고, 실무에서 어떻게 활용할지 살펴보겠습니다.

컬렉션 프레임워크란?

자바의 컬렉션 프레임워크는 java.util 패키지에 포함된 자료구조 라이브러리로, 데이터를 효율적으로 저장하고 관리하기 위한 클래스와 인터페이스의 집합입니다. 그 중에서도 List 인터페이스는 순서가 있는 데이터 집합을 나타내며, 인덱스를 통한 접근이 가능합니다.

List 인터페이스

List는 순서가 있는 객체 집합을 다루는 인터페이스로, 다음과 같은 주요 기능을 제공합니다:

  • 인덱스를 통한 요소 접근
  • 요소 추가 및 삭제
  • 컬렉션 내 요소 검색
  • 중복 요소 허용

ArrayList의 특징

ArrayList는 List 인터페이스를 구현한 클래스로, 내부적으로 배열을 사용합니다:

  1. 동적 배열 구조: 내부적으로 배열을 사용하지만, 필요에 따라 크기가 자동으로 조정됩니다.
  2. 인덱스 기반 접근: 배열의 특성상 인덱스를 통한 직접 접근이 가능하여 조회 성능이 뛰어납니다.
  3. 메모리 효율성: 연속된 메모리 공간을 사용하여 캐시 효율성이 높습니다.
  4. 데이터 추가/삭제: 데이터 추가 및 삭제 시 요소들을 이동시켜야 하므로 비용이 발생합니다.
  5.  
// ArrayList 사용 예시
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("A");
arrayList.add("B");
arrayList.add("C");
System.out.println(arrayList.get(1)); // B 출력

LinkedList의 특징

LinkedList는 노드들이 서로 연결된 구조로 되어있는 자료구조입니다:

  1. 이중 연결 리스트 구조: 각 노드는 자신의 데이터와 이전/다음 노드의 참조를 가집니다.
  2. 요소 추가/삭제: 노드 간의 참조만 변경하면 되므로, 특히 첫 번째와 마지막 위치에서의 추가/삭제가 효율적입니다.
  3. 메모리 사용: 각 노드마다 추가적인 참조 데이터를 저장해야 하므로 메모리 사용량이 더 많습니다.
  4. 접근 성능: 특정 인덱스에 접근하기 위해서는 처음부터 순차적으로 탐색해야 하므로 조회 성능이 상대적으로 낮습니다.
// 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가 적합한 경우

  1. 조회가 빈번할 때: 인덱스를 통한 접근이 주로 필요한 경우
  2. 요소 추가/삭제가 주로 끝에서 이루어질 때
  3. 메모리 사용량이 중요할 때
  4. 대용량 데이터를 다룰 때: 캐시 효율성 때문에 성능이 더 좋을 수 있음

LinkedList가 적합한 경우

  1. 앞이나 끝에서 요소 추가/삭제가 매우 빈번할 때
  2. 스택이나 큐와 같은 자료구조 구현 시: 양방향 조회 기능이 필요할 때
  3. 데이터 크기가 자주 변하지만 크기가 그리 크지 않을 때

실무에서의 활용

실무에서는 대부분 ArrayList를 선호하는 경향이 있습니다. 그 이유는 다음과 같습니다:

  1. 일반적인 성능 우위: 대부분의 사용 사례에서 ArrayList가 더 나은 성능을 보입니다.
  2. 메모리 효율성: LinkedList는 각 노드마다 추가 참조를 저장해야 하므로 메모리 사용량이 더 많습니다.
  3. 캐시 지역성: ArrayList는 연속된 메모리 공간을 사용하므로 CPU 캐시를 더 효율적으로 활용할 수 있습니다.
  4. 단순성: 구현 및 사용 방식이 더 단순하고 직관적입니다.

다만, 요소의 삽입/삭제가 매우 빈번하게 일어나는 특수한 경우에는 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와 컬렉션의 통합