3주차 Unit 2.1 — 배열의 한계와 컬렉션의 등장

Psj·2026년 5월 18일

F-lab

목록 보기
78/240

Unit 2.1 — 배열의 한계와 컬렉션의 등장

F-LAB JAVA · 3주차 · Phase 2 · 컬렉션 프레임워크 전체 지도
🚀 Phase 2 시작 — 1·2주차에서 본 컬렉션의 전체 풍경 완성


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 배열의 본질컬렉션과의 결정적 차이 는?
  • 배열의 5가지 한계 는 무엇인가?
  • Collection 프레임워크 가 왜 등장했나? (Java 1.2, 1998년)
  • IterableCollectionList/Set/Queue 인터페이스 계층은?
  • Map 이 왜 Collection 의 자식이 아닌가?
  • List vs Set vs Queue vs Map 의 의미적 차이는?
  • 박승제씨가 1·2주차에서 본 컬렉션 위에 어떤 전체 지도 가 더해지나?
  • ILIC 코드에서 자료구조 선택의 출발점 은?

🎯 핵심 한 문장

컬렉션은 "배열의 한계를 메우기 위해 등장한 자료구조 + 알고리즘의 프레임워크" 다.
배열은 가장 기본이지만, 크기 고정 · 삽입/삭제 불편 · 단일 타입 등의 한계가 명확.
자바 1.2 (1998) 의 컬렉션 프레임워크는 이 한계를 인터페이스로 추상화 해서,
박승제씨가 매일 쓰는 List<Shipment> , Map<Long, Cargo> 같은 패턴의 토대를 만들었다.

비유 — 도구 상자의 진화

시스템비유
배열 (int[])단일 칸막이 상자 — 같은 크기, 같은 종류만
컬렉션 (List<Shipment>)모듈형 도구 상자 — 종류·크기 유연, 다양한 도구
List순서 있는 도구 (드라이버 1, 2, 3번)
Set중복 없는 도구 모음 (서로 다른 종류)
Queue순번 줄 (선착순 처리)
Map라벨이 붙은 서랍 (이름으로 찾기)

→ 배열은 하드웨어 자료구조, 컬렉션은 소프트웨어 추상화.


🧭 9개 섹션 로드맵

1. 배열의 본질 — 메모리 레벨 정리
2. 배열의 5가지 한계
3. 컬렉션 프레임워크의 등장 (Java 1.2, 1998)
4. 자바 컬렉션 계층 다이어그램
5. Collection 과 Map 의 분리
6. List/Set/Queue 의 의미적 차이
7. ILIC 실무 — 자료구조 선택 출발점
8. 흔한 실수 + 안티패턴
9. 면접 + 자기 점검

1️⃣ 배열의 본질 — 메모리 레벨 정리

1.1 배열의 정의

int[] arr = new int[10];   // 길이 10 인 int 배열
arr[0] = 100;              // 인덱스로 접근
arr[1] = 200;

배열의 본질:

  • 동일한 타입 의 요소들이
  • 연속된 메모리
  • 고정된 크기 로 저장된 자료구조

1.2 메모리 다이어그램

int[] arr = new int[5];
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
Heap 메모리:

  int[] 객체:
  ┌─────────────────────────────────────────────────────┐
  │ Header (Mark Word + Class Pointer)                  │  16 bytes
  ├─────────────────────────────────────────────────────┤
  │ Length: 5                                            │  4 bytes
  ├─────┬─────┬─────┬─────┬─────┬─────────────────────┤
  │ 10  │ 20  │ 30  │  0  │  0  │ Padding             │  4×5 + ?
  └─────┴─────┴─────┴─────┴─────┴─────────────────────┘
   [0]   [1]   [2]   [3]   [4]
   
   주소: 0xAA00, 0xAA04, 0xAA08, 0xAA0C, 0xAA10
   → 연속된 메모리

→ 박승제씨가 2주차 Unit 1.5 (Object Header) 에서 본 패턴과 동일.

1.3 배열의 4가지 강점

1. 빠른 인덱스 접근
   arr[i] → O(1)
   주소 계산: 시작 주소 + i × element_size
   
2. 메모리 효율
   - 헤더 외에 추가 메타데이터 없음
   - 연속 메모리로 캐시 효율 ↑
   - 작은 메모리 사용

3. 단순함
   - 자료구조의 기본
   - JVM 이 직접 지원
   - 모든 다른 자료구조의 토대

4. CPU 캐시 친화적
   - 연속 메모리 → L1/L2/L3 캐시 활용
   - 순회 매우 빠름

1.4 자바 배열의 특이점

// primitive 배열
int[] ints = new int[10];
double[] doubles = new double[5];

// 객체 배열
String[] strings = new String[10];
Shipment[] shipments = new Shipment[5];

// 다차원 배열 (배열의 배열)
int[][] matrix = new int[3][4];

박승제씨가 알아둘 것:

  • 배열도 객체 (Heap 에 저장)
  • length 필드 (메서드 아님)
  • 공변성 (covariance): String[]Object[] 로 취급
  • 제네릭과 다른 동작

