JAVA

Java 배열과 컬렉션 프레임워크 입문 — Array에서 List, Set, Map까지 한 번에 잡기

IT Lab 2026. 2. 18. 10:00

Java에서 배열의 한계를 넘어 List, Set, Map으로 자연스럽게 확장하는 흐름과 실무 선택 기준을 정리합니다.

 

처음에는 배열로 충분해 보이는데, 조금만 기능이 늘어나면 “중복 제거는?”, “빠른 검색은?”, “키로 바로 찾고 싶은데?” 같은 요구가 바로 생깁니다. 이때 무작정 ArrayList만 쓰기 시작하면, 나중에 성능과 코드 가독성에서 비용을 치르게 됩니다.

핵심 개념 (Java 컬렉션 선택 기준: 배열 → List → Set → Map)

배열에서 List, Set, Map으로 확장되는 선택 흐름을 보여주는 다이어그램

Java의 컬렉션 프레임워크는 “데이터를 어떤 규칙으로 담고, 어떤 작업을 빠르게 할 것인가”를 선택하는 도구 상자라고 보시면 됩니다. 배열에서 시작해 List → Set → Map으로 갈수록 “기능(규칙)”이 명확해지고, 그만큼 의도가 코드에 잘 드러납니다.

1) 배열(Array): 가장 단순하지만 ‘크기’가 고정

배열은 메모리 구조가 단순하고 접근이 빠릅니다(인덱스로 바로 접근). 하지만 크기가 고정이라서 요소를 추가/삭제하는 순간부터 불편해지고, “중복 금지”, “키 기반 조회” 같은 규칙을 직접 구현해야 합니다.

  • 강점: 빠른 인덱스 접근, 단순함
  • 약점: 크기 변경 불가, 편의 메서드 부족, 규칙(중복/키) 표현 어려움

2) List: “순서가 있고, 중복을 허용하는” 목록

List는 배열의 자연스러운 확장판입니다. 크기가 유동적이고, 정렬/필터/부분조회 같은 작업이 편해집니다. 다만 “같은 값이 들어가면 안 된다” 같은 규칙을 List로 표현하면, 결국 매번 contains()로 검사하는 코드가 퍼지기 쉽습니다.

  • 대표 구현: ArrayList, LinkedList
  • 언제 쓰나: “입력 순서가 중요”하거나 “같은 값이 여러 번 들어갈 수 있는” 데이터

3) Set: “중복이 없는” 집합

Set은 “유일성(uniqueness)”을 자료구조 레벨에서 보장합니다. 예를 들어 사용자 권한 목록, 태그 목록처럼 “같은 값이 여러 번 들어오면 안 되는” 데이터는 Set이 의도를 가장 정확히 표현합니다.

  • 대표 구현: HashSet, LinkedHashSet, TreeSet
  • 핵심: 중복 판단 기준은 equals()/hashCode()(Hash 기반) 또는 Comparator(정렬 기반)에 의해 결정

4) Map: “키로 값에 바로 접근”하는 사전

Map은 컬렉션 중 실무에서 가장 자주 ‘성능 이슈를 줄여주는’ 도구입니다. 리스트에서 특정 ID를 가진 객체를 찾기 위해 매번 순회하면 O(n)이지만, Map으로 인덱싱하면 보통 O(1)에 가깝게 접근합니다.

  • 대표 구현: HashMap, LinkedHashMap, TreeMap
  • 언제 쓰나: “ID → 객체”, “코드 → 설정값”처럼 키 기반 조회가 핵심일 때

한눈에 보는 선택 가이드 (Java 컬렉션 비교)

아래 표는 “무엇을 보장하고, 어떤 작업이 빠른지” 기준으로 빠르게 고르기 위한 요약입니다.

타입 중복 허용 순서 보장 핵심 강점 대표 구현 주 사용처
Array(배열) O O 인덱스 접근 빠름, 단순 int[], String[] 고정 크기, 성능 민감, 원시 타입
List O O 유동 크기, 순차 처리 편함 ArrayList 화면 목록, 로그, 입력 순서 중요
Set X 구현체별 상이 유일성 보장 HashSet, LinkedHashSet 중복 제거, 권한/태그
Map 키 중복 X 구현체별 상이 키 기반 빠른 조회 HashMap ID 인덱스, 캐시, 룩업 테이블

