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

자바 컬렉션 프레임워크 완전정복: Map, Stack, Queue 및 Deque

zenjoydev 2025. 4. 29. 00:08

안녕하세요! 오늘은 자바 컬렉션 프레임워크의 중요한 자료구조들인 Map, Stack, Queue, 그리고 Deque에 대해 자세히 알아보겠습니다. 각 자료구조의 특징과 활용법, 그리고 실제 코드 예시를 통해 언제 어떤 자료구조를 사용해야 하는지 명확하게 이해할 수 있도록 설명드리겠습니다.

목차

  1. Map
  2. Map vs Set
  3. Entry Set
  4. Stack
  5. Queue
  6. Deque
  7. 자료구조 선택 가이드

1. Map

Map은 키(Key)와 값(Value) 형태로 데이터를 저장하는 자료구조입니다. 각 키는 유일성을 가지며, 값은 중복이 가능합니다.

주요 특징

  • 키-값 쌍: 데이터를 키-값 쌍으로 저장
  • 키 유일성: 키는 중복될 수 없음
  • 값 중복 가능: 값은 중복 허용
  • 순서 보장 없음: 일반 HashMap의 경우 순서 보장 안함 (LinkedHashMap은 예외)

주요 메서드

  • put(key, value): 키-값 쌍 추가, dart의 add와 같은 기능
  • putIfAbsent(key, value): 해당 키가 없는 경우에만 값 추가
  • get(key): 키에 해당하는 값 반환
  • containsKey(key): 키 존재 여부 확인
  • containsValue(value): 값 존재 여부 확인 (성능이 좋지 않음)
  • keySet(): 모든 키를 Set으로 반환
  • values(): 모든 값을 Collection으로 반환
  • entrySet(): 모든 키-값 쌍을 Set으로 반환

구현체

  • HashMap: 가장 많이 사용되는 Map 구현체, 해시 알고리즘 기반
  • LinkedHashMap: 삽입 순서를 유지
  • TreeMap: 키의 자연 순서대로 정렬

예시 코드

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;

public class MapExample {
    public static void main(String[] args) {
        // HashMap 예시
        Map<String, Integer> studentScores = new HashMap<>();
        
        // 데이터 추가
        studentScores.put("김철수", 85);
        studentScores.put("박영희", 92);
        studentScores.put("이민수", 78);
        
        // 이미 존재하는 키의 경우 값 덮어쓰기
        studentScores.put("김철수", 88);
        
        // 존재하지 않는 경우에만 추가
        studentScores.putIfAbsent("정지훈", 90);
        studentScores.putIfAbsent("김철수", 77); // 이미 존재하므로 무시됨
        
        System.out.println("학생 점수표: " + studentScores);
        
        // 값 조회
        System.out.println("박영희의 점수: " + studentScores.get("박영희"));
        System.out.println("홍길동의 점수: " + studentScores.get("홍길동")); // 존재하지 않으면 null 반환
        
        // 키 존재 여부 확인
        System.out.println("이민수 존재 여부: " + studentScores.containsKey("이민수"));
        
        // 모든 키 출력
        System.out.println("모든 학생 이름: " + studentScores.keySet());
        
        // 모든 값 출력
        System.out.println("모든 점수: " + studentScores.values());
        
        // for-each로 맵 순회
        System.out.println("\n== 학생별 점수 ==");
        for (Map.Entry<String, Integer> entry : studentScores.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // LinkedHashMap 예시 (삽입 순서 유지)
        Map<String, String> linkedMap = new LinkedHashMap<>();
        linkedMap.put("첫번째", "First");
        linkedMap.put("두번째", "Second");
        linkedMap.put("세번째", "Third");
        System.out.println("\nLinkedHashMap(삽입 순서 유지): " + linkedMap);
        
        // TreeMap 예시 (키 기준 정렬)
        Map<String, String> treeMap = new TreeMap<>();
        treeMap.put("Z", "Zebra");
        treeMap.put("A", "Apple");
        treeMap.put("B", "Banana");
        System.out.println("TreeMap(키 기준 정렬): " + treeMap);
    }
}

2. Map vs Set

Map과 Set은 모두 해시 테이블을 기반으로 하지만, 사용 목적과 구조에 차이가 있습니다.

주요 차이점

특성MapSet

저장 형태 키-값 쌍 단일 값
중복 키는 중복 불가, 값은 중복 가능 모든 요소 중복 불가
주요 구현체 HashMap, LinkedHashMap, TreeMap HashSet, LinkedHashSet, TreeSet
내부 구현 해시 테이블 Map 기반 (HashSet은 HashMap 사용)

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class MapVsSetExample {
    public static void main(String[] args) {
        // Map 예시
        Map<String, Integer> ageMap = new HashMap<>();
        ageMap.put("Alice", 25);
        ageMap.put("Bob", 30);
        ageMap.put("Charlie", 22);
        
        // Set 예시
        Set<String> nameSet = new HashSet<>();
        nameSet.add("Alice");
        nameSet.add("Bob");
        nameSet.add("Charlie");
        nameSet.add("Alice"); // 중복값은 무시됨
        
        System.out.println("Map 크기: " + ageMap.size()); // 3
        System.out.println("Set 크기: " + nameSet.size()); // 3 (중복이 제거됨)
        
        // Map은 키로 조회 가능
        System.out.println("Bob의 나이: " + ageMap.get("Bob"));
        
        // Set은 존재 여부만 확인 가능
        System.out.println("Bob이 Set에 있는가? " + nameSet.contains("Bob"));
    }
}

3. Entry Set

Map의 entrySet() 메서드는 Map.Entry 객체의 Set을 반환합니다. 각 Entry는 키와 값을 가지고 있는 객체입니다.

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class EntrySetExample {
    public static void main(String[] args) {
        Map<String, String> countryCapitals = new HashMap<>();
        countryCapitals.put("대한민국", "서울");
        countryCapitals.put("일본", "도쿄");
        countryCapitals.put("중국", "베이징");
        countryCapitals.put("미국", "워싱턴 D.C.");
        
        // Entry 객체를 통해 키와 값에 접근
        Set<Map.Entry<String, String>> entries = countryCapitals.entrySet();
        
        System.out.println("국가 및 수도 목록:");
        for (Map.Entry<String, String> entry : entries) {
            System.out.println(entry.getKey() + "의 수도는 " + entry.getValue());
        }
        
        // 람다식을 사용한 방법 (Java 8 이상)
        System.out.println("\n람다식 사용:");
        countryCapitals.forEach((country, capital) -> 
            System.out.println(country + "의 수도는 " + capital)
        );
    }
}

4. Stack

Stack은 한쪽 끝에서만 데이터의 삽입과 삭제가 이루어지는 LIFO(Last In First Out) 구조의 자료구조입니다.

주요 특징