1.5 ArrayList 와의 관계 (2주차 복습)

박승제씨가 2주차 Unit 5.2 에서 본 ArrayList:

public class ArrayList<E> {
    transient Object[] elementData;   // 내부는 배열!
    private int size;
    
    // ...
}

→ ArrayList 는 "배열을 감싼 동적 컬렉션".
→ 1.5배 확장으로 가변 크기 흉내.

1.6 ILIC 에서의 배열 vs 컬렉션

// 배열 사용 — 거의 안 함
public class ShipmentBatchProcessor {
    Shipment[] shipments = new Shipment[100];   // ❌ 어색
}

// 컬렉션 사용 — 표준
public class ShipmentBatchProcessor {
    List<Shipment> shipments = new ArrayList<>();   // ✓ 표준
}

박승제씨가 ILIC 에서 배열을 직접 쓸 일:

  • byte[], char[] (저수준 I/O)
  • 매우 작은 고정 크기 컬렉션 (String[] PORTS = {"BUSAN", "SEOUL"})
  • 성능 크리티컬 코드 (드물게)
  • 그 외엔 항상 컬렉션

1.7 자기 점검 답변

배열이 컬렉션보다 빠른 이유와 그래도 컬렉션을 쓰는 이유는?

:

  • 배열이 빠른 이유:

    • 연속 메모리 (캐시 효율)
    • 헤더 외 메타데이터 없음
    • JVM 네이티브 지원
    • O(1) 인덱스 접근
  • 그래도 컬렉션 쓰는 이유:

    • 가변 크기
    • 풍부한 API (add/remove/contains/sort 등)
    • 다양한 자료구조 (Set, Map, Queue)
    • 알고리즘 자동 처리
    • 코드 가독성 ↑

유지보수성 > 약간의 성능.


2️⃣ 배열의 5가지 한계

2.1 한계 1 — 크기 고정

int[] arr = new int[10];
arr[10] = 100;   // ❌ ArrayIndexOutOfBoundsException

배열은 생성 시 크기 결정, 이후 변경 불가.

해결책:

  • 더 큰 배열 새로 생성
  • 기존 데이터 복사
  • 기존 배열 GC 대상으로
int[] old = new int[10];
// ... 사용
int[] bigger = new int[20];
System.arraycopy(old, 0, bigger, 0, 10);   // 수동 복사

→ 매번 이런 코드는 비효율.
→ ArrayList 의 1.5배 확장은 이 작업을 자동화.

2.2 한계 2 — 삽입/삭제 메서드 없음

int[] arr = {1, 2, 3, 4, 5};

// 인덱스 2 에 99 삽입하려면?
// arr.insert(2, 99); ← ❌ 그런 메서드 없음

// 수동으로:
int[] result = new int[arr.length + 1];
System.arraycopy(arr, 0, result, 0, 2);
result[2] = 99;
System.arraycopy(arr, 2, result, 3, 3);
// result = {1, 2, 99, 3, 4, 5}

→ 매번 복잡한 로직 작성.
→ ArrayList 는 add(i, e) 한 줄.

2.3 한계 3 — 길이는 알지만 사용 중인 요소 수는 모름

String[] names = new String[10];
names[0] = "Alice";
names[1] = "Bob";

names.length;   // 10 (배열 크기)
// 실제 사용 중인 요소 수 (= 2) 는 직접 추적 필요

→ 자체 카운터를 직접 관리해야 함.
→ ArrayList 는 size() 메서드 제공.

2.4 한계 4 — 단일 자료구조

// 배열로 다른 자료구조 만들기 = 매우 복잡

// Stack 시뮬레이션
class IntStack {
    int[] data = new int[100];
    int top = -1;
    
    void push(int v) { data[++top] = v; }
    int pop() { return data[top--]; }
}

// Queue 시뮬레이션 — 더 복잡
// HashMap 시뮬레이션 — 매우 복잡

→ 자료구조마다 직접 구현.
→ 컬렉션 프레임워크는 표준 구현 제공.

2.5 한계 5 — 빈약한 API

int[] arr = {3, 1, 4, 1, 5};

// 정렬
Arrays.sort(arr);   // 그나마 Arrays 유틸리티

// 검색
int idx = Arrays.binarySearch(arr, 4);

// 포함 여부
// arr.contains(4);   ← ❌ 없음
boolean has = false;
for (int n : arr) {
    if (n == 4) { has = true; break; }
}

// 변환
// arr.map(...);     ← ❌ 없음

// 필터링
// arr.filter(...);  ← ❌ 없음

→ 배열은 자료를 담는 것 외에 거의 안 해줌.
→ 컬렉션은 풍부한 API + Stream 까지.

2.6 5가지 한계 정리

한계배열컬렉션
크기고정가변
삽입/삭제메서드 없음add/remove
사용 요소 수직접 추적size()
자료구조 종류1개다양 (List/Set/Map/Queue)
API빈약풍부 + Stream

2.7 자기 점검 답변

배열의 가장 치명적 한계는?

:
1. 크기 고정 — 동적 데이터 다루기 어려움
2. API 빈약 — 매번 직접 구현
3. 단일 자료구조 — Map, Set 등 별도 구현 필요

