자바의 Collection 구조에 대하여 - List, Set, Map의 비교분석

J쭈디·2025년 2월 12일
0

1. 자바의 Collection 구조를 알아야 하는 이유

자바에서는 비슷한 역할을 하지만 조금 더 프로젝트 코드에 알맞는 컬렉션 구조를 사용하는 것이 프로젝트의 성능과 유지보수에 영향이 많이 가기 때문에, 프로젝트의 고도화를 하려면 Collection 또한 역할에 맞게 알고 사용하는 것이 중요하다.

사실 Collection 프레임워크 자체는 List나, Set, Map 모두가 여러가지 속성값을 저장할 수 이 있는 형태라는 점에서는 동일하다. 그럼 이걸 왜 구분해서 사용하는가? 우리는 그것부터 알아야 한다.

2. Collection 프레임워크 파헤치기

1. List 방식

List 방식은 아마 Collection 프레임워크 중 가장 먼저 접근하게 되는 방식이라고 생각한다. List의 특징은 다음과 같다.

1-1. List의 특징

  • 순서가 유지되며, 필요 시 정렬도 가능하다.
    • 기본 구조는 단순 선형구조로 되어있다.
    • 순서를 정하고 싶다면 Stack, Queue, Deque 컬렉션을 사용할 수 있다.
  • 중복 값을 허용한다.

1-2. 선형 List 방식 (default)

<선형 List 코드 예시>

 List<String> list = new ArrayList<>();
 list.add("a");
 list.add("b");
 list.add("c");

이러한 코드가 있다면 저장 순서는 a, b, c이고 이 순서는 유지가 된다.
구현체는 ArrayList와 LinkedList가 주로 쓰인다.

  • ArrayList
    • 동적 배열 기반으로 조회가 빠르다
    • 마지막 위치에서의 추가, 삭제는 빠르지만 중간에서 삽입/삭제를 할 경우 순서가 있기 때문에 느려진다.
  • LinkedList
    • 연결 리스트 기반으로 중간에 데이터가 많아져도 ArrayList에 비해 비교적 삽입/삭제가 빠르다.
    • 특정 위치를 조회할 때 상대적으로 느리다.
  • 값을 추가할 때는 add를 사용하고 제거할 때는 remove를 사용한다.

1-3. Stack(후입선출)을 사용한 List 방식

비유해보자면 스택은 빨래바구니나 장독대 같은 패여진 형태를 생각하면 좋다.
스택은 제일 첫번째로 들어간 요소가 제일 나중에 빠져나온다.

  • 구현체는 Stack, ArrayDeque 사용
    • Stack은 비효율적이라서 잘 안 쓰이고, Deque를 활용한 구현체인 ArrayDeque가 더 많이 쓰인다.
  • 값을 추가할 때는 push(), 값을제거할 때는 pop() 사용
  • 맨 위에 어떤 값이 있는지 확인하기 위해서는 peek() 사용

<Stack 예시 코드>

        Deque<Integer> stack = new ArrayDeque<>();

        stack.push(10);  // 삽입
        stack.push(20);
        stack.push(30);
        System.out.println(stack.pop()); // 30 (후입선출)
        System.out.println(stack.peek()); // 20 (맨 위 값 확인)

이러한 형태로 진행된다.


이런 느낌으로 제일 먼저 들어간 건 어두운 옷인데, 꺼낼 댄 제일 위의 파란 옷을 꺼내게 되는 것처럼 생각하면 편하다.
pop() 사용하게 되면 이 설명처럼 제일 위의 파란 옷을 꺼내는 것이다.

1-4. Queue(선입선출)를 사용한 List 방식

이건 줄서기랑 비슷하다고 보면 편하다.
은행이나 마트에서 먼저 줄을 선 사람이 먼저 볼일을 끝내고 나갈 수 있는 것과 동일하다.

이런 느낌이 되겠다. 사실 이미지까지 굳이 필요없으나 글자만 있으면 졸리니까 GPT를 두드려 패서 가져왔다.

  • 구현체는 LinkedList, ArrayDeque 사용
    • LinkedList는 Queue 인터페이스를 구현하여 사용하기 때문에 Deque를 활용한 구현체인 ArrayDeque가 더 빠르고 메모리상 효율적이다.
  • 값을 추가할 때는 add(), 값을제거할 때는 poll() 사용
  • 맨 위에 어떤 값이 있는지 확인하기 위해서는 peek() 사용