  • LIFO: 가장 나중에 들어간 데이터가 가장 먼저 나옴
  • push/pop: 데이터 삽입은 push, 데이터 꺼내기는 pop으로 표현
  • Legacy: 자바의 Stack 클래스는 Vector를 상속한 레거시 클래스

참고: 자바에서는 Stack 클래스 대신 Deque 인터페이스의 구현체(ArrayDeque)를 사용하는 것이 권장됩니다.

예시 코드

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Stack;

public class StackExample {
    public static void main(String[] args) {
        // 기존 Stack 클래스 사용 (권장하지 않음)
        Stack<String> oldStack = new Stack<>();
        oldStack.push("첫번째");
        oldStack.push("두번째");
        oldStack.push("세번째");
        
        System.out.println("기존 Stack 내용: " + oldStack);
        System.out.println("pop: " + oldStack.pop()); // 세번째
        System.out.println("peek: " + oldStack.peek()); // 두번째 (꺼내지 않고 조회만)
        
        // 권장: Deque 인터페이스 사용
        Deque<String> stack = new ArrayDeque<>();
        stack.push("첫번째");
        stack.push("두번째");
        stack.push("세번째");
        
        System.out.println("\nArrayDeque을 스택으로 사용:");
        System.out.println("스택 내용: " + stack);
        System.out.println("pop: " + stack.pop()); // 세번째
        System.out.println("peek: " + stack.peek()); // 두번째
        
        // 스택 응용: 괄호 검사
        System.out.println("\n괄호 검사:");
        System.out.println("((){}[]) 유효한가? " + isBalanced("((){}[])"));
        System.out.println("({)} 유효한가? " + isBalanced("({)}"));
    }
    
    // 스택을 활용한 괄호 균형 검사 함수
    public static boolean isBalanced(String expr) {
        Deque<Character> stack = new ArrayDeque<>();
        
        for (char ch : expr.toCharArray()) {
            if (ch == '(' || ch == '{' || ch == '[') {
                stack.push(ch);
            } else if (ch == ')' || ch == '}' || ch == ']') {
                if (stack.isEmpty()) {
                    return false;
                }
                
                char top = stack.pop();
                if ((ch == ')' && top != '(') ||
                    (ch == '}' && top != '{') ||
                    (ch == ']' && top != '[')) {
                    return false;
                }
            }
        }
        
        return stack.isEmpty();
    }
}

5. Queue

Queue는 데이터를 순서대로 처리하는 FIFO(First In First Out) 구조의 자료구조입니다.

주요 특징