이 3가지가 가장 큰 한계.
→ 컬렉션 프레임워크가 이 모든 것을 해결.


3️⃣ 컬렉션 프레임워크의 등장 (Java 1.2, 1998)

3.1 역사적 배경

Java 1.0 (1996):
  - 배열만 있음
  - Vector, Hashtable (제한적)
  - 자료구조 부족

Java 1.1 (1997):
  - 약간의 개선
  - 여전히 배열 중심

Java 1.2 (1998) ★:
  - "Collection Framework" 도입
  - List, Set, Map 인터페이스 등장
  - ArrayList, LinkedList, HashMap, HashSet 등 구현체
  - Collections 유틸리티 클래스

Java 5 (2004):
  - Generic 추가
  - List<String> 같은 타입 안전성

Java 8 (2014):
  - Stream API
  - forEach, removeIf 등 default 메서드

Java 9+:
  - List.of(), Map.of() (불변 컬렉션)
  - 다양한 개선

→ Java 1.2 가 컬렉션의 현대적 시작.

3.2 설계 목표

Java 컬렉션 프레임워크 설계자 (Joshua Bloch) 의 목표:

1. 통일된 인터페이스
   → List, Set 등 공통 동작 추상화
   → 코드 일관성

2. 다양한 구현체
   → 같은 인터페이스에 여러 구현 (ArrayList vs LinkedList)
   → 성능 트레이드오프 선택

3. 알고리즘 표준화
   → Collections.sort(), Collections.shuffle()
   → 자료구조 무관 적용

4. 타입 안전성 (Java 5+)
   → 제네릭으로 컴파일 타임 검증
   → 런타임 ClassCastException 방지

5. 확장성
   → 사용자 정의 자료구조도 통합 가능
   → AbstractList 등 추상 클래스 제공

3.3 컬렉션 프레임워크의 가치

박승제씨가 ILIC 에서:

// 컬렉션 프레임워크 없으면:
public class ShipmentManager {
    Shipment[] shipments = new Shipment[1000];
    int size = 0;
    
    void add(Shipment s) {
        if (size >= shipments.length) {
            Shipment[] bigger = new Shipment[shipments.length * 2];
            System.arraycopy(shipments, 0, bigger, 0, size);
            shipments = bigger;
        }
        shipments[size++] = s;
    }
    
    // remove, contains, sort, ... 모두 직접 구현
}

// 컬렉션 프레임워크 사용:
public class ShipmentManager {
    private final List<Shipment> shipments = new ArrayList<>();
    // add, remove, contains 모두 사용 가능
}

코드량 1/10, 버그 가능성 1/100.
→ 박승제씨가 비즈니스 로직에 집중 가능.

3.4 컬렉션의 7가지 핵심 추상화

자바 컬렉션이 제공하는 추상화:

1. Iterable      — "순회 가능"
2. Collection    — "요소 모음"
3. List          — "순서 있는 모음"
4. Set           — "중복 없는 모음"
5. Queue         — "선입선출"
6. Deque         — "양방향 큐"
7. Map           — "키-값 매핑"

각각이 인터페이스 로 정의 → 다양한 구현체 가능.

3.5 자기 점검 답변

Java 컬렉션 프레임워크의 등장 이유와 가치는?

:
1. 배열의 5가지 한계 극복
2. 통일된 인터페이스 로 코드 일관성
3. 다양한 구현체 로 성능 트레이드오프 선택
4. 알고리즘 표준화 (sort, shuffle 등)
5. 타입 안전성 (제네릭, Java 5+)
6. 확장성 (사용자 정의 자료구조)

자바 개발자의 생산성 혁명.


4️⃣ 자바 컬렉션 계층 다이어그램

4.1 전체 계층 한눈에

Iterable<E>  (java.lang)
  │
  ▼
Collection<E>  (java.util)
  │
  ├── List<E>
  │   ├── ArrayList<E>
  │   ├── LinkedList<E>
  │   ├── Vector<E>  ★ (Java 1.0, legacy)
  │   │   └── Stack<E>  ★ (legacy)
  │   ├── CopyOnWriteArrayList<E>
  │   └── ImmutableList (Java 9+: List.of())
  │
  ├── Set<E>
  │   ├── HashSet<E>
  │   │   └── LinkedHashSet<E>
  │   ├── TreeSet<E>  (NavigableSet, SortedSet 구현)
  │   ├── EnumSet<E>
  │   └── ConcurrentSkipListSet<E>
  │
  └── Queue<E>
      ├── LinkedList<E>  (List 와 Queue 둘 다 구현)
      ├── PriorityQueue<E>
      ├── ConcurrentLinkedQueue<E>
      ├── BlockingQueue<E>  (interface)
      │   ├── ArrayBlockingQueue<E>
      │   └── LinkedBlockingQueue<E>
      └── Deque<E>  (interface)
          ├── ArrayDeque<E>
          ├── LinkedList<E>  (또 등장)
          └── ConcurrentLinkedDeque<E>