<Queue 예시 코드>

        Queue<String> queue = new LinkedList<>();

        queue.add("A"); // enqueue
        queue.add("B");
        queue.add("C");

        System.out.println(queue.poll()); // A (선입선출)
        System.out.println(queue.peek()); // B (맨 앞 요소 확인)

위 코드 처럼 poll()을 사용해주면 맨 앞의 요소가 빠져나가고 반환이 된다.

1-5. Deque(양방향 큐)를 사용한 List 방식

Deque는 큐와 비슷한데 대신 양방향의 삽입/삭제가 가능한 구조이다.

  • 구현체는 LinkedList, ArrayDeque 사용
    • Deque에서는 ArrayDeque가 배열 기반으로 빠르기 때문에 가장 많이 사용하는 구현체이다.
    • LinkedList는 연결리스트 기반인데 ArrayDeque보다 느리다.
  • 값을 추가할 때는 add(), 값을제거할 때는 remove()를 사용을 하는데 First나 Last 등을 붙여서 앞에서인지 뒤에서인지 정할 수 있다.
  • 맨 위에 어떤 값이 있는지 확인하기 위해서는 peek() 사용

<Deque 예시 코드>

        Deque<Integer> deque = new ArrayDeque<>();

        deque.addFirst(10); // 앞에 추가
        deque.addLast(20); // 뒤에 추가
        deque.addFirst(5);
        System.out.println(deque); // [5, 10, 20]

        System.out.println(deque.removeFirst()); // 5
        System.out.println(deque.removeLast()); // 20

위 코드 처럼 remove(First/Last)를 사용해주면 맨 앞의 요소가 빠져나가고 반환이 된다.

2. Set 방식

Set은 중복을 허용하지 않는 데이터 구조로 List와 달리 순서가 없는 게 큰 특징 중 하나이다.

2-1. Set의 특징

  • 중복 데이터를 허용하지 않음 → List와의 가장 큰 차이점
  • 순서를 유지하지 않음 (HashSet) → LinkedHashSet만 순서 유지
  • 빠른 검색 성능 (O(1) ~ O(log n)) → HashSet이 가장 빠름
  • 자동 정렬 (TreeSet) → TreeSet은 O(log n) 성능으로 자동 정렬됨

2-2. HashSet을 사용한 Set 방식

가장 일반적인 Set 구현체로 빠른 검색속도가 장점이다. 하지만 저장순서를 보장하지 않는다는 점도 유의해야 한다.

        Set<String> hashSet = new HashSet<>();
        hashSet.add("Banana");
        hashSet.add("Apple");
        hashSet.add("Apple"); // 중복된 값은 저장되지 않음

        System.out.println(hashSet); // [Apple, Banana] (순서 보장 안 됨)

이러한 형태로 사용되는데 어떤 값이 언제 나올지 사용자는 모르게 된다. 단, 중복이 없기 때문에 같은 문자가 또 나올 일이 없고, 유니크한 값을 넣기에 적절한 형태라 볼 수 있다.

2-3. LinkedHashSet을 사용한 Set 방식

Set이지만 입력 순서를 유지할 수 있게 해주는 구현체를 사용한 방식이다.

  Set<String> linkedHashSet = new LinkedHashSet<>();
        linkedHashSet.add("Banana");
        linkedHashSet.add("Apple");
        linkedHashSet.add("Cherry");

        System.out.println(linkedHashSet); // [Banana, Apple, Cherry] (입력 순서 유지)

이러한 형태로 저장되며 입력 순서가 저장 순서와 동일하게 들어간다.

근데 나는 이 형태로 쓸거면 왜 굳이 Set을 쓸지 잘 이해는 안 간다.
AI한테 이 부분 물어보니까 LinkedListSet은 중복을 허용하지 않으면서 순서를 유지해야할 때 유용하다고 한다.
ex) 사용자 방문기록을 중복 없이 순서를 보장하며 보기 등

2-4. TreeSet을 사용한 Set 방식

순서가 유지된다는 점에서느 위와 비슷한데 Treeset은 단순히 삽입된 순서를 유지하는 것이 아니라 오름차순 정렬된 상태를 유지하는 방식이다.
그리고 만약 new TreeSet<>(Comparator.reverseOrder())를 사용한다면 내림차순 정렬도 가능하다.