  • FIFO: 먼저 들어간 데이터가 먼저 나옴
  • offer/poll: 데이터 삽입은 offer, 데이터 꺼내기는 poll로 표현
  • 선입선출: 입력 순서대로 처리해야 하는 작업에 적합

예시 코드

import java.util.ArrayDeque;
import java.util.LinkedList;
import java.util.Queue;

public class QueueExample {
    public static void main(String[] args) {
        // LinkedList로 Queue 구현
        Queue<String> queue1 = new LinkedList<>();
        queue1.offer("첫번째 고객");
        queue1.offer("두번째 고객");
        queue1.offer("세번째 고객");
        
        System.out.println("LinkedList 큐: " + queue1);
        System.out.println("첫 번째 고객 응대: " + queue1.poll());
        System.out.println("다음 고객 확인: " + queue1.peek());
        System.out.println("남은 고객 수: " + queue1.size());
        
        // ArrayDeque로 Queue 구현
        Queue<String> queue2 = new ArrayDeque<>();
        queue2.offer("태스크 1");
        queue2.offer("태스크 2");
        queue2.offer("태스크 3");
        
        System.out.println("\nArrayDeque 큐: " + queue2);
        
        // 모든 태스크 처리
        System.out.println("\n태스크 처리:");
        while (!queue2.isEmpty()) {
            String task = queue2.poll();
            System.out.println(task + " 처리 중...");
        }
        
        // 프린터 대기열 시뮬레이션
        Queue<PrintJob> printQueue = new LinkedList<>();
        printQueue.offer(new PrintJob("보고서.docx", 5));
        printQueue.offer(new PrintJob("사진.jpg", 10));
        printQueue.offer(new PrintJob("발표자료.pptx", 7));
        
        System.out.println("\n프린터 대기열 처리:");
        processPrintJobs(printQueue);
    }
    
    static class PrintJob {
        String filename;
        int pages;
        
        PrintJob(String filename, int pages) {
            this.filename = filename;
            this.pages = pages;
        }
    }
    
    static void processPrintJobs(Queue<PrintJob> queue) {
        int totalTime = 0;
        
        while (!queue.isEmpty()) {
            PrintJob job = queue.poll();
            int printTime = job.pages * 1; // 1초당 1페이지 가정
            totalTime += printTime;
            
            System.out.println(job.filename + " 인쇄 중... (" + job.pages + 
                              "페이지, " + printTime + "초 소요)");
        }
        
        System.out.println("모든 인쇄 작업 완료. 총 " + totalTime + "초 소요.");
    }
}

6. Deque

Deque(Double-Ended Queue)는 양쪽 끝에서 데이터의 삽입과 삭제가 가능한 자료구조입니다. Stack과 Queue의 기능을 모두 제공합니다.

주요 특징

  • 양방향 접근: 앞/뒤 양쪽에서 요소 추가/삭제 가능
  • Stack + Queue: 스택과 큐의 기능을 모두 제공
  • 구현체: ArrayDeque, LinkedList

주요 메서드

  • 앞쪽 추가: offerFirst(), addFirst(), push()
  • 뒤쪽 추가: offerLast(), addLast(), offer(), add()
  • 앞쪽 삭제: pollFirst(), removeFirst(), pop()
  • 뒤쪽 삭제: pollLast(), removeLast()
  • 조회: peekFirst(), peekLast(), peek()

예시 코드

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedList;

public class DequeExample {
    public static void main(String[] args) {
        // ArrayDeque 사용
        Deque<String> deque = new ArrayDeque<>();
        
        // 앞에 추가
        deque.offerFirst("첫번째");
        deque.offerFirst("새로운 첫번째");
        
        // 뒤에 추가
        deque.offerLast("마지막");
        deque.offerLast("새로운 마지막");
        
        System.out.println("Deque 상태: " + deque);
        
        // 앞에서 제거
        System.out.println("앞에서 제거: " + deque.pollFirst());
        
        // 뒤에서 제거
        System.out.println("뒤에서 제거: " + deque.pollLast());
        
        System.out.println("연산 후 Deque: " + deque);
        
        // Deque를 스택으로 사용
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(1);
        stack.push(2);
        stack.push(3);
        
        System.out.println("\nDeque를 스택으로 사용:");
        System.out.println("스택: " + stack);
        System.out.println("pop: " + stack.pop());
        System.out.println("pop 후: " + stack);
        
        // Deque를 큐로 사용
        Deque<Integer> queue = new ArrayDeque<>();
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        
        System.out.println("\nDeque를 큐로 사용:");
        System.out.println("큐: " + queue);
        System.out.println("poll: " + queue.poll());
        System.out.println("poll 후: " + queue);
        
        // LinkedList vs ArrayDeque 성능 비교
        System.out.println("\n성능 비교 (10,000개 요소 추가):");
        performanceTest();
    }
    