Map<K, V>  (java.util)  ★ Collection 아님!
  │
  ├── HashMap<K, V>
  │   └── LinkedHashMap<K, V>
  ├── TreeMap<K, V>  (NavigableMap, SortedMap 구현)
  ├── Hashtable<K, V>  ★ (Java 1.0, legacy)
  │   └── Properties  ★ (legacy)
  ├── ConcurrentHashMap<K, V>
  ├── ConcurrentSkipListMap<K, V>
  ├── EnumMap<K, V>
  ├── WeakHashMap<K, V>
  ├── IdentityHashMap<K, V>
  └── ImmutableMap (Java 9+: Map.of())

4.2 인터페이스만 추출

Iterable<E>
  └── Collection<E>
      ├── List<E>
      ├── Set<E>
      │   └── SortedSet<E>
      │       └── NavigableSet<E>
      └── Queue<E>
          └── Deque<E>

Map<K, V>
  └── SortedMap<K, V>
      └── NavigableMap<K, V>

인터페이스의 계층 이 핵심.
→ 구현체는 다양하지만 인터페이스로 추상화.

4.3 박승제씨가 매일 쓰는 인터페이스

// 변수 선언 시 인터페이스 우선
List<Shipment> shipments = new ArrayList<>();      // 구현체 ArrayList
Set<String> codes = new HashSet<>();                 // 구현체 HashSet
Map<Long, Cargo> cargoes = new HashMap<>();          // 구현체 HashMap

// 메서드 매개변수도 인터페이스
public void process(List<Shipment> shipments) { ... }
public Set<String> extractCodes(Collection<Shipment> input) { ... }

이유:

  • 구현체 교체 가능 (new ArrayList<>()new LinkedList<>())
  • 코드 유연성 ↑
  • 인터페이스 의존이 좋은 설계 원칙 (DI, 리스코프 등)

4.4 ILIC 코드 표준 패턴

박승제씨의 ILIC 코드에서:

@Service
public class ShipmentService {
    
    private final List<ShipmentListener> listeners = new ArrayList<>();
    private final Map<Long, Shipment> cache = new ConcurrentHashMap<>();
    private final Set<String> processedBlNos = ConcurrentHashMap.newKeySet();
    
    public List<Shipment> findByStatus(ShipmentStatus status) {
        return repository.findByStatus(status);
        // 반환 타입은 List<Shipment> (인터페이스)
        // 실제 구현체는 ArrayList (JPA 가 반환)
    }
}

인터페이스 의존 + 구현체 자유.

4.5 자기 점검 답변

자바 컬렉션 계층의 최상위 인터페이스는?

:

  • Iterable<E> (java.lang 패키지)
  • 그 아래 Collection<E> (java.util)
  • 그 아래 List, Set, Queue
  • Map별도 계층 (Collection 자식이 아님)

Iterable 을 구현하면 for-each 사용 가능.


5️⃣ Collection 과 Map 의 분리

5.1 Map 이 Collection 자식이 아닌 이유

박승제씨가 2주차 Unit 5.1 에서도 본 핵심 질문:

"왜 Map<K,V> 는 Collection<E> 를 상속하지 않는가?"

5.2 답 1 — 본질적 차이

Collection<E>:
  - 단일 요소 (E) 들의 모음
  - 각 요소가 자기 자체로 의미 있음
  - 예: List<Shipment> — Shipment 들의 모음
  
Map<K, V>:
  - 키 (K) 와 값 (V) 의 매핑
  - 키-값 쌍 (Entry) 이 단위
  - 예: Map<Long, Shipment> — id 로 Shipment 조회
  
공통 인터페이스가 어색한 이유:
  - Collection 의 add(E e) 가 Map 에선 의미 없음
  - Map 의 put(K, V) 는 Collection 에 없음
  - iterator() 가 Collection 은 E, Map 은 무엇?

단일 요소 vs 키-값 쌍 의 차이.

5.3 답 2 — 수학적 다른 추상화

Collection 의 본질:
  S = {e₁, e₂, e₃, ...}   ← 집합 / 시퀀스
  
Map 의 본질:
  f: K → V                  ← 함수 (사상)
  f(k₁) = v₁, f(k₂) = v₂

수학적으로:

  • Collection = 집합 또는 시퀀스
  • Map = 함수 또는 사상

→ 서로 다른 추상화.

5.4 답 3 — API 의 모순 회피

만약 Map 이 Collection 을 상속했다면:

// 가상의 잘못된 설계
Map<Long, Shipment> map = ...;
map.add(???);   // 무엇을? 키만? 값만? 둘 다?
map.contains(???);   // 키? 값? 엔트리?
map.remove(???);     // 모호

→ 모든 메서드가 모호해짐.
→ 분리가 명확한 설계.

5.5 그래도 연결되는 부분

Map 은 Collection 자식이 아니지만, 세 가지 view 제공:

Map<Long, Shipment> map = ...;

// 1. 키들의 집합 (Set)
Set<Long> keys = map.keySet();

// 2. 값들의 컬렉션 (Collection, 중복 가능)
Collection<Shipment> values = map.values();

// 3. 엔트리들의 집합 (Set<Map.Entry>)
Set<Map.Entry<Long, Shipment>> entries = map.entrySet();

