List, ArrayList, LinkedList — 헷갈림의 원인부터 정리하기

revo·2025년 12월 16일

자바

목록 보기
1/30


자바 컬렉션을 처음 배우면서 가장 오래 헷갈렸던 게 List, ArrayList, LinkedList의 관계였다.

코드를 보면 다들 이렇게 쓰고 있다.

List<Integer> list = new ArrayList<>();

그런데 막상 생각해보면 의문이 생긴다.
이미 new ArrayList()를 썼는데, 왜 굳이 List로 선언하는 걸까?
그리고 ArrayListLinkedList는 정확히 뭐가 다른 걸까?

이 글은 그 질문에 대해 다시 봐도 이해할 수 있도록 정리한 내용이다.


List는 자료구조가 아니라 인터페이스다

먼저 가장 중요한 사실부터 짚고 가야 한다.

List는 자료구조가 아니다.
List인터페이스(interface)다.

List<Integer> list;

이 코드는 객체를 만드는 코드가 아니다.
단지 다음과 같은 약속(계약)만 정의한다.

  • 순서가 있다
  • 중복을 허용한다
  • add, get, remove, size 같은 메서드를 제공한다

하지만 중요한 점은 이것이다.

List는 내부가 배열인지, 연결 리스트인지, 어떻게 저장되는지에 대해서는 아무것도 모른다.

즉, List는 “무엇을 할 수 있는지”만 정의한 역할(role)이다.


ArrayList와 LinkedList는 List를 구현한 실제 클래스다

반면에 ArrayList, LinkedList는 다르다.

ArrayList<Integer> list = new ArrayList<>();

이건 실제 객체 생성이다.
그리고 이 둘은 공통점이 있다.

ArrayList  implements List
LinkedList implements List

즉,

  • ArrayList도 List이고
  • LinkedList도 List다

다만 내부 구현 방식이 완전히 다르다.


ArrayList의 정체: 배열 기반 리스트

ArrayList는 내부적으로 배열을 사용한다.

특징을 정확히 정리하면 다음과 같다.

  • 인덱스 접근이 빠르다 (get(i) → O(1))
  • 중간 삽입/삭제는 느리다 (뒤 요소를 전부 밀어야 함)
  • 크기가 꽉 차면 더 큰 배열로 복사해서 확장한다
List<Integer> list = new ArrayList<>();
list.add(10);
list.get(0); // 빠름

“조회가 많고, 중간 삽입/삭제가 적을 때” 적합하다.


LinkedList의 정체: 노드 연결 기반 리스트

LinkedList는 내부적으로 노드들이 연결된 구조다.

특징은 정반대에 가깝다.

  • 중간 삽입/삭제가 빠르다 (포인터만 변경)
  • 인덱스 접근은 느리다 (get(i) → O(n))
  • 앞/뒤 삽입, 삭제에 강하다
List<Integer> list = new LinkedList<>();
list.add(10);
list.remove(0);

“앞뒤 삽입/삭제가 잦은 경우”에 적합하다.


그럼 왜 다들 List로 선언하는 걸까?

다시 이 코드로 돌아가 보자.

List<Integer> list = new ArrayList<>();

이 문장을 정확히 해석하면 이렇다.

“지금 구현은 ArrayList지만, 나는 이 객체를 List 기능만 사용하겠다.”

여기서 중요한 포인트는 두 가지다.


선언부는 ‘사용 범위’를 정한다

List<Integer> list

이 한 줄은 컴파일러에게 이렇게 말하는 것과 같다.

“이 변수는 List 인터페이스에 정의된 기능만 쓸 수 있다.”

그래서 이런 코드는 애초에 허용되지 않는다.

list.ensureCapacity(100); // 컴파일 에러

ensureCapacityArrayList 전용 메서드이기 때문이다.

이건 불편함이 아니라 의도적인 제한이다.


생성부는 ‘현재 구현’을 정한다

new ArrayList<>()

이건 단순히 말한다.

“지금은 ArrayList로 구현한다.”

즉,

  • 구현은 정했지만
  • 의존성은 제한했다

이게 핵심이다.


“나중에 바꿀 수 있어서”라는 설명의 정확한 의미

자주 듣는 설명은 이렇다.

“나중에 LinkedList로 바꿀 수도 있으니까 List로 선언한다”

이 말은 난 이해가 안된다.

그래서 아래처럼 이해했다.

“ArrayList에만 있는 기능에 의존하는 코드가 생기는 걸 처음부터 막기 위해 List로 선언한다.”

에러를 나중에 고치는 게 목적이 아니다.
에러가 날 수 있는 설계 자체를 못 하게 막는 것이 목적이다.


언제 ArrayList로 선언해도 되는가

그렇다고 항상 List로만 선언해야 하는 건 아니다.

다음 경우에는 ArrayList로 선언해도 된다.

ArrayList<Integer> list = new ArrayList<>();
  • ArrayList 전용 메서드를 반드시 써야 할 때
  • 학습용, 실험용 코드
  • 구현 변경 가능성이 전혀 없을 때

한 번에 정리하는 비교 표

구분ListArrayListLinkedList
종류인터페이스클래스클래스
역할기능 계약배열 기반 구현연결 리스트 구현
객체 생성불가가능가능
인덱스 접근정의만 함빠름느림
중간 삽입/삭제정의만 함느림빠름
선언 용도다형성구현 고정구현 고정

이 개념을 이해하고 나서 바뀐 생각

예전에는
List<Integer> list = new ArrayList<>();
이 문장이 괜히 복잡해 보였다.

지금은 이렇게 보인다.

“구현은 숨기고, 역할만 드러낸 설계”


정리

  • List는 자료구조가 아니라 인터페이스
  • ArrayList, LinkedListList의 구현체
  • 선언을 List로 하는 이유는 구현 의존 코드를 구조적으로 막기 위해서
  • 생성 시점에 구현은 정하지만, 사용 시점에서는 역할에만 의존한다

0개의 댓글