    static void performanceTest() {
        int elements = 1_000_000;
        
        // LinkedList 테스트
        long startTime = System.currentTimeMillis();
        Deque<Integer> linkedList = new LinkedList<>();
        for (int i = 0; i < elements; i++) {
            linkedList.offerFirst(i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("LinkedList 앞에 추가: " + (endTime - startTime) + "ms");
        
        // ArrayDeque 테스트
        startTime = System.currentTimeMillis();
        Deque<Integer> arrayDeque = new ArrayDeque<>();
        for (int i = 0; i < elements; i++) {
            arrayDeque.offerFirst(i);
        }
        endTime = System.currentTimeMillis();
        System.out.println("ArrayDeque 앞에 추가: " + (endTime - startTime) + "ms");
    }
}

7. 자료구조 선택 가이드

상황에 따라 적절한 자료구조를 선택하는 것이 중요합니다. 다음은 각 상황에 맞는 자료구조 선택 가이드입니다.

Map 관련

  • 키-값 쌍 저장이 필요할 때: HashMap 사용
  • 삽입 순서 유지가 필요할 때: LinkedHashMap 사용
  • 키 기준으로 정렬이 필요할 때: TreeMap 사용

Set 관련

  • 중복 없는 데이터 저장이 필요할 때: HashSet 사용
  • 삽입 순서 유지가 필요할 때: LinkedHashSet 사용
  • 요소를 정렬된 상태로 유지해야 할 때: TreeSet 사용

Stack/Queue 관련

  • LIFO(Last In First Out) 구조가 필요할 때: Deque 구현체를 Stack으로 사용
  • FIFO(First In First Out) 구조가 필요할 때: Queue 구현체 사용
  • 양방향 삽입/삭제가 필요할 때: Deque 구현체 사용
  • 빠른 성능이 중요할 때: ArrayDeque 사용
  • 메모리 효율성이 중요할 때: LinkedList 사용

실제 사용 예시

import java.util.*;

public class DataStructureSelectionGuide {
    public static void main(String[] args) {
        // 1. 웹 사이트 사용자 세션 관리 (키-값 저장) - HashMap
        Map<String, UserSession> sessions = new HashMap<>();
        sessions.put("user123", new UserSession("user123", System.currentTimeMillis()));
        
        // 2. 최근 검색어 기록 (삽입 순서 유지) - LinkedHashMap
        Map<String, Integer> recentSearches = new LinkedHashMap<>(16, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
                return size() > 10; // 최대 10개만 유지
            }
        };
        
        // 3. 중복 제거된 로그 ID 저장 - HashSet
        Set<String> uniqueLogIds = new HashSet<>();
        
        // 4. 브라우저 방문 기록 (삽입 순서 유지) - LinkedHashSet
        Set<String> browserHistory = new LinkedHashSet<>();
        
        // 5. 작업 스케줄링 (우선순위 기반) - PriorityQueue
        PriorityQueue<Task> taskQueue = new PriorityQueue<>();
        
        // 6. 웹페이지 탐색 기록 (뒤로가기/앞으로가기) - Deque
        Deque<String> browserNavigation = new ArrayDeque<>();
    }
    
    static class UserSession {
        String userId;
        long timestamp;
        
        UserSession(String userId, long timestamp) {
            this.userId = userId;
            this.timestamp = timestamp;
        }
    }
    
    static class Task implements Comparable<Task> {
        int priority;
        String name;
        
        Task(String name, int priority) {
            this.name = name;
            this.priority = priority;
        }
        
        @Override
        public int compareTo(Task other) {
            return Integer.compare(this.priority, other.priority);
        }
    }
}

결론

지금까지 자바 컬렉션 프레임워크의 Map, Stack, Queue, Deque에 대해 알아보았습니다. 각 자료구조는 고유한 특성과 장단점을 가지고 있으며, 상황에 맞게 적절한 자료구조를 선택하는 것이 중요합니다.

Map은 키-값 쌍으로 데이터를 저장할 때 유용하며, Stack은 LIFO, Queue는 FIFO 구조를 제공합니다. Deque는 양방향 접근이 가능하여 Stack과 Queue의 기능을 모두 제공하는 유연한 자료구조입니다.

적절한 상황에 최적화된 자료구조를 사용하면 성능 최적화에 보다 유리하게 작용합니다. 따라서 각 자료구조의 특성을 잘 이해하고 요구사항에 맞게 선택하는 것이 중요합니다.


자바 컬렉션 프레임워크에 관한 질문이나 의견이 있으시면 댓글로 남겨주세요! 다음에는 더 다양한 자바 프로그래밍 주제로 찾아뵙겠습니다. 감사합니다!