→ View 를 통해 Collection 처럼 다룰 수 있음.
→ for-each 도 가능:

for (Map.Entry<Long, Shipment> entry : map.entrySet()) {
    // ...
}

5.6 자바 표준 라이브러리의 다른 예

java.util 의 다른 분리:
  - Iterator (순회 도구) vs Iterable (순회 가능)
  - Comparator (외부 비교) vs Comparable (자기 비교)
  - Function vs BiFunction
  
같은 패턴:
  - "비슷해 보이지만 본질이 다르면 분리"

→ 자바 설계의 일관된 철학.

5.7 ILIC 에서의 의미

// 박승제씨가 메서드 시그니처 만들 때:

// ❌ Collection 으로 통일 시도
public void process(Collection<?> data) { ... }   // 모호

// ✓ List 와 Map 분리
public void processShipments(List<Shipment> list) { ... }
public void processIndexed(Map<Long, Shipment> map) { ... }

// ✓ 또는 view 활용
public void processKeys(Set<Long> ids) { ... }
public void processValues(Collection<Shipment> shipments) { ... }

각 자료구조의 특성에 맞는 시그니처.

5.8 자기 점검 답변

Map 이 Collection 의 자식이 아닌 이유 3가지는?

:
1. 본질적 차이: 단일 요소 vs 키-값 쌍
2. 수학적 다른 추상화: 집합 vs 함수
3. API 의 모순 회피: add, contains 등이 Map 에선 모호

→ 그러나 view (keySet, values, entrySet) 로 Collection 처럼 다룰 수 있음.


6️⃣ List/Set/Queue 의 의미적 차이

6.1 의미적 차이 표

자료구조핵심 의미순서중복인덱스
List"순서 있는 리스트"✓ 삽입 순서✓ 허용
Set"중복 없는 집합"보통 X (LinkedHashSet 예외)
Queue"처리 대기 줄"처리 순서✓ 허용
Deque"양방향 큐"양 끝

의도 에 따라 선택.

6.2 List — 순서 있는 모음

List<Shipment> shipments = new ArrayList<>();

// 삽입 순서 보존
shipments.add(shipment1);
shipments.add(shipment2);
shipments.add(shipment3);

// 인덱스 접근
shipments.get(0);   // shipment1
shipments.get(2);   // shipment3

// 중복 허용
shipments.add(shipment1);   // 또 추가 가능

언제 List 를 쓰나:

  • 순서가 의미 있을 때 (실행 단계, 화물 순서 등)
  • 인덱스 접근 필요할 때
  • 중복 가능할 때

6.3 Set — 중복 없는 모음

Set<String> uniqueBlNos = new HashSet<>();

uniqueBlNos.add("BL-001");
uniqueBlNos.add("BL-002");
uniqueBlNos.add("BL-001");   // 중복 → 무시됨

uniqueBlNos.size();   // 2

// 포함 여부 빠름 (해시 기반 O(1))
uniqueBlNos.contains("BL-001");   // true

언제 Set 을 쓰나:

  • 중복 제거가 필요할 때
  • "있는지 없는지" 검사가 빈번할 때
  • 순서가 중요하지 않을 때

6.4 Queue — 처리 대기 줄 (FIFO)

Queue<Task> taskQueue = new LinkedList<>();
// 또는 ArrayDeque 권장 (2주차 Phase 5)

taskQueue.offer(task1);   // 끝에 추가
taskQueue.offer(task2);
taskQueue.offer(task3);

Task t = taskQueue.poll();   // 앞에서 꺼냄 → task1
Task t2 = taskQueue.poll();  // → task2

// 또는 peek (꺼내지 않고 보기)
taskQueue.peek();   // task3 (꺼내지 않음)

언제 Queue 를 쓰나:

  • 순차 처리 대기 (작업 큐, 이벤트 큐)
  • BFS 그래프 탐색
  • 메시지 큐 시뮬레이션

6.5 Map — 키-값 매핑

Map<Long, Shipment> shipmentById = new HashMap<>();

shipmentById.put(1L, shipment1);
shipmentById.put(2L, shipment2);

Shipment s = shipmentById.get(1L);   // O(1) 조회

언제 Map 을 쓰나:

  • 키로 빠른 조회가 필요할 때
  • 인덱싱이 필요할 때
  • 캐시 구현

6.6 네 가지 선택의 결정 트리

박승제씨의 자료구조 선택 트리:

1. 키로 값 찾는가?
   YES → Map
   NO  → 2

2. 중복 허용?
   NO  → Set
   YES → 3

3. FIFO 처리?
   YES → Queue
   NO  → List

6.7 ILIC 실전 사용 패턴

@Service
public class ShipmentService {
    
    // List — 순서 있는 운송 목록
    private final List<Shipment> recentShipments = new ArrayList<>();
    
    // Set — 중복 없는 처리된 ID
    private final Set<Long> processedIds = new HashSet<>();
    
    // Map — ID로 빠른 조회 캐시
    private final Map<Long, Shipment> cache = new HashMap<>();
    
