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

자바 컬렉션 프레임워크 실전 가이드: Map과 Deque의 숨겨진 활용법

zenjoydev 2025. 4. 30. 15:35

안녕하세요! 오늘은 자바 컬렉션 프레임워크의 핵심 부분인 Map과 Deque의 실전 활용법과 자주 놓치기 쉬운 유용한 메서드들을 알아보겠습니다. 이론을 넘어서 실무에서 정말 유용하게 활용할 수 있는 다양한 팁과 트릭들을 공유해드립니다!

목차

  1. 맵에서 키와 값 다루기
  2. Map의 고급 메서드들
  3. 불변 맵 생성하기
  4. Stack vs Deque
  5. Map 활용 실전 코드
  6. 주의사항 및 팁

1. 맵에서 키와 값 다루기

Map은 키-값 쌍으로 데이터를 저장하는 자료구조입니다. 이 데이터에 접근하는 방법은 크게 세 가지가 있습니다:

keySet(), values(), entrySet()

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

public class MapAccessExample {
    public static void main(String[] args) {
        Map<String, Integer> studentScores = new HashMap<>();
        studentScores.put("Alice", 95);
        studentScores.put("Bob", 82);
        studentScores.put("Charlie", 88);
        
        // 1. keySet() - 모든 키를 Set으로 반환
        System.out.println("== 모든 학생 명단 ==");
        for (String name : studentScores.keySet()) {
            System.out.println(name);
        }
        
        // 2. values() - 모든 값을 Collection으로 반환
        System.out.println("\n== 모든 점수 ==");
        for (Integer score : studentScores.values()) {
            System.out.println(score);
        }
        
        // 3. entrySet() - 모든 키-값 쌍을 Set으로 반환
        System.out.println("\n== 학생별 점수 ==");
        for (Map.Entry<String, Integer> entry : studentScores.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

특정 키 찾고 작업하기

특정 키를 찾고 작업을 수행할 때는 containsKey() + get()을 함께 활용하는 것이 효율적입니다:

// 좋은 방법: containsKey() + get()
if (studentScores.containsKey("Alice")) {
    int score = studentScores.get("Alice");
    System.out.println("Alice의 점수: " + score);
}

// 전체 데이터에 대해 작업할 때는 keySet() 또는 entrySet() 사용
for (String name : studentScores.keySet()) {
    // 키를 기반으로 작업
    System.out.println(name + "의 점수: " + studentScores.get(name));
}

// 모든 키-값에 대해 직접 접근할 때는 entrySet() 사용 - 가장 효율적
for (Map.Entry<String, Integer> entry : studentScores.entrySet()) {
    String name = entry.getKey();
    Integer score = entry.getValue();
    
    if (score >= 90) {
        System.out.println(name + "는 A등급입니다.");
    }
}

2. Map의 고급 메서드들

put vs putIfAbsent

두 메서드 모두 맵에 키-값 쌍을 추가하지만 중요한 차이가 있습니다:

Map<String, String> userEmails = new HashMap<>();

// put - 이미 키가 존재해도 값을 덮어씁니다
userEmails.put("john", "john@example.com");
userEmails.put("john", "john.doe@example.com"); // 값이 덮어써짐

// putIfAbsent - 키가 없을 때만 값을 추가합니다
userEmails.clear();
userEmails.putIfAbsent("john", "john@example.com");
userEmails.putIfAbsent("john", "john.doe@example.com"); // 무시됨 (이미 키가 존재하므로)

System.out.println(userEmails.get("john")); // john@example.com 출력

getOrDefault 활용하기

키가 존재하지 않을 때 기본값을 사용하고 싶다면 getOrDefault를 활용하세요:

Map<String, Integer> wordCount = new HashMap<>();
wordCount.put("hello", 5);
wordCount.put("world", 3);

// 키가 없으면 기본값 0 반환
int countJava = wordCount.getOrDefault("java", 0);
System.out.println("'java'의 등장 횟수: " + countJava); // 0 출력

// 단어 빈도수 계산에 유용
String text = "apple banana apple orange banana apple";
String[] words = text.split(" ");

Map<String, Integer> frequency = new HashMap<>();
for (String word : words) {
    // 기존에 있으면 값 가져오고, 없으면 0을 기본값으로 사용 후 1 증가
    frequency.put(word, frequency.getOrDefault(word, 0) + 1);
}

System.out.println(frequency); // {orange=1, banana=2, apple=3} 출력

contains 메서드의 주의점

containsKey()는 키의 존재 여부만 확인하고, 값에 접근하지 않아 효율적입니다. 반면 containsValue()는 모든 값을 순회하므로 성능이 떨어집니다:

// 효율적 - O(1) 시간 복잡도
boolean hasKey = studentScores.containsKey("Bob");

// 비효율적 - O(n) 시간 복잡도 (모든 값을 검사)
boolean hasValue = studentScores.containsValue(88);

// 값으로 키 찾기 필요시 - 별도의 역방향 맵을 유지하는 것이 좋음
Map<Integer, String> scoreToName = new HashMap<>();
for (Map.Entry<String, Integer> entry : studentScores.entrySet()) {
    scoreToName.put(entry.getValue(), entry.getKey());
}

3. 불변 맵 생성하기

Java 9부터 Map.of()를 이용해 불변 맵을 간편하게 생성할 수 있습니다:

// 불변 맵 생성 (Java 9+)
Map<String, Integer> constants = Map.of(
    "MAX_VALUE", 100,
    "MIN_VALUE", 1,
    "DEFAULT", 50
);

// constants.put("NEW_VALUE", 75); // UnsupportedOperationException 발생

// 더 많은 항목이 필요한 경우 Map.ofEntries 사용
import static java.util.Map.entry;

Map<String, String> countries = Map.ofEntries(
    entry("KR", "South Korea"),
    entry("US", "United States"),
    entry("JP", "Japan"),
    entry("FR", "France"),
    entry("UK", "United Kingdom")
);

4. Stack vs Deque

자바에서 스택 자료구조를 사용할 때는 레거시 Stack 클래스보다 ArrayDeque를 사용하는 것이 좋습니다:

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

public class StackVsDequeExample {
    public static void main(String[] args) {
        // 레거시 Stack 클래스 (권장하지 않음)
        Stack<String> legacyStack = new Stack<>();
        
        // ArrayDeque를 스택으로 사용 (권장)
        Deque<String> stack = new ArrayDeque<>();
        
        // 삽입
        stack.push("첫번째");
        stack.push("두번째");
        stack.push("세번째");
        
        // 최상위 요소 확인 (제거하지 않음)
        System.out.println("맨 위 요소: " + stack.peek());
        
        // 요소 제거 및 반환
        while (!stack.isEmpty()) {
            System.out.println("제거: " + stack.pop());
        }
    }
}

ArrayDeque가 Stack보다 나은 이유

  1. 성능: ArrayDeque는 더 빠릅니다
  2. 일관성: Stack 클래스는 Vector를 상속받는 레거시 코드로, 동기화 오버헤드가 있습니다
  3. 기능성: Deque 인터페이스는 양방향 작업을 지원합니다

5. Map 활용 실전 코드

간단한 캐시 구현하기

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

public class SimpleCache {
    private final Map<String, Object> cache = new HashMap<>();
    
    public Object get(String key) {
        // 캐시에 있으면 반환, 없으면 계산 후 저장
        if (!cache.containsKey(key)) {
            Object value = computeExpensiveOperation(key);
            cache.put(key, value);
            return value;
        }
        return cache.get(key);
    }
    
    // 더 깔끔한 방법 - computeIfAbsent 사용
    public Object getOptimized(String key) {
        return cache.computeIfAbsent(key, this::computeExpensiveOperation);
    }
    
    private Object computeExpensiveOperation(String key) {
        // 실제로는 비용이 많이 드는 연산 수행
        System.out.println(key + "에 대한 비용이 많이 드는 연산 수행 중...");
        return "Result for " + key;
    }
    
    public static void main(String[] args) {
        SimpleCache cache = new SimpleCache();
        
        // 첫 번째 호출 - 계산 수행
        System.out.println(cache.get("key1"));
        
        // 두 번째 호출 - 캐시에서 가져옴
        System.out.println(cache.get("key1"));
        
        // 다른 키 호출 - 새로운 계산
        System.out.println(cache.get("key2"));
        
        // 최적화된 방법으로 동일한 작업 수행
        System.out.println("\n최적화된 방법 사용:");
        System.out.println(cache.getOptimized("key3"));
        System.out.println(cache.getOptimized("key3"));
    }
}

단어 빈도수 계산기

import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class WordFrequencyCounter {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("텍스트를 입력하세요:");
        String text = scanner.nextLine();
        
        // 단어 빈도수 계산
        Map<String, Integer> wordFrequency = countWords(text);
        
        // 결과 출력
        System.out.println("\n단어 빈도수:");
        for (Map.Entry<String, Integer> entry : wordFrequency.entrySet()) {
            System.out.printf("%-15s: %d\n", entry.getKey(), entry.getValue());
        }
    }
    
    private static Map<String, Integer> countWords(String text) {
        Map<String, Integer> frequency = new HashMap<>();
        
        // 텍스트를 단어로 분리
        String[] words = text.toLowerCase().split("\\W+");
        
        for (String word : words) {
            if (word.isEmpty()) continue;
            
            // getOrDefault 활용
            frequency.put(word, frequency.getOrDefault(word, 0) + 1);
        }
        
        return frequency;
    }
}

6. 주의사항 및 팁

  1. Map은 키와 값 모두 저장해야 합니다!
    • 객체를 키로 사용할 때는 반드시 equals()와 hashCode()를 올바르게 오버라이딩해야 합니다.
  2. 값으로 키 찾기는 비효율적입니다.
    • 값으로 키를 찾아야 할 때는 별도의 역방향 맵을 유지하는 것이 좋습니다.
  3. 맵 순회 시 가장 효율적인 방법
    • 키만 필요: keySet()
    • 키-값 쌍 모두 필요: entrySet()
    • 모든 값에 대한 연산: values()
  4. 불변 맵이 필요하면 Map.of() 사용
    • 수정이 필요 없는 상수 데이터에 적합합니다.
  5. 스택 기능은 ArrayDeque로!
    • 레거시 Stack 클래스는 미사용을 권장합니다.

이러한 기법들을 활용하면 더 깔끔하고 효율적인 코드를 작성할 수 있습니다. Map과 Deque의 다양한 메서드를 적재적소에 활용하여 코드 품질과 성능을 향상시켜보세요!

아래는 위의 학습 내용을 잘 보여주는 종합 예제입니다:

import java.util.*;

public class CollectionUtilsExample {
    public static void main(String[] args) {
        // Map 예제
        Map<String, Student> studentMap = new HashMap<>();
        
        // 학생 추가
        studentMap.put("S001", new Student("S001", "Alice", 95));
        studentMap.put("S002", new Student("S002", "Bob", 82));
        studentMap.put("S003", new Student("S003", "Charlie", 88));
        
        // getOrDefault 사용 - 존재하지 않는 학생 ID 처리
        Student unknown = studentMap.getOrDefault("S999", Student.UNKNOWN);
        System.out.println("ID S999의 학생: " + unknown.getName());
        
        // putIfAbsent 사용 - 이미 존재하는 키는 덮어쓰지 않음
        studentMap.putIfAbsent("S001", new Student("S001", "Alex", 75));
        System.out.println("ID S001의 학생: " + studentMap.get("S001").getName()); // Alice 출력
        
        // Deque를 스택으로 사용
        Deque<String> history = new ArrayDeque<>();
        history.push("메인 페이지");
        history.push("제품 목록");
        history.push("제품 상세");
        
        System.out.println("\n브라우저 방문 기록(역순):");
        while (!history.isEmpty()) {
            System.out.println(history.pop());
        }
        
        // Deque를 큐로 사용
        Deque<Task> taskQueue = new ArrayDeque<>();
        taskQueue.offer(new Task("이메일 확인", 3));
        taskQueue.offer(new Task("보고서 작성", 1));
        taskQueue.offer(new Task("회의 준비", 2));
        
        System.out.println("\n작업 처리 순서:");
        while (!taskQueue.isEmpty()) {
            Task task = taskQueue.poll();
            System.out.println(task.getName() + " (우선순위: " + task.getPriority() + ")");
        }
    }
    
    static class Student {
        private final String id;
        private final String name;
        private final int score;
        
        public static final Student UNKNOWN = new Student("UNKNOWN", "Unknown Student", 0);
        
        public Student(String id, String name, int score) {
            this.id = id;
            this.name = name;
            this.score = score;
        }
        
        public String getId() { return id; }
        public String getName() { return name; }
        public int getScore() { return score; }
        
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Student student = (Student) o;
            return Objects.equals(id, student.id);
        }
        
        @Override
        public int hashCode() {
            return Objects.hash(id);
        }
    }
    
    static class Task {
        private final String name;
        private final int priority;
        
        public Task(String name, int priority) {
            this.name = name;
            this.priority = priority;
        }
        
        public String getName() { return name; }
        public int getPriority() { return priority; }
    }
}

실무에서 컬렉션 프레임워크를 사용할 때 이런 팁들을 적용하면 더 효율적이고 읽기 쉬운 코드를 작성할 수 있습니다. 특히 Map과 Deque의 다양한 메서드들을 적절히 활용한다면 많은 로직을 더 간결하게 표현할 수 있죠!


이 글이 Java 컬렉션 프레임워크의 활용에 대한 이해를 높이는 데 도움이 되었기를 바랍니다. 추가 질문이나 토론 주제가 있으시면 언제든지 댓글로 남겨주세요! 다른 컬렉션 프레임워크 활용법이나 구체적인 사례에 대해서도 함께 이야기하면 좋겠습니다.