컬렉션이 “의도를 드러내는” 방식 (흐름도)

배열에서 컬렉션으로 갈수록 “규칙을 코드로 강제”할 수 있습니다. 즉, 실수할 여지가 줄고 유지보수가 쉬워집니다.

flowchart LR
  A["Array: fixed size, index access"] --> B["List: ordered, allows duplicates"]
  B --> C["Set: unique elements"]
  B --> D["Map: key to value lookup"]
  C --> D

배열의 단순함에서 시작해, 요구사항(중복/키 조회)에 맞춰 List·Set·Map으로 확장되는 흐름입니다.

 

코드 예제 (배열 → List → Set → Map을 한 번에 실행)

아래 코드는 “배열로 시작한 데이터”를 List로 다루고, Set으로 중복을 제거한 뒤, Map으로 빠르게 조회하는 전형적인 흐름을 보여줍니다. Java 17에서 그대로 실행 가능합니다.

import java.util.*;
import java.util.stream.Collectors;

public class CollectionsIntroDemo {

    // Java 16+ record: 불변 데이터 모델에 적합 (Java 17 LTS에서 사용 가능)
    record User(long id, String name) {}

    public static void main(String[] args) {
        // 1) Array: 고정 크기, 단순 저장
        String[] rawNames = {"kim", "lee", "kim", "park", "lee"};

        // 2) List: 순서가 있고 중복 허용 (배열 -> 리스트)
        List<String> nameList = new ArrayList<>(Arrays.asList(rawNames));
        nameList.add("choi"); // 크기 유동
        System.out.println("List (ordered, duplicates ok): " + nameList);

        // 3) Set: 중복 제거 (리스트 -> 셋)
        // LinkedHashSet: "중복 제거 + 입력 순서 유지"가 필요할 때 유용
        Set<String> uniqueNames = new LinkedHashSet<>(nameList);
        System.out.println("Set (unique, keeps insertion order): " + uniqueNames);

        // 4) Map: 키 기반 조회 (ID -> User)
        List<User> users = List.of(
                new User(1L, "kim"),
                new User(2L, "lee"),
                new User(3L, "park")
        );

        Map<Long, User> userById = users.stream()
                .collect(Collectors.toMap(User::id, u -> u));

        System.out.println("Map lookup id=2: " + userById.get(2L));

        // Map이 특히 빛나는 지점: 리스트에서 매번 찾기 vs 맵으로 바로 찾기
        long targetId = 3L;

        User foundByLoop = null;
        for (User u : users) {
            if (u.id() == targetId) {
                foundByLoop = u;
                break;
            }
        }
        System.out.println("List search (loop) id=3: " + foundByLoop);

        User foundByMap = userById.get(targetId); // 보통 O(1)에 가까운 접근
        System.out.println("Map search (get) id=3: " + foundByMap);

        // 보너스: "중복 제거"를 Stream으로도 가능 (순서 유지)
        List<String> distinctInOrder = nameList.stream().distinct().toList();
        System.out.println("Stream distinct (keeps encounter order): " + distinctInOrder);
    }
}

실무 팁

💡 실무에서는
“중복 제거”를 List.contains()로 버티지 마세요. 데이터가 커질수록 contains()는 반복 호출 시 비용이 눈덩이처럼 커집니다. 중복이 요구사항이라면 자료구조를 Set으로 바꾸는 게 보통 더 단순하고 안전합니다(의도가 코드에 박힙니다).

💡 실무에서는
조회가 핵심이면 일찍 Map으로 인덱싱해 두는 습관이 도움이 됩니다. 예를 들어 “주문ID로 주문 찾기”, “상품코드로 가격 찾기”처럼 키가 명확한데도 List에서 매번 순회하면, 기능 추가와 함께 성능 문제가 뒤늦게 터지는 경우가 많습니다. 로딩 시점에 Map<Id, Entity>를 만들어두면 코드도 짧아지고 버그도 줄어듭니다.


핵심 요약: 배열은 단순하지만 확장성이 낮고, List는 순서/중복을 표현하며, Set은 유일성을 강제하고, Map은 키 기반 조회를 빠르게 만듭니다.
다음 글: #13 ArrayList vs LinkedList — 진짜 차이