    // Queue — 처리 대기 큐
    private final Queue<ShipmentEvent> eventQueue = new ConcurrentLinkedQueue<>();
}

→ 같은 데이터를 다양한 자료구조로 다룸.

6.8 자기 점검 답변

"중복 없는 운송장 번호 목록을 빠르게 검색" 하려면 어떤 자료구조?

:

  • Set (특히 HashSet)
  • 이유:
    • 중복 제거 (Set 의 특성)
    • 빠른 검색 (HashSet 의 O(1) contains)

만약 정렬도 필요하면:

  • TreeSet (정렬된 Set)

만약 삽입 순서도 유지하면:

  • LinkedHashSet

→ 같은 "중복 없는" 요구도 추가 조건에 따라 다른 선택.


7️⃣ ILIC 실무 — 자료구조 선택 출발점

7.1 자료구조 선택의 4단계 의사결정

박승제씨의 결정 프로세스:

Step 1: 데이터의 본질 파악
  - 키-값 매핑? → Map
  - 순서 있는 시퀀스? → List
  - 중복 없는 집합? → Set
  - 처리 대기? → Queue

Step 2: 추가 요구사항 확인
  - 정렬 필요? → TreeXxx
  - 삽입 순서 유지? → LinkedXxx
  - 멀티스레드 안전? → ConcurrentXxx
  - 불변? → List.of(), Set.of(), Map.of()

Step 3: 성능 트레이드오프 검토
  - 빈번 조회? → HashMap, HashSet
  - 빈번 삽입/삭제? → LinkedList (단, Phase 5 에서 본 캐시 효율도)
  - 인덱스 접근? → ArrayList

Step 4: 구현체 선택
  - List → ArrayList (기본), LinkedList (양쪽 작업), CopyOnWriteArrayList (멀티스레드)
  - Set → HashSet (기본), TreeSet (정렬), LinkedHashSet (순서 유지)
  - Map → HashMap (기본), TreeMap (정렬), ConcurrentHashMap (멀티스레드)
  - Queue → ArrayDeque (기본), PriorityQueue (우선순위), BlockingQueue (멀티스레드)

7.2 ILIC 의 일반적 패턴 90:9:1

박승제씨가 ILIC 코드에서 90% 사용:
  - ArrayList<E>
  - HashMap<K, V>
  - HashSet<E>

9% 사용:
  - ArrayDeque<E> (큐/스택 필요 시)
  - LinkedHashMap (LRU 캐시 등)
  - ConcurrentHashMap (멀티스레드)
  - TreeMap (정렬 키 필요)

1% 사용:
  - LinkedList (드물게)
  - Vector, Hashtable (절대 안 씀, legacy)
  - 특수 컬렉션 (EnumMap, WeakHashMap 등)

→ 박승제씨가 매일 결정하는 90:9:1.

7.3 표준 시나리오들

시나리오 1 — DB 조회 결과

// JPA Repository 반환
List<Shipment> shipments = repository.findAll();
// 내부 구현은 ArrayList
// 인터페이스는 List

시나리오 2 — 캐싱

// 단일 스레드 캐시
private final Map<Long, Shipment> cache = new HashMap<>();

// 멀티스레드 캐시
private final Map<Long, Shipment> sharedCache = new ConcurrentHashMap<>();

// 더 진보된 캐시
private final Cache<Long, Shipment> caffeineCache = 
    Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(Duration.ofHours(1))
        .build();

시나리오 3 — 이벤트 큐

// 단일 스레드 이벤트 큐
private final Queue<ShipmentEvent> eventQueue = new ArrayDeque<>();

// 멀티스레드 안전 이벤트 큐
private final Queue<ShipmentEvent> sharedQueue = new ConcurrentLinkedQueue<>();

// 블로킹 큐 (소비자가 대기)
private final BlockingQueue<ShipmentEvent> blockingQueue = 
    new LinkedBlockingQueue<>(1000);

시나리오 4 — 권한 집합

public class User {
    private Set<Permission> permissions;   // 중복 없는 권한
    
    public boolean hasPermission(Permission p) {
        return permissions.contains(p);   // O(1)
    }
}

시나리오 5 — 처리된 ID 추적

public class BatchProcessor {
    private final Set<Long> processedIds = new HashSet<>();
    
    public void process(Shipment s) {
        if (processedIds.contains(s.getId())) {
            return;   // 이미 처리됨
        }
        // ... 처리
        processedIds.add(s.getId());
    }
}

시나리오 6 — 그룹핑

Map<String, List<Shipment>> byRoute = shipments.stream()
    .collect(Collectors.groupingBy(Shipment::getRoute));
// 결과: 노선별로 그룹 지어진 Shipment 들

시나리오 7 — 정렬된 키

// 시간순 이벤트
TreeMap<LocalDateTime, ShipmentEvent> timeline = new TreeMap<>();

timeline.put(t1, event1);
timeline.put(t2, event2);

// 시간순으로 순회
timeline.forEach((time, event) -> { ... });

// 특정 시간 범위
SortedMap<LocalDateTime, ShipmentEvent> recent = 
    timeline.tailMap(LocalDateTime.now().minusDays(1));

