List
* List란?
List는 자바 컬렉션 프레임워크에서 가장 많이 사용되는 자료구조 중 하나로, 데이터를 순차적으로 저장하며 중복을 허용하고 순서를 유지한다는 특징을 가짐
- 순서 보장 : 데이터를 삽입한 순서대로 저장하며, 인덱스를 통해 요소에 접근 가능
- 중복 허용 : 동일한 값을 여러 번 저장할 수 있음
- ArrayList, LinkedList 등이 있음
List 추상화
컬렉션을 다룰때, 구체적인 구현체(ArrayList, LinkedList 등)에 의존하면 유연성과 확장성이 떨어져 추상화(인터페이스)를 통해 코드를 더 유연하게 만들 수 있다.
* 자바 컬렉션 주요 인터페이스
인터페이스 | 특징 | 대표 구현체 |
List | 순서 유지, 인덱스로 접근 가능, 중복 허용 | ArrayList, LinkedList, Vector |
Set | 순서 없음, 중복 허용 안함 | HashSet, LinkedHashSet, TreeSet |
Map | 키-값(key-value) 쌍 저장, 키 중복 안됨 | HashMap, LinkedHashMap, TreeMap |
Queue | 선입선출(First-In-First-Out) 구조 | LinkedList, PriorityQueue |
Deque | 양방향 큐(Double Ended Queue), 양쪽에서 삽입 삭제 가능 | ArrayDeque, LinkedList |
- Collection 인터페이스는 List, Set, Queue의 부모 인터페이스
- Map은 Collection을 구현하지 않고 별도의 자료구조로 관리됨
- 공통적인 연산(반복, 추가, 삭제) 등은 상위 인터페이스에서 정의되고, 각 구현체에서 구체화되는 구조
- 자바 컬렉션은 인터페이스(List, Set, Map 등) 타입으로 선언하고 필요한 구현체(ArrayList, HashSet 등)는 런타임에 주입하는 것이 가장 유연하고 유지보수하기 좋은 설계
* List 인터페이스 주요 메서드
메서드 | 설명 |
add(E e) | 리스트의 끝에 지정된 요소를 추가한다. |
add(int index, E element) | 리스트의 지정된 위치에 요소를 삽입한다. |
addAll(Collection<? extends E> c) | 지정된 컬렉션의 모든 요소를 리스트의 끝에 추가한다. |
addAll(int index, Collection<?extends E> c) | 지정된 컬렉션의 모든 요소를 리스트의 지정된 위치에 추가한다. |
get(int index) | 리스트에서 지정된 위치의 요소를 반환한다. |
set(int index, E element) | 지정한 위치의 요소를 변경하고, 이전 요소를 반환한다. |
remove(int index) | 리스트에서 지정된 위치의 요소를 제거하고 그 요소를 반환한다. |
remove(Object o) | 리스트에서 지정된 첫 번째 요소를 제거한다. |
clear() | 리스트에서 모든 요소를 제거한다. |
indexOf(Object o) | 리스트에서 지정된 요소의 첫 번째 인덱스를 반환한다. |
lastIndexOf(Object o) | 리스트에서 지정된 요소의 마지막 인덱스를 반환한다. |
contains(Object o) | 리스트가 지정된 요소를 포함하고 있는지 여부를 반환한다. |
sort(Comparator<? super E> c) | 리스트의 요소를 지정된 비교자에 따라 정렬한다. |
subList(int fromIndex, int toIndex) | 리스트의 일부분의 뷰를 반환한다. |
size() | 리스트의 요소 수를 반환한다. |
isEmpty() | 리스트가 비어있는지 여부를 반환한다. |
iterator() | 리스트의 요소에 대한 반복자를 반환한다. |
toArray() | 리스트의 모든 요소를 배열로 반환한다. |
toArray(T[] a) | 리스트의 모든 요소를 지정된 배열로 반환한다. |
1. 리스트를 구체 클래스(ArrayList)로 선언한 경우
컬렉션을 다룰 때, ArrayList, LinkedList 같은 구체적인 구현체에 직접 의존하는 코드를 작성할 경우, 직관적일 수 있으나 유연성과 확장성 측면에서 단점을 가짐
public class BatchProcessor {
private final MyArrayList<Integer> list;
public BatchProcessor() {
this.list = new MyArrayList<>();
}
public void logic(int size) {
for (int i = 0; i < size; i++) {
list.add(i);
}
}
}
* 문제점
- 구체 클래스(ArrayList 등)에 직접 의존할 경우 리스트 구현체를 교체하려면 코드 수정을 직접 해야함
- 단위 테스트 시 모킹(mocking)이 어렵고, 다양한 상황에 맞춰 확장이 어려움
- 이 방식은 OOP(Object-Oriented Programming : 객체 지향 프로그래밍)의 원칙(SOLID 원칙) 중 하나인 "인터페이스에 의존하고 구현체에 의존하지 않는다"에 위반됨
+ SOLID 원칙 중 DIP에 위반
- DIP : Dependency Inversion Principle, 의존 역전 원칙
- 고수준 모듈은 저수준 모듈에 의존해서는 안되며, 둘 다 추상화에 의존
- 상위 모듈(비즈니스 로직 클래스)은 하위 모듈(구현 클래스 ArrayList, LinkedList)에 직접 의존하지 않고, 둘 다 인터페이스에 의존하도록 설계해야 한다.
- 고수준 모듈은 저수준 모듈에 의존해서는 안되며, 둘 다 추상화에 의존
2. List 인터페이스로 추상화
List를 사용할 때 ArrayList나 LinkedList와 같은 구체 클래스에 직접 의존하는 대신 인터페이스(List)를 통해 추상화하면 더 유연하고 유지보수하기 좋은 코드를 만들 수 있음
public static void main(String[] args) {
MyList<Integer> list = new MyLinkedList<>();
BatchProcessor processor = new BatchProcessor(list);
processor.logic(50_000);
}
* 추상화하면 좋은 점
- 유연한 구조 : 구현체에 종속되지 않고, 인터페이스 타입을 통해 다양한 구현체로 교체 가능
- ArrayList -> LinkedList 전환 시에도 list 선언부만 바꾸면 됨
- 확장성 향상 : 새로운 리스트 구현체가 생겨도 기존 로직 수정 없이 확장 가능
- OCP(개방-폐쇄 원칙) 일부 만족 : 기존 코드를 변경하지 않고 기능 확장이 가능
- 테스트 편의성 증가 : 테스트할 때 Mock 구현체로 대체 가능하여 테스트 유연성이 좋아짐
+ SOLID 원칙 중 OCP
- OCP : Open-Closed Principle, 개방-폐쇄 원칙
- 소프트웨어 요소는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다
- 즉, 기존 코드를 수정하지 않고 새로운 기능을 추가하거나 확장할 수 있도록 설계해야 한다
* 보완해야 할 점
- 구현체를 직접 생성하고 있음
- newArrayList<>() 처럼 구현체를 직접 new로 생성하고 있어 여전히 코드 일부가 구현체에 의존
- 의존성 주입이 아님
- 인터페이스를 사용했지만, main 메서드나 클라이언트 코드에서 구현체를 직접 생성하기 때문에 DI(Dependency Injection) 구조는 아님
- 변경 시 코드 수정 필요
- 구현체를 교체하려면 여전히 생성 코드를 수정해야하므로, 런타임에 유연하게 변경하기 어려움
3. 의존관계 주입(DI) 적용
기존에는 main() 메서드에 직접 구현체(ArrayList, LinkedList)를 생성했지만, DI(Dependency Injection)를 적용하면 외부에서 의존 객체를 주입받을 수 있음
public class BatchProcessor {
private final MyList<Integer> list;
public BatchProcessor(MyList<Integer> list) {
this.list = list; // 의존관계 주입
}
public void logic(int size) {
for (int i = 0; i < size; i++) {
list.add(i);
}
}
}
public class Main {
public static void main(String[] args) {
MyList<Integer> list = new MyArrayList<>();
// MyList<Integer> list = new MyLinkedList<>(); // 교체도 가능
BatchProcessor processor = new BatchProcessor(list); // 주입
processor.logic(50_000);
}
}
* DI 적용 장점
- 구현체 교체가 쉬움
- new ArrayList<>() -> new LinkedList<>()로 바꾸기만 하면 리스트 변경 가능
- 테스트 코드 작성이 쉬움
- OCP(개방-폐쇄 원칙) 만족
- 기존 로직은 그대로 두고, 다양한 구현체를 확장해서 사용 가능
- DIP(의존 역전 원칙) 만족
- 고수준 모듈은 저수준 구현체에 직접 의존하지 않고 인터페이스에만 의존
4. 컴파일 타임, 런타임 의존 관계
* 컴파일 타임 의존관계
- 소스 코드 작성 시점(=컴파일 시점)에 어떤 타입(클래스, 인터페이스)에 의존하고 있는지를 의미
- 컴파일러는 인터페이스만 보고 타입 체크를 수행
MyList<Integer> list = new MyArrayList<>();
- 컴파일 시점에 list는 MyList 인터페이스 타입에 의존하고 있고, 코드 작성 시점에서 list.add()같은 메서드는 MyList에 정의된 기능만 호출할 수 있음
+ 컴파일 타임이란?
- 코드를 작성하고 컴파일(빌드) 하는 시점
- 문법 오류, 타입 체크 등 코드 구조의 정합성을 검사
* 런타임 의존관계
- 실제 프로그램이 실행될 때 어떤 구현체가 객체로 들어왔는지를 의미
- 실제 동작은 MyArrayList가 수행, 코드는 MyList에만 의존
MyList<Integer> list = new MyArrayList<>();
+ 런타임이란?
- 프로그램이 실제로 실행되는 시점
- 객체가 메모리에 올라가고 메서드가 호출되고 나서 결과가 나오는 시점
- 객체가 실제 동작을 수행
* 컴파일 타임 의존관계 vs 런타임 의존관계
구분 | 정의 | 예시 | 의미 |
컴파일 타임 의존관계 | 코드 작성 시점의 타입 의존 | MyList<Integer> list | 인터페이스에 의존(유연한 구조) |
런타임 의존관계 | 실행 시 실제 객체 | new MyArrayList<>() | 구현체 주입(동작 결정) |
- 두 가지를 분리하게 되면 유지보수성과 확장성을 확보할 수 있다
- 코드는 List에만 의존하고, 실제 어떤 구현체를 쓸지는 런타임에 결정하는 것이 객체지향적인 설계
5. 전략 패턴(Strategy Pattern)
- 정의
- 알고리즘을 정의하고 각각을 캡슐화하며 이들을 서로 교환 가능하게 만드는 디자인 패턴
- 실행 시점에 알고리즘(전략)을 선택할 수 있도록 해주는 유연한 구조를 만드는 방법
- 구성 요소
- Context : 문맥
- 전략을 사용하는 클래스
- ex. BatchProcessor
- Strategy : 전략 인터페이스
- 알고리즘을 정의하는 인터페이스
- ex. MyList
- Concrete Strategy : 구현 클래스
- 실제 알고리즘을 구현한 클래스들
- ex. MyArrayList
- Context : 문맥
public interface MyList<E> {
void add(E e);
E get(int index);
}
// Concrete Strategy
public class MyArrayList<E> implements MyList<E> { ... }
public class MyLinkedList<E> implements MyList<E> { ... }
// Context
public class BatchProcessor {
private final MyList<Integer> list;
public BatchProcessor(MyList<Integer> list) {
this.list = list;
}
public void logic(int size) {
for (int i = 0; i < size; i++) {
list.add(i);
}
}
}
- 전략 패턴의 효과
- 실행 시점에 전략을 교체할 수 있음
- 다양한 구현체를 쉽게 확장 가능
- OCP(개방-폐쇄 원칙), DIP(의존 역전 원칙)를 자연스럽게 만족
리스트 성능 비교
직접 구현한 리스트, 자바 기본 리스트
* 테스트 개요
- 동일한 크기의 데이터를 각 리스트에 추가해 처리 시간 측정
- 구현체에 따라 시간 복잡도가 다르기 때문에 성능 차이 확인 가능
* 테스트 리스트
구분 | 리스트 종류 | 구조 | 특징 |
자바 기본 리스트 | ArrayList | 배열 기반 | 조회 빠름, 삽입/삭제 느림 |
LinkedList | 이중 연결 리스트 | 조회 느림, 삽입/삭제 빠름 | |
직접 구현한 리스트 | MyArrayList | 배열 기반 | 학습용, ArrayList 구조 모방 |
MyLinkedList | 단일 연결 리스트 | 학습용, LinkedList 구조 모방 |
* 리스트별 성능 비교
1. Java 기본 리스트 비교
연산 | ArrayList | LinkedList |
조회 | O(1) - 빠름 | O(n) - 느림 |
삽입 | O(n) - 느림 | O(1) - 빠름 (앞/뒤 기준) |
삭제 | O(n) - 느림 | O(1) - 빠름 (앞/뒤 기준) |
- ArrayList는 조회 중심 작업에 유리
- LinkedList는 삽입/삭제가 많은 경우 적합
2. 직접 구현한 리스트 비교
연산 | MyArrayList | MyLinkedList |
구조 | 배열 기반 | 단방향 노드 기반 |
조회 | O(1) - 빠름 | O(n) - 느림 |
삽입 | O(n) - 느림 | O(1) - 빠름 (앞/뒤 기준) |
3. 자바 리스트 vs 직접 구현한 리스트
비교 항목 | 자바 리스트(ArrayList, LinkedList) | 직접 구현한 리스트(MyArrayList, MyLinkedList) |
안정성 | 매우 높음 | 낮음(예외처리 없음, 기능 제한) |
성능 최적화 | O | 단순 구현 |
기능 다양성 | 다양함(sort, subList, stream 등) | X |
총정리
- List
- 자바 컬렉션 프레임워크의 핵심 인터페이스로 순서를 유지하며 중복 데이터를 허용하는 선형 자료구조
- ArrayList, LinkedList
- 추상화의 필요성
- 구체적인 구현체에 직접 의존하면 유연성과 확장성이 떨어지므로, List 인터페이스를 통해 추상화하면 다양한 구현체를 유연하게 교체할 수 있음
- 객체지향 원칙 적용
- DIP(의존 역전 원칙) : 구현체가 아닌 인터페이스에 의존
- OCP(개방-폐쇄 원칙) : 기능 확장에는 열려 있고, 변경에는 닫혀 있도록 설계
- 의존 관계 주입(DI)
- 리스트 구현체를 외부에서 주입함으로써 코드 수정 없이 다양한 구현체를 사용할 수 있어 테스트와 유지보수에 유리
- 컴파일 타임 vs 런타임 의존관계
- 컴파일 타임 의존관계 : 인터페이스 타입에 의존 -> 변경 유연
- 런타임 의존관계 : 실제 객체는 다양한 구현체로 대체 가능
- 전략 패턴 적용 : 실행 시점에 전략을 교체할 수 있도록 설계 -> 확장성과 테스트 편의성 향상
- 자바 리스트 성능 비교
- ArrayList : 조회 성능 우수 O(1), 삽입/삭제는 비효율적 O(n)
- LinkedList : 삽입/삭제는 빠름 O(1), 조회는 느림 O(n)
- 실무에서는 대부분 ArrayList 사용, 상황에 따라 LinkedList 고려
즉, 리스트는 상황에 따라 구현체를 선택해야 하며 인터페이스 기반 설계를 통해 유연하고 유지보수하기 쉬운 구조를 만들 수 있다.
<참고> - 김영한의 실전 자바 중급2편 강의 섹션 6. 컬렉션 프레임워크 List
'JAVA > 제네릭(Generic), 컬렉션(Collection Framework)' 카테고리의 다른 글
[컬렉션 프레임워크] HashSet (0) | 2025.04.08 |
---|---|
[컬렉션 프레임워크] Hash (0) | 2025.04.07 |
[컬렉션 프레임워크] LinkedList (0) | 2025.04.03 |
[컬렉션 프레임워크] ArrayList (0) | 2025.04.02 |
[제네릭] 제네릭 문법 정리 : 타입 제한, 메서드, 와일드카드 (0) | 2025.04.01 |