안녕하세요! 오늘은 자바 컬렉션 프레임워크의 핵심 부분인 Map과 Deque의 실전 활용법과 자주 놓치기 쉬운 유용한 메서드들을 알아보겠습니다. 이론을 넘어서 실무에서 정말 유용하게 활용할 수 있는 다양한 팁과 트릭들을 공유해드립니다!
목차
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보다 나은 이유
- 성능: ArrayDeque는 더 빠릅니다
- 일관성: Stack 클래스는 Vector를 상속받는 레거시 코드로, 동기화 오버헤드가 있습니다
- 기능성: 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. 주의사항 및 팁
- Map은 키와 값 모두 저장해야 합니다!
- 객체를 키로 사용할 때는 반드시 equals()와 hashCode()를 올바르게 오버라이딩해야 합니다.
- 값으로 키 찾기는 비효율적입니다.
- 값으로 키를 찾아야 할 때는 별도의 역방향 맵을 유지하는 것이 좋습니다.
- 맵 순회 시 가장 효율적인 방법
- 키만 필요: keySet()
- 키-값 쌍 모두 필요: entrySet()
- 모든 값에 대한 연산: values()
- 불변 맵이 필요하면 Map.of() 사용
- 수정이 필요 없는 상수 데이터에 적합합니다.
- 스택 기능은 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 컬렉션 프레임워크의 활용에 대한 이해를 높이는 데 도움이 되었기를 바랍니다. 추가 질문이나 토론 주제가 있으시면 언제든지 댓글로 남겨주세요! 다른 컬렉션 프레임워크 활용법이나 구체적인 사례에 대해서도 함께 이야기하면 좋겠습니다.
'📚 학습 기록 > Java 기초 & 중급' 카테고리의 다른 글
자바 컬렉션 프레임워크의 정렬과 유틸리티 클래스 완벽 가이드 (1) | 2025.05.10 |
---|---|
자바 컬렉션 프레임워크의 순회와 정렬 기초 (1) | 2025.04.30 |
자바 컬렉션 프레임워크 완전정복: Map, Stack, Queue 및 Deque (2) | 2025.04.29 |
자바 Set 컬렉션 완전 정복: HashSet, LinkedHashSet, TreeSet 비교와 활용 (1) | 2025.04.24 |
자바 HashSet 완벽 가이드: equals()와 hashCode()의 중요성과 제네릭 활용법 (1) | 2025.04.22 |