7.4 ILIC 코드 리뷰 체크리스트

박승제씨의 컬렉션 사용 검토:

변수 선언:
  - 인터페이스 타입으로 선언?
  - 적절한 구현체 선택?
  - 초기 크기 지정 (큰 컬렉션)?
  
사용 패턴:
  - List 인데 contains 빈번? → Set 으로 변환?
  - List 인데 앞쪽 삽입? → Deque 검토?
  - Map 인데 정렬 필요? → TreeMap?
  - 멀티스레드 환경? → Concurrent 컬렉션?
  
메서드 시그니처:
  - 매개변수가 인터페이스?
  - 반환 타입이 인터페이스?
  - 불변 컬렉션 반환 (List.copyOf)?
  
성능:
  - 빈번 조회면 HashSet/HashMap?
  - 정렬 필요 시 적절한 자료구조?
  - 메모리 사용 적절?

7.5 자기 점검 답변

ILIC 에서 가장 자주 쓰는 자료구조 3가지는?

:
1. List — 도메인 객체 모음 (ArrayList)
2. Map<K, V> — 키로 조회 (HashMap, ConcurrentHashMap)
3. Set — 중복 제거, 빠른 검색 (HashSet)

→ 90% 가 이 3가지로 해결.


8️⃣ 흔한 실수 + 안티패턴

8.1 실수 1 — 배열을 직접 노출

// ❌ Entity 가 배열을 노출
public class Shipment {
    private Cargo[] cargoes;   // 배열
    
    public Cargo[] getCargoes() {
        return cargoes;   // 외부에서 변경 가능
    }
}

// ✓ 컬렉션 + 방어적 복사
public class Shipment {
    private final List<Cargo> cargoes = new ArrayList<>();
    
    public List<Cargo> getCargoes() {
        return List.copyOf(cargoes);   // 불변
    }
}

8.2 실수 2 — 구현체 타입으로 변수 선언

// ❌ 구현체로 선언
ArrayList<Shipment> shipments = new ArrayList<>();

// ✓ 인터페이스로 선언
List<Shipment> shipments = new ArrayList<>();

이유:

  • 구현체 교체 자유
  • 코드 유연성 ↑
  • DI 와의 일관성

8.3 실수 3 — Vector 사용

// ❌ Vector 사용
Vector<Shipment> shipments = new Vector<>();

문제:

  • Java 1.0 legacy
  • 모든 메서드 synchronized → 느림
  • 멀티스레드 환경에서도 ArrayList + 동기화 더 권장
// ✓ 멀티스레드 환경
List<Shipment> shipments = Collections.synchronizedList(new ArrayList<>());

// ✓ 또는 더 권장
List<Shipment> shipments = new CopyOnWriteArrayList<>();   // 읽기 위주

// ✓ 또는
List<Shipment> shipments = new ArrayList<>();   // ReadWriteLock 으로 직접

8.4 실수 4 — Stack 클래스 사용

// ❌ Stack (Legacy)
Stack<Frame> stack = new Stack<>();
stack.push(frame);
Frame f = stack.pop();

문제:

  • Vector 상속 → synchronized 비용
  • API 일관성 없음
// ✓ ArrayDeque
Deque<Frame> stack = new ArrayDeque<>();
stack.push(frame);
Frame f = stack.pop();

8.5 실수 5 — Hashtable 사용

// ❌ Hashtable (Legacy)
Hashtable<Long, Shipment> map = new Hashtable<>();

문제:

  • Java 1.0 legacy
  • 모든 메서드 synchronized → 느림
  • null 키/값 거부
// ✓ HashMap (단일 스레드)
Map<Long, Shipment> map = new HashMap<>();

// ✓ ConcurrentHashMap (멀티 스레드)
ConcurrentMap<Long, Shipment> map = new ConcurrentHashMap<>();

8.6 실수 6 — 배열을 List 로 잘못 변환

String[] arr = {"A", "B", "C"};

// ❌ 함정
List<String> list = Arrays.asList(arr);
list.add("D");   // UnsupportedOperationException!

Arrays.asList():

  • 고정 크기 List 반환
  • add/remove 불가
  • set/get 만 가능
// ✓ 가변 List 로 변환
List<String> list = new ArrayList<>(Arrays.asList(arr));

// ✓ 또는 Stream
List<String> list = Arrays.stream(arr).collect(Collectors.toList());

// ✓ 또는 Java 9+
List<String> immutable = List.of("A", "B", "C");

8.7 실수 7 — Map.get null 체크 누락

Map<Long, Shipment> cache = new HashMap<>();

// ❌ null 체크 누락
Shipment s = cache.get(id);
s.getBlNo();   // NPE 가능!

// ✓ null 체크
Shipment s = cache.get(id);
if (s != null) {
    s.getBlNo();
}

// ✓ Optional
Optional.ofNullable(cache.get(id))
    .ifPresent(shipment -> ...);

// ✓ getOrDefault
Shipment s = cache.getOrDefault(id, defaultShipment);

8.8 실수 8 — Iterator 안 쓰고 remove 시도

