Java에서 말하는 컬렉션 프레임워크란, 여러 개의 데이터를 효율적으로 저장하고, 관리하고, 탐색하기 위한 자료구조 + 이를 다루는 공통 인터페이스의 집합이다. 쉽게 말해 데이터 묶음을 다루기 위한 도구 모음과 같은 것이다.
컬렉션 프레임워크를 사용하면 검색, 정렬, 중복 처리 등을 훨씬 편하게 할 수 있다. 실무에서 사용되는 패턴 위주로 정리해봤다.
자바 컬렉션은 크게 List, Set, Map 으로 나눠지고 대표 구현채로는 ArrayList, LinkedList, HashSet, HashMap, TreeMap 등이 있다.
- List - 순서가 있는 데이터 모음
- Set - 중복 없는 데이터 모음
- Map - 키-값 쌍 데이터 구조
- HashMap - 가장 많이 쓰이는 Map 구현체
1. List
개념 및 구조
List는 순서가 있는 데이터의 모음이다. 배열과 유사하지만 크기가 동적으로 변할 수 있다.
[요소1] → [요소2] → [요소3] → [요소4]
↑ ↑
인덱스 0 인덱스 3
주요 특징
- 순서 보장: 삽입한 순서대로 저장된다
- 중복 허용: 같은 값을 여러 번 저장할 수 있다
- 인덱스 접근: get(index)로 특정 위치의 요소에 접근할 수 있다
- 동적 크기: 요소 추가/삭제 시 자동으로 크기가 조정된다
구현체 종류
- ArrayList: 배열 기반, 빠른 조회, 느린 삽입/삭제
- LinkedList: 노드 기반, 빠른 삽입/삭제, 느린 조회
장점
✅ 인덱스로 빠른 접근 (ArrayList 기준 O(1))
✅ 순서가 중요한 데이터 처리에 적합
✅ 중복 데이터 저장이 가능하다
✅ Stream API와 잘 어울린다
단점
❌ 중간 삽입/삭제 시 성능 저하 (ArrayList)
❌ 중복 체크가 필요할 때 별도 로직이 필요하다 ❌ 특정 값 검색 시 순차 탐색이 필요하다 (O(n))
기본 사용법
// 생성
List<String> names = new ArrayList<>();
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 추가
names.add("김철수");
names.add("이영희");
names.add("박민수");
// 조회
String first = names.get(0); // "김철수"
int size = names.size(); // 3
// 순회
for (String name : names) {
System.out.println(name);
}
// Stream API 활용
List<String> filtered = names.stream()
.filter(name -> name.startsWith("김"))
.toList();
2. Set
개념 및 구조
Set은 중복을 허용하지 않는 데이터의 모음이다. 수학의 집합(Set) 개념과 동일하다.
{요소1, 요소2, 요소3}
↑ ↑ ↑
중복 없음, 순서 보장 안됨 (HashSet 기준)
주요 특징
- 중복 불가: 같은 값을 두 번 저장할 수 없다
- 순서 보장 안됨: HashSet은 순서를 보장하지 않으며, LinkedHashSet은 삽입 순서를 보장한다
- 빠른 검색: contains() 메서드가 매우 빠르다 (O(1))
구현체 종류
- HashSet: 해시 테이블 기반, 가장 빠르다, 순서가 없다
- LinkedHashSet: HashSet + 삽입 순서 보장
- TreeSet: 정렬된 순서로 저장된다, O(log n) 성능이다
장점
✅ 중복 자동 제거
✅ 빠른 존재 여부 확인 (contains)
✅ 집합 연산 (합집합, 교집합 등) 지원
✅ 메모리 효율적 (중복 데이터 제거)
단점
❌ 순서를 보장하지 않는다 (HashSet)
❌ 인덱스 접근이 불가능하다
❌ 정렬이 필요하면 TreeSet을 사용한다 (성능 저하)
기본 사용법
// 생성
Set<String> uniqueNames = new HashSet<>();
Set<Integer> numbers = Set.of(1, 2, 3, 4, 5); // 불변 Set
// 추가
uniqueNames.add("김철수");
uniqueNames.add("이영희");
uniqueNames.add("김철수"); // 중복, 추가되지 않는다
// 조회
boolean exists = uniqueNames.contains("김철수"); // true
int size = uniqueNames.size(); // 2
// 중복 제거 예시
List<String> namesWithDuplicates = Arrays.asList("김", "이", "김", "박");
Set<String> unique = new HashSet<>(namesWithDuplicates); // {김, 이, 박}
// 집합 연산
Set<String> set1 = Set.of("A", "B", "C");
Set<String> set2 = Set.of("B", "C", "D");
Set<String> union = new HashSet<>(set1);
union.addAll(set2); // 합집합: {A, B, C, D}
3. Map
개념 및 구조
Map은 키(Key)와 값(Value)의 쌍으로 데이터를 저장하는 자료구조이다.
키(Key) → 값(Value)
"name" → "김철수"
"age" → 30
"city" → "서울"
주요 특징
- 키-값 쌍: 각 데이터는 고유한 키와 값으로 구성된다
- 키는 중복 불가: 같은 키는 하나만 존재한다
- 값은 중복 가능: 서로 다른 키가 같은 값을 가질 수 있다
- 빠른 검색: 키로 값을 빠르게 찾을 수 있다 (O(1))
구현체 종류
- HashMap: 해시 테이블 기반, 가장 빠르다, 순서가 없다
- LinkedHashMap: HashMap + 삽입 순서 보장
- TreeMap: 키 기준 정렬, O(log n) 성능이다
장점
✅ 키로 빠른 값 조회 (O(1))
✅ 키-값 관계를 명확하게 표현
✅ 중복 키 자동 처리 (덮어쓰기)
✅ 다양한 데이터 타입 조합이 가능하다
단점
❌ 순서를 보장하지 않는다 (HashMap)
❌ 키가 없으면 값 접근이 불가능하다
❌ 메모리 사용량이 상대적으로 크다
기본 사용법
// 생성
Map<String, String> userInfo = new HashMap<>();
Map<Integer, String> idToName = Map.of(
1, "김철수",
2, "이영희",
3, "박민수"
);
// 추가/수정
userInfo.put("name", "김철수");
userInfo.put("age", "30");
userInfo.put("city", "서울");
// 조회
String name = userInfo.get("name"); // "김철수"
String age = userInfo.getOrDefault("age", "0"); // 키가 없으면 기본값
// 존재 여부 확인
boolean hasName = userInfo.containsKey("name"); // true
boolean hasValue = userInfo.containsValue("김철수"); // true
// 순회
for (Map.Entry<String, String> entry : userInfo.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 키만 순회
for (String key : userInfo.keySet()) {
System.out.println(key);
}
// 값만 순회
for (String value : userInfo.values()) {
System.out.println(value);
}
4. HashMap
개념 및 구조
HashMap은 Map 인터페이스의 가장 널리 사용되는 구현체이다. 해시 테이블을 기반으로 한다.
해시 버킷 구조:
[0] → [키1:값1] → [키2:값2]
[1] → [키3:값3]
[2] → null
[3] → [키4:값4]
주요 특징
- 해시 함수: 키의 해시값을 계산하여 버킷에 저장한다
- 평균 O(1) 성능: 조회, 삽입, 삭제가 평균적으로 매우 빠르다
- 순서 보장 안 됨: 삽입 순서와 무관하게 저장된다
- null 허용: 키와 값 모두 null을 허용한다 (단, 키는 하나만)
장점
✅ 매우 빠른 조회/삽입/삭제 (평균 O(1))
✅ 메모리 효율적
✅ 가장 널리 사용되어 검증되었다
✅ 다양한 데이터 타입 지원
단점
❌ 최악의 경우 O(n) 성능 (해시 충돌 시)
❌ 순서를 보장하지 않는다
❌ 동기화를 지원하지 않는다 (멀티스레드 환경에서는 ConcurrentHashMap 사용)
기본 사용법
// 생성
HashMap<String, Integer> scoreMap = new HashMap<>();
// 추가
scoreMap.put("김철수", 95);
scoreMap.put("이영희", 88);
scoreMap.put("박민수", 92);
// 조회
Integer score = scoreMap.get("김철수"); // 95
Integer defaultScore = scoreMap.getOrDefault("홍길동", 0); // 키가 없으면 0
// 수정
scoreMap.put("김철수", 100); // 기존 값 덮어쓰기
scoreMap.replace("이영희", 90); // 키가 존재할 때만 수정
// 삭제
scoreMap.remove("박민수");
scoreMap.remove("김철수", 95); // 키와 값이 모두 일치할 때만 삭제
// 크기 확인
int size = scoreMap.size(); // 2
boolean isEmpty = scoreMap.isEmpty(); // false
// 모든 키-값 쌍 제거
scoreMap.clear();
5. 실전 사용 예시
예시 1: 사용자 목록 관리 (List)
시나리오: 여러 사용자의 정보를 순서대로 관리하고, 특정 조건에 맞는 사용자만 필터링
public class UserService {
// 사용자 정보를 순서대로 저장
public List<UserInfo> getUserList() {
List<UserInfo> users = new ArrayList<>();
users.add(new UserInfo("김철수", 30, "서울"));
users.add(new UserInfo("이영희", 25, "부산"));
users.add(new UserInfo("박민수", 35, "대구"));
return users;
}
// 특정 지역 사용자만 필터링
public List<UserInfo> getUsersByCity(List<UserInfo> users, String city) {
return users.stream()
.filter(user -> city.equals(user.getCity()))
.toList();
}
// 나이순으로 정렬
public List<UserInfo> sortByAge(List<UserInfo> users) {
return users.stream()
.sorted(Comparator.comparingInt(UserInfo::getAge))
.toList();
}
}
class UserInfo {
private String name;
private int age;
private String city;
// 생성자, getter, setter 생략
}
왜 List를 사용하는가?
- 사용자 목록은 순서가 중요하다 (최신순, 가입순 등)
- 같은 사용자가 여러 번 나타날 수 있다
- 인덱스로 특정 위치의 사용자에 빠르게 접근할 수 있다
예시 2: 중복 제거 및 빠른 검색 (Set)
시나리오: 설문 답변에서 중복된 질문 코드를 확인하고, 유효한 답변인지 검증
public class SurveyService {
// 중복된 질문 코드 확인
public boolean hasDuplicateQuestions(List<SurveyAnswer> answers) {
List<String> questionCodes = answers.stream()
.map(SurveyAnswer::getQuestionCode)
.toList();
// Set을 사용하여 중복 확인
Set<String> uniqueCodes = new HashSet<>(questionCodes);
// 크기가 다르면 중복이 있다는 의미이다
return questionCodes.size() != uniqueCodes.size();
}
// 허용된 질문 코드 목록과 비교
public boolean isValidQuestions(List<SurveyAnswer> answers) {
Set<String> allowedCodes = Set.of("Q001", "Q002", "Q003", "Q004");
Set<String> answerCodes = answers.stream()
.map(SurveyAnswer::getQuestionCode)
.collect(Collectors.toSet());
// 모든 답변이 허용된 코드에 포함되는지 확인한다
return allowedCodes.containsAll(answerCodes);
}
// 빠른 존재 여부 확인
public boolean isQuestionExists(String questionCode) {
Set<String> validCodes = Set.of("Q001", "Q002", "Q003");
return validCodes.contains(questionCode); // O(1) 빠른 검색
}
}
class SurveyAnswer {
private String questionCode;
private String answer;
// 생성자, getter, setter 생략
}
왜 Set을 사용하는가?
- 중복 자동 제거로 별도 로직이 불필요하다
- contains() 메서드가 매우 빠르다 (O(1))
- 집합 연산 (합집합, 교집합)이 간단하다
예시 3: 사용자별 통계 집계 (HashMap)
시나리오: 여러 사용자의 점수를 관리하고, 특정 사용자의 점수를 빠르게 조회
public class ScoreService {
// 사용자별 점수 저장 및 조회
public Map<String, Integer> calculateUserScores(List<ScoreRecord> records) {
Map<String, Integer> scoreMap = new HashMap<>();
// 사용자별 점수 합계 계산
for (ScoreRecord record : records) {
String userId = record.getUserId();
int score = record.getScore();
// 기존 점수가 있으면 더하고, 없으면 새로 추가
scoreMap.put(userId, scoreMap.getOrDefault(userId, 0) + score);
}
return scoreMap;
}
// 특정 사용자 점수 조회
public Integer getUserScore(Map<String, Integer> scoreMap, String userId) {
return scoreMap.getOrDefault(userId, 0);
}
// 상위 N명 사용자 조회
public List<String> getTopUsers(Map<String, Integer> scoreMap, int topN) {
return scoreMap.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(topN)
.map(Map.Entry::getKey)
.toList();
}
}
class ScoreRecord {
private String userId;
private int score;
// 생성자, getter, setter 생략
}
왜 HashMap을 사용하는가?
- 사용자 ID(키)로 점수(값)를 빠르게 조회할 수 있다 (O(1))
- 키-값 관계가 명확하다
- 동적 추가/수정이 용이하다
예시 4: 복잡한 데이터 구조 (중첩 Map)
시나리오: 제품별 카테고리별 가격 정보를 관리
public class ProductService {
// 제품명 → (카테고리 → 가격) 구조
public Map<String, Map<String, Integer>> getProductPriceMap() {
Map<String, Map<String, Integer>> productMap = new HashMap<>();
// 제품별 카테고리별 가격 설정
Map<String, Integer> productAPrices = new HashMap<>();
productAPrices.put("기본형", 10000);
productAPrices.put("프리미엄", 20000);
productMap.put("제품A", productAPrices);
Map<String, Integer> productBPrices = new HashMap<>();
productBPrices.put("기본형", 15000);
productBPrices.put("프리미엄", 25000);
productMap.put("제품B", productBPrices);
return productMap;
}
// 특정 제품의 특정 카테고리 가격 조회
public Integer getPrice(
Map<String, Map<String, Integer>> productMap,
String productName,
String category) {
Map<String, Integer> prices = productMap.get(productName);
if (prices == null) {
return null;
}
return prices.get(category);
}
// 모든 제품의 기본형 가격 조회
public Map<String, Integer> getAllBasicPrices(
Map<String, Map<String, Integer>> productMap) {
Map<String, Integer> result = new HashMap<>();
for (Map.Entry<String, Map<String, Integer>> entry : productMap.entrySet()) {
String productName = entry.getKey();
Map<String, Integer> prices = entry.getValue();
Integer basicPrice = prices.get("기본형");
if (basicPrice != null) {
result.put(productName, basicPrice);
}
}
return result;
}
}
왜 중첩 Map을 사용하는가?
- 2차원 데이터 구조를 명확하게 표현한다
- 제품명과 카테고리로 빠르게 가격을 조회할 수 있다
- 계층적 데이터 구조 표현에 적합하다
예시 5: 날짜별 데이터 관리 (LinkedHashMap)
시나리오: 날짜 순서대로 데이터를 저장하고 순회해야 하는 경우
public class DailyDataService {
// 날짜 순서를 보장하면서 데이터 저장
public Map<LocalDate, Integer> getDailyStatistics(List<DataRecord> records) {
// LinkedHashMap: 삽입 순서 보장
Map<LocalDate, Integer> dailyMap = new LinkedHashMap<>();
for (DataRecord record : records) {
LocalDate date = record.getDate();
int value = record.getValue();
// 같은 날짜의 값들을 합산
dailyMap.put(date, dailyMap.getOrDefault(date, 0) + value);
}
return dailyMap; // 삽입한 순서대로 반환된다
}
// 최근 N일 데이터 조회
public List<Map.Entry<LocalDate, Integer>> getRecentDays(
Map<LocalDate, Integer> dailyMap, int days) {
return dailyMap.entrySet().stream()
.skip(Math.max(0, dailyMap.size() - days))
.toList();
}
}
class DataRecord {
private LocalDate date;
private int value;
// 생성자, getter, setter 생략
}
왜 LinkedHashMap을 사용하는가?
- 날짜 순서가 중요하다
- 삽입 순서를 보장하면서 Map의 빠른 조회 성능을 유지한다
- 최신 데이터부터 순회하기 쉽다
6. 언제 무엇을 사용할까?
선택 가이드
상황 추천 자료구조 이유
| 순서가 중요하고 중복 허용 | List (ArrayList) | 인덱스 접근, 순서 보장 |
| 중복 제거가 필요 | Set (HashSet) | 자동 중복 제거 |
| 빠른 검색이 필요 | Set (HashSet) | contains()가 O(1) |
| 키로 값을 찾아야 함 | Map (HashMap) | 키-값 쌍 구조 |
| 삽입 순서 보장 필요 | LinkedHashSet / LinkedHashMap | 순서 보장 + 빠른 성능 |
| 정렬이 필요 | TreeSet / TreeMap | 자동 정렬 |
| 중간 삽입/삭제가 많음 | LinkedList | 빠른 삽입/삭제 |
| 조회가 많음 | ArrayList | 빠른 인덱스 접근 |
성능 비교 요약
연산 ArrayList LinkedList HashSet HashMap
| 인덱스 조회 | O(1) | O(n) | - | - |
| 값 검색 | O(n) | O(n) | O(1) | O(1) |
| 삽입 (끝) | O(1) | O(1) | O(1) | O(1) |
| 삽입 (중간) | O(n) | O(1) | - | - |
| 삭제 | O(n) | O(1) | O(1) | O(1) |
실전 팁
- 기본적으로 ArrayList와 HashMap을 사용한다
- 대부분의 경우에 충분히 빠르고 안정적이다
- 중복 체크가 필요하면 Set을 활용한다
- List.contains()는 O(n), Set.contains()는 O(1)이다
- 순서가 중요하면 Linked 계열을 사용한다
- LinkedHashSet, LinkedHashMap
- 멀티스레드 환경에서는 동기화된 컬렉션을 사용한다
- ConcurrentHashMap, Collections.synchronizedList()
- 불변 컬렉션이 필요하면
- List.of(), Set.of(), Map.of()를 사용한다
마무리
Java 컬렉션 프레임워크는 다양한 상황에 맞는 최적의 자료구조를 제공한다. 각 자료구조의 특징과 성능을 이해하고, 상황에 맞게 선택하는 것이 중요하다.
- List: 순서가 있고 중복 허용
- Set: 중복 없음, 빠른 검색
- Map: 키-값 쌍, 빠른 조회
- HashMap: 가장 널리 사용되는 Map 구현체
올바른 자료구조 선택은 코드의 가독성과 성능을 크게 향상시킨다!
'Dev Log' 카테고리의 다른 글
| 무한스크롤을 적용하며 다시 보게 된 페이징 구조 (0) | 2026.03.01 |
|---|---|
| 파일 업로드: “썸네일/메인 이미지” 업로드에서 두 필드가 동기화되는 버그 트러블슈팅 (1) | 2026.02.22 |
| 사용자 추천 기능은 어떻게 테이블을 설계해야 할까 (0) | 2026.02.08 |
| 프론트엔드에서 처리하는가, 백엔드에서 처리하는가 (0) | 2026.02.01 |
| 두 대의 서버로 분산된 로그를 실시간으로 추적하는 방법 (0) | 2026.01.25 |