내부에서 이진 탐색 트리를 기반으로 하여 검색, 추가, 삭제가 용이하다.

        Set<String> treeSet = new TreeSet<>();
        treeSet.add("Banana");
        treeSet.add("Apple");
        treeSet.add("Cherry");

        System.out.println(treeSet); // [Apple, Banana, Cherry] (자동 정렬)

이런식으로 자동 정렬이 된다는 특징이 있다.

3. Map 방식

map 방식은 문자열과 숫자형을 하나씩 묶어서 저장하는 방식이라고 이해하면 편하다.


이처럼 운동선수와 등번호 같은 느낌으로 이해하면 편할 것이다. 몇 번 선수라고 해도 그 선수를 바로 찾을 수 있는 것처럼 말이다.

3-1. Map의 특징

  • Key-Value 쌍으로 저장됨
  • Key는 중복될 수 없음 → Set과 유사
  • 빠른 검색 (O(1) ~ O(log n)) → HashMap이 가장 빠름
  • 순서 유지 (LinkedHashMap), 자동 정렬 (TreeMap) 가능

3-2. HashMap을 사용한 Map 방식

Map 또한 Set과 마찬가지로 HashMap이 가장 일반적인 구현체로 검색속도가 빠르다는 특징이 있다. 그리고 숫자인 key는 중복이 안되지만 (기본은) 문자열인 Value는 중복이 가능하다.

        Map<Integer, String> hashMap = new HashMap<>();
        hashMap.put(1, "Apple");
        hashMap.put(2, "Banana");
        hashMap.put(1, "Cherry"); // 같은 Key이므로 값이 덮어써짐

        System.out.println(hashMap); // {1=Cherry, 2=Banana}
        System.out.println(hashMap.get(1)); // Cherry (Key를 이용해 값 검색)

위의 코드에서 볼 수 있듯이 Key에서 중복이 발생하면 나중에 저장된 값으로 덮어씌워진다. 그리고 get을 사용하게 되면 해당 키에 해당되는 Value 값을 반환해준다.

위에서 설명했듯이 등번호로 선수를 찾는 것과 동일한 이치다.

3-3. LinkedHashMap을 사용한 Map 방식

LinkedHashSet과 유사하게 입력 순서를 유지하는 Map 형태이다.

        Map<Integer, String> linkedHashMap = new LinkedHashMap<>();
        linkedHashMap.put(1, "Apple");
        linkedHashMap.put(2, "Banana");

        System.out.println(linkedHashMap); // {1=Apple, 2=Banana} (입력 순서 유지)

만약 여기서 입력값이 처음이 2라고 해도 2부터 출력되는 유지방식이다. 정렬과는 상관 없이 정말 들어온 순서대로 유지된다.

3-4. TreeMap을 사용한 Map 방식

TreeMap 또한 TreeSet과 유사한데, 이 때는 기본 숫자형태인 Key값이 기준이 된다.
이것도 내부적으로 이진 탐색 트리 기반으로 작동된다.
검수해달랬더니 GPT가 잔소리를 늘어놔서 어쩔 수 없이 정렬 인터페이스에 대한 내용 추가
TreeSet이고 TreeMap이고 다 Comparable인터페이스(정렬을 위한 인터페이스)를 구현해야 한다는 것이 큰 특징이라고 한다. 이 인터페이스를 사용하면 직접 정렬을 커스터마이징하는 것도 가능하다.

        Map<Integer, String> treeMap = new TreeMap<>();
        treeMap.put(2, "Banana");
        treeMap.put(1, "Apple");

        System.out.println(treeMap); // {1=Apple, 2=Banana} (Key 기준 정렬)

이러한 형태로 저장되는데 아마 여기서 애플이 2번이고 바나나가 1번이라면 1번 바나나, 2번 애플 이런식으로 저장되었을 것이다. (기준이 번호인 key이기 때문)

3. 결론

  • List: 순서 유지, 중복 가능 → 스택(Stack), 큐(Queue), 데크(Deque) 형태로 변형 가능
  • Set: 중복 제거 → HashSet, TreeSet, LinkedHashSet
  • Map: Key-Value 저장 → HashMap, LinkedHashMap, TreeMap
profile
언제 어느 위치에 있더라도 그 자리의 최선을 다 하는 사람이 되고 싶습니다.

0개의 댓글