// ❌ ConcurrentModificationException
List<Shipment> list = new ArrayList<>();
// ... 데이터
for (Shipment s : list) {
    if (s.isExpired()) {
        list.remove(s);   // ❌ CME
    }
}

// ✓ Iterator
Iterator<Shipment> it = list.iterator();
while (it.hasNext()) {
    Shipment s = it.next();
    if (s.isExpired()) {
        it.remove();
    }
}

// ✓ Java 8+ removeIf
list.removeIf(Shipment::isExpired);

박승제씨가 2주차 Unit 6.2 에서 본 패턴.

8.9 ILIC 안티패턴 체크리스트

박승제씨가 ILIC 코드에서 피할 것:

레거시:
  - Vector → ArrayList 또는 CopyOnWriteArrayList
  - Hashtable → HashMap 또는 ConcurrentHashMap
  - Stack → ArrayDeque
  
배열 노출:
  - Entity 가 배열 반환 → 컬렉션
  - 매개변수가 배열 → 컬렉션
  
타입 선언:
  - 구현체로 변수 선언 → 인터페이스
  - 구현체로 메서드 시그니처 → 인터페이스
  
변환 함정:
  - Arrays.asList 후 add → new ArrayList<>()
  - List.of 변경 시도 → 불변 의식
  
순회 + 삭제:
  - for-each 안 list.remove → Iterator 또는 removeIf

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
배열과 컬렉션 차이?고정 vs 가변, 단일 자료구조 vs 다양
Java 컬렉션 프레임워크 등장?Java 1.2 (1998), Joshua Bloch
Map 이 Collection 자식 아닌 이유?단일 요소 vs 키-값 쌍 본질 차이
List/Set/Queue 차이?순서/중복/처리 의도
Vector 안 쓰는 이유?Legacy, synchronized 비용
Stack 클래스 안 쓰는 이유?Legacy, ArrayDeque 권장
Iterable 의 역할?for-each 가능
Arrays.asList 함정?고정 크기, add 불가
인터페이스로 변수 선언?구현체 교체 자유, 유연성
ConcurrentModificationException?순회 중 외부 변경 시. Iterator.remove

9.2 자기 점검 체크리스트

기본 이해

  • 배열의 5가지 한계를 안다
  • 컬렉션 프레임워크 등장 배경 (Java 1.2) 을 안다
  • Iterable → Collection → List/Set/Queue 계층을 안다
  • Map 이 별도 계층인 이유를 설명할 수 있다
  • List/Set/Queue/Map 의미적 차이를 안다

실전 적용

  • 시나리오에 맞는 자료구조 선택
  • 인터페이스로 변수 선언
  • Vector/Hashtable/Stack 피함
  • Arrays.asList 함정 회피
  • Iterator/removeIf 안전 삭제

면접 대비

  • 컬렉션 프레임워크 큰 그림 설명
  • Map 분리 이유 3가지
  • List/Set/Queue 선택 기준
  • Legacy 클래스 식별
  • 안티패턴 코드 리뷰

🎯 핵심 요약 — 3줄 정리

1. 컬렉션 = 배열의 한계를 메우는 프레임워크

  • 5가지 한계: 크기/삽입/사용 요소/자료구조/API
  • Java 1.2 (1998) 의 혁신
  • 인터페이스 + 다양한 구현체

2. 계층 구조

  • Iterable → Collection → List/Set/Queue
  • Map 은 별도 (키-값 쌍의 본질)
  • 인터페이스로 변수 선언 권장

3. ILIC 표준

  • 90% ArrayList/HashMap/HashSet
  • 9% Concurrent/Tree/ArrayDeque
  • 1% 특수
  • Legacy (Vector/Hashtable/Stack) 절대 사용 X

📚 다음으로...

Unit 2.2 — Set 3형제 (HashSet / TreeSet / LinkedHashSet)

이번 Unit에서 컬렉션 전체 지도를 봤다면, 다음은 Set 인터페이스의 3가지 구현체.

  • HashSet: 순서 없음, 빠른 검색
  • TreeSet: 정렬 순서, O(log n)
  • LinkedHashSet: 삽입 순서 유지
  • 각각의 메모리 구조와 선택 기준
  • ILIC 실무 사용 패턴

Phase 2 진행 상황

🚀 Phase 2 — 컬렉션 프레임워크 전체 지도
  ✅ Unit 2.1 배열의 한계와 컬렉션의 등장 ← 여기
  ⏭ Unit 2.2 Set 3형제 (HashSet/TreeSet/LinkedHashSet)
  ⏭ Unit 2.3 List 3형제 (ArrayList/LinkedList/Vector)
  ⏭ Unit 2.4 Queue (FIFO 자료구조)
  ⏭ Unit 2.5 Map 5형제
  ⏭ Unit 2.6 컬렉션 전체 지도 정리

3주차 누적 진행

✅ Phase 1 — Pass by Value (1.1 ~ 1.3 완주)
🚀 Phase 2 — 컬렉션 프레임워크 (1/6 진행)
⏭ Phase 3-10

총: 4/43 Unit 작성 (약 9%)
profile
Software Developer

0개의 댓글