JAVA/제네릭(Generic), 컬렉션(Collection Framework)

[컬렉션 프레임워크] List

jiyoon0000 2025. 4. 4. 23:19
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
더보기
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

반응형