자바 컬렉션 프레임워크 완전 정리
자바를 공부하다 보면 배열만으로는 부족하다는 순간이 꼭 온다. 처음에는 숫자 몇 개, 문자열 몇 개 저장하는 정도라서 배열로도 충분해 보이지만, 실제로 프로그램을 만들기 시작하면 데이터의 개수가 계속 바뀌고, 중복을 막아야 하거나, 빠르게 찾거나, 순서를 유지해야 하는 상황이 자주 생긴다. 이럴 때 사용하는 것이 바로 컬렉션 프레임워크(Collection Framework)다.
컬렉션은 쉽게 말하면 여러 데이터를 다루기 쉽게 만들어 놓은 도구 모음이다. 자바에서는 단순히 "값을 담는 통" 정도가 아니라, 데이터를 어떤 방식으로 저장하고, 어떤 속도로 추가하고, 어떤 방식으로 검색하고, 중복을 허용할지 말지까지 구조적으로 나누어 제공한다. 그래서 컬렉션을 제대로 이해하면 단순 문법을 아는 수준을 넘어서 "어떤 문제에 어떤 자료구조를 선택해야 하는가"까지 생각할 수 있게 된다.
1. 컬렉션 프레임워크가 왜 중요한가
자바에서 개발을 하다 보면 회원 목록, 게시글 목록, 주문 내역, 댓글 리스트, 로그 기록, 중복 없는 태그 모음, 아이디와 회원 정보를 연결하는 구조 등이 계속 등장한다. 이런 데이터들을 모두 배열로만 처리하려고 하면 불편한 점이 많다. 배열은 길이가 고정되어 있기 때문에 처음 크기를 잘못 잡으면 다시 복사해야 하고, 중간 삽입이나 삭제도 번거롭다.
반면 컬렉션은 데이터의 개수가 유동적으로 변해도 대응할 수 있고, 중복 허용 여부, 정렬 여부, 탐색 속도, 삽입 방식에 따라 다양한 구현체를 고를 수 있다. 즉, 컬렉션은 단순 편의 기능이 아니라 코드의 기본 뼈대라고 볼 수 있다.
2. 컬렉션 프레임워크의 큰 구조
자바 컬렉션 프레임워크는 크게 보면 List, Set, Map으로 나뉜다. 이 세 가지를 먼저 머리에 잡아 두면 훨씬 이해가 쉬워진다.
List는 순서가 있고, 중복 저장이 가능하다.
Set은 중복 저장이 불가능하다.
Map은 key와 value 한 쌍으로 데이터를 저장한다.
이걸 아주 단순하게 비유하면 다음과 같다.
List는 출석부처럼 순서대로 적는 구조다.
Set은 중복 없이 닉네임만 모아 두는 구조다.
Map은 아이디를 키로 해서 회원 정보를 찾아가는 구조다.
3. List: 순서가 필요하고 중복을 허용해야 할 때
List는 가장 많이 쓰는 컬렉션이다. 데이터를 넣은 순서가 유지되고, 같은 값도 여러 번 저장할 수 있다. 예를 들어 게시판 글 목록, 댓글 목록, 주문 상품 목록처럼 순서가 있고 중복도 가능한 데이터에 잘 맞는다.
3-1. ArrayList
ArrayList는 내부적으로 배열 기반으로 동작한다. 그래서 인덱스로 접근하는 속도가 빠르다. 예를 들어 0번째, 5번째, 100번째 데이터를 꺼내는 작업이 효율적이다. 대신 중간에 데이터를 삽입하거나 삭제하면 뒤에 있는 요소들을 한 칸씩 밀거나 당겨야 해서 비용이 든다.
즉, ArrayList는 다음 상황에 잘 어울린다.
데이터를 끝에 계속 추가하는 경우
인덱스로 자주 조회하는 경우
중간 삽입/삭제보다 읽기가 많은 경우
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("자바");
list.add("스프링");
list.add("오라클");
list.add("자바");
System.out.println(list);
System.out.println("0번째 요소: " + list.get(0));
System.out.println("전체 크기: " + list.size());
list.remove(1);
System.out.println(list);
}
}
위 코드에서 같은 "자바"가 두 번 들어간 것을 보면 List는 중복을 허용한다는 점을 알 수 있다. 그리고 get(0)처럼 인덱스로 꺼낼 수 있기 때문에 배열과 비슷하게 다루기 쉽다.
3-2. LinkedList
LinkedList는 내부적으로 각 데이터가 서로 연결된 구조라고 이해하면 된다. 배열처럼 한 덩어리로 붙어 있는 것이 아니라, 앞 요소와 뒤 요소가 이어지는 방식이다. 그래서 중간 삽입이나 삭제가 ArrayList보다 유리한 경우가 있다. 하지만 인덱스로 특정 위치를 찾으려면 앞에서부터 따라가야 해서 조회는 상대적으로 불리하다.
즉, LinkedList는 다음 상황에 어울린다.
중간 삽입/삭제가 자주 일어나는 경우
맨 앞/맨 뒤에서 넣고 빼는 작업이 많은 경우
다만 초보 단계에서는 대부분 ArrayList를 더 많이 사용하게 된다. 실제로도 일반적인 목록 처리는 ArrayList가 더 자주 쓰인다. LinkedList는 자료구조 특성을 이해하는 데 중요하지만, 무조건 더 좋은 구조라고 보면 안 된다.
4. Set: 중복을 허용하지 않을 때
Set은 같은 값을 두 번 저장할 수 없다. 예를 들어 사용자가 검색한 키워드에서 중복을 제거하고 싶거나, 태그 목록에서 같은 태그가 여러 번 들어가면 안 되는 경우에 유용하다.
4-1. HashSet
HashSet은 중복 제거를 빠르게 처리할 때 많이 사용한다. 내부적으로 해시 기반 구조를 사용하기 때문에 저장과 검색 속도가 빠른 편이다. 다만 중요한 점은 순서를 보장하지 않는다는 것이다. 즉, 넣은 순서대로 꺼내진다고 기대하면 안 된다.
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("java");
set.add("spring");
set.add("oracle");
set.add("java");
System.out.println(set);
System.out.println("크기: " + set.size());
}
}
"java"를 두 번 넣었지만 실제로는 한 번만 저장된다. 이것이 Set의 핵심이다.
4-2. LinkedHashSet
HashSet은 순서를 보장하지 않지만, LinkedHashSet은 입력 순서를 유지한다. 중복은 막고 싶지만 화면에 보여줄 때는 입력 순서를 살리고 싶다면 LinkedHashSet이 더 적합하다.
4-3. TreeSet
TreeSet은 데이터를 정렬된 상태로 관리한다. 예를 들어 숫자를 넣으면 자동 오름차순 정렬이 되고, 문자열도 정렬 기준에 맞게 저장된다. 정렬이 필요할 때는 편리하지만, 단순한 HashSet보다 속도 면에서는 불리할 수 있다.
5. Map: key와 value로 저장하는 구조
Map은 List나 Set과 조금 다르게 값 하나만 저장하는 것이 아니라 key와 value를 한 쌍으로 저장한다. 예를 들어 회원 아이디를 key로 두고, 회원 객체를 value로 저장할 수 있다. 이 구조는 실제 개발에서 매우 자주 등장한다.
예를 들어 다음과 같은 상황을 생각해 볼 수 있다.
아이디로 회원 찾기
상품 번호로 상품 정보 찾기
설정 이름으로 설정 값 찾기
5-1. HashMap
HashMap은 가장 대표적인 Map 구현체다. 키를 이용해 값을 빠르게 찾을 수 있다는 것이 큰 장점이다. 예를 들어 "user01"이라는 key로 회원 정보를 저장해 두면, 필요할 때 바로 꺼낼 수 있다.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("김자바", 90);
scoreMap.put("박스프링", 85);
scoreMap.put("이오라클", 95);
System.out.println(scoreMap.get("김자바"));
System.out.println(scoreMap.containsKey("박스프링"));
System.out.println(scoreMap.containsValue(100));
scoreMap.put("김자바", 100);
System.out.println(scoreMap);
}
}
여기서 중요한 점은 key는 중복될 수 없다는 것이다. 같은 key로 put을 다시 하면 새로 추가되는 것이 아니라 기존 값이 덮어써진다. 즉, Map은 "이 key에 대응하는 값은 하나"라는 개념이다.
5-2. LinkedHashMap
LinkedHashMap은 입력 순서를 유지하는 Map이다. HashMap은 순서를 보장하지 않지만, LinkedHashMap은 넣은 순서대로 순회할 수 있다. 화면 출력 순서가 중요할 때 유용하다.
5-3. TreeMap
TreeMap은 key를 기준으로 정렬한다. 예를 들어 숫자 key라면 오름차순, 문자열 key라면 사전 순으로 정렬된다. 자동 정렬이 필요할 때는 편리하지만 속도 면에서는 HashMap보다 불리할 수 있다.
6. 컬렉션을 볼 때 꼭 알아야 하는 제네릭(Generic)
컬렉션을 사용할 때는 보통 제네릭을 함께 사용한다. 예를 들어 ArrayList 안에 문자열만 넣고 싶다면 List<String>처럼 타입을 지정한다. 이렇게 하면 다른 타입이 실수로 들어가는 것을 막을 수 있고, 꺼낼 때도 형변환 없이 바로 사용할 수 있다.
List<String> names = new ArrayList<>();
names.add("홍길동");
names.add("임꺽정");
// names.add(100); // 컴파일 오류
제네릭이 없던 시절에는 Object로 다 받아서 꺼낼 때마다 형변환을 해야 했다. 하지만 제네릭을 사용하면 컴파일 단계에서 타입 오류를 막을 수 있어서 코드가 더 안전하고 읽기 쉬워진다.
7. equals()와 hashCode()가 왜 중요한가
Set이나 Map을 제대로 이해하려면 반드시 equals()와 hashCode()를 알아야 한다. 특히 HashSet, HashMap 같은 해시 기반 컬렉션은 객체의 중복 판단이나 key 비교를 할 때 이 두 메서드를 사용한다.
예를 들어 회원 번호가 같으면 같은 회원으로 보고 싶다고 하자. 그런데 equals()와 hashCode()를 재정의하지 않으면 겉으로 값이 같아 보여도 다른 객체로 인식될 수 있다.
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
class Member {
private int memberNo;
private String name;
public Member(int memberNo, String name) {
this.memberNo = memberNo;
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Member)) return false;
Member other = (Member) obj;
return memberNo == other.memberNo;
}
@Override
public int hashCode() {
return Objects.hash(memberNo);
}
@Override
public String toString() {
return "Member{memberNo=" + memberNo + ", name='" + name + "'}";
}
}
public class Main {
public static void main(String[] args) {
Set<Member> set = new HashSet<>();
set.add(new Member(1, "김철수"));
set.add(new Member(1, "김철수"));
System.out.println(set);
System.out.println("크기: " + set.size());
}
}
위처럼 equals()와 hashCode()를 올바르게 재정의하면 동일한 회원 번호를 가진 객체를 중복으로 보지 않게 할 수 있다. 이 부분은 면접에서도 자주 물어보는 주제다. "HashSet에서 중복은 어떻게 판단하나요?"라는 질문이 나오면 equals와 hashCode를 같이 말할 수 있어야 한다.
8. 컬렉션 순회 방법
컬렉션에 저장한 데이터를 사용하려면 순회가 필요하다. 가장 기본적인 방법은 for문, 향상된 for문, Iterator, 그리고 람다식 forEach가 있다.
8-1. 향상된 for문
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String item : list) {
System.out.println(item);
}
가장 간단하고 읽기 쉬운 방식이다. 조회만 할 때 자주 사용한다.
8-2. Iterator
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("사과");
list.add("바나나");
list.add("포도");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String fruit = it.next();
System.out.println(fruit);
}
}
}
Iterator는 순회 도중 삭제가 필요할 때도 중요하다. 일반 for문이나 향상된 for문에서 바로 remove를 잘못 쓰면 예외가 발생할 수 있는데, Iterator를 사용하면 컬렉션 구조를 더 안전하게 다룰 수 있다.
9. 시간 복잡도 관점에서 보는 컬렉션
컬렉션은 단순히 문법으로 외우면 금방 헷갈린다. 대신 "어떤 작업이 빠른가"를 기준으로 보면 정리가 잘 된다.
ArrayList는 인덱스 조회가 빠르다.
LinkedList는 중간 삽입/삭제에 강점이 있다.
HashSet은 중복 없는 저장과 빠른 검색에 유리하다.
HashMap은 key 기반 조회가 빠르다.
TreeSet, TreeMap은 정렬을 유지하는 대신 더 무거울 수 있다.
물론 실제 성능은 데이터 개수, 사용 패턴, JVM 최적화 등에 따라 달라질 수 있다. 그래서 무조건 외우기보다는 "이 구조는 왜 이런 특징이 생기는가"를 이해하는 것이 더 중요하다.
10. 자주 하는 실수
10-1. List와 Set의 목적을 섞는 경우
중복을 허용하면 안 되는 데이터인데 List를 쓰는 경우가 많다. 예를 들어 태그 목록, 권한 목록, 좋아요 누른 회원 목록 같은 것은 중복이 생기면 안 되므로 Set이 더 자연스럽다.
10-2. Map을 순서 있는 자료처럼 착각하는 경우
HashMap은 기본적으로 순서를 보장하지 않는다. 출력 순서가 중요하면 LinkedHashMap이나 TreeMap을 검토해야 한다.
10-3. 객체를 Set이나 Map key로 쓰면서 equals/hashCode를 재정의하지 않는 경우
이건 정말 많이 하는 실수다. 겉보기 값이 같아도 다른 객체로 취급되어 중복 제거가 안 되거나 key 조회가 예상대로 되지 않을 수 있다.
10-4. ArrayList에서 중간 삭제가 많은데도 무조건 사용하는 경우
ArrayList가 가장 익숙하다고 해서 모든 상황에 쓰면 안 된다. 데이터 구조를 선택할 때는 사용 패턴을 먼저 봐야 한다.
11. 실전 예제로 이해하기
간단한 회원 관리 상황을 생각해 보자.
회원 가입 순서대로 보여줘야 한다면 List가 필요하다.
중복 없는 관심 태그를 저장하려면 Set이 필요하다.
회원 아이디로 회원 정보를 바로 찾으려면 Map이 필요하다.
import java.util.*;
class User {
private String userId;
private String name;
public User(String userId, String name) {
this.userId = userId;
this.name = name;
}
public String getUserId() {
return userId;
}
@Override
public String toString() {
return "User{userId='" + userId + "', name='" + name + "'}";
}
}
public class Main {
public static void main(String[] args) {
List<User> joinOrder = new ArrayList<>();
Set<String> tags = new LinkedHashSet<>();
Map<String, User> userMap = new HashMap<>();
User user1 = new User("user01", "김자바");
User user2 = new User("user02", "박스프링");
joinOrder.add(user1);
joinOrder.add(user2);
tags.add("백엔드");
tags.add("자바");
tags.add("스프링");
tags.add("자바");
userMap.put(user1.getUserId(), user1);
userMap.put(user2.getUserId(), user2);
System.out.println("가입 순서:");
for (User user : joinOrder) {
System.out.println(user);
}
System.out.println("\n중복 없는 태그:");
for (String tag : tags) {
System.out.println(tag);
}
System.out.println("\n아이디로 회원 조회:");
System.out.println(userMap.get("user01"));
}
}
이 예제 하나만 봐도 알 수 있다. 같은 프로그램 안에서도 목적에 따라 List, Set, Map을 각각 다르게 써야 한다. 컬렉션은 하나만 잘 외우는 것이 아니라 상황에 맞게 고를 줄 아는 것이 중요하다.
12. 언제 어떤 컬렉션을 골라야 할까
정리하면 다음 기준으로 선택하면 된다.
순서가 필요하고 중복 허용이면 List
중복 제거가 핵심이면 Set
key로 빠르게 찾고 싶으면 Map
그 안에서 다시 세부적으로 나눠 본다.
조회가 많으면 ArrayList
삽입/삭제가 많으면 LinkedList 고려
빠른 중복 제거는 HashSet
입력 순서 유지가 필요하면 LinkedHashSet
자동 정렬이 필요하면 TreeSet
빠른 key 조회는 HashMap
입력 순서 유지 Map은 LinkedHashMap
정렬되는 Map은 TreeMap
13. 컬렉션을 공부할 때 중요한 관점
이 구조는 중복을 허용하는가?
순서를 유지하는가?
검색이 빠른가?
중간 삽입/삭제에 유리한가?
정렬이 자동으로 되는가?
객체 비교는 어떻게 이루어지는가?
이 질문에 답할 수 있으면 단순 암기가 아니라 진짜 이해한 상태에 가까워진다.
14. 마무리
자바 컬렉션 프레임워크는 단순히 List, Set, Map 이름만 외우는 파트가 아니다. 데이터를 어떻게 저장하고, 어떻게 찾고, 어떻게 중복을 막고, 어떤 구조를 선택해야 하는지까지 이어지는 핵심 주제다.
처음에는 ArrayList와 HashMap만 자주 쓰게 되더라도 괜찮다. 대신 왜 ArrayList를 썼는지, 왜 HashMap이 필요한지, Set을 쓰면 어떤 점이 달라지는지 계속 생각하면서 코드를 짜는 습관이 중요하다. 이런 감각이 쌓이면 단순히 자바 문법을 아는 수준이 아니라 문제에 맞는 구조를 고를 줄 아는 개발자로 성장할 수 있다.
컬렉션은 자바의 기본이지만, 동시에 자바 실력을 크게 갈라놓는 주제이기도 하다. 얕게 보면 그냥 자료 담는 통이고, 깊게 보면 프로그램 구조를 바꾸는 도구다. 그래서 자바를 제대로 공부하려면 컬렉션은 반드시 한 번 깊게 정리하고 넘어가야 한다.
'Computer Science > Java' 카테고리의 다른 글
| [JAVA] 자바 예외 처리 완전 정리 (0) | 2026.03.15 |
|---|---|
| [JAVA] 다형성 (0) | 2026.02.15 |
| [JAVA] 10 컬렉션(List / Set / Map)과 제네릭 (0) | 2026.02.15 |
| [JAVA] 08 객체지향(OOP) 4대 특징 (0) | 2026.02.15 |
| [JAVA] 07 클래스와 객체 (0) | 2026.02.15 |