Java에 대하여 - 4

SUM·2025년 1월 15일

Java

목록 보기
4/5

★컬렉션 기초

1. JCF(Java Collection FrameWork)란?

(1) 배경

예전에는 자료구조를 이용할 때 같은 역할이나 목적이 같지만 구현체의 메서드명이 달라 어려움을 느꼈다.(Vector, Array, HashTable)
즉, 확장이 쉽지 않았으며 표준 인터페이스를 구현하지 않았다.

(2) JCF란?

다수의 데이터를 쉽고 효과적으로 처리할 수 있는 표준화된 방법을 제공하는 클래스의 집합. 간단히 생각하면 자료구조 모음이라고 생각하면 된다.

(3) JCF의 계층 구조

크게 Iterable을 상속한 인터페이스와 Map을 상속한 인터페이스로 나눌 수 있다. Iterable이 포함된 인터페이스는 List, Queue, Set이다.

출처 : Java Collection FrameWork(JCF)란?

(4) Iterable vs Iterator

Java 컬렉션 프레임워크에서 IterableIterator 는 각각 “반복을 제공하는 객체”와 “실제 반복을 수행하는 도구”를 나타냅니다. 간단히 요약하면:

  • Iterable: “반복 가능한(순회할 수 있는)” 객체를 의미하며,
    - Iterator<T> iterator() 메서드를 통해 반복자를 생성할 수 있는 인터페이스
  • Iterator: “실제 반복을 수행”하는 인터페이스이며,
    - hasNext(), next(), remove() 등의 메서드를 통해 컬렉션 요소를 하나씩 순회합니다.

아래에서 구체적으로 살펴보겠습니다.


1. Iterable 인터페이스

  1. 정의

    public interface Iterable<T> {
        Iterator<T> iterator();
    }
    • Iterable을 구현한 클래스(예: Collection 인터페이스를 구현한 대부분의 컬렉션)는 iterator() 메서드를 제공해야 합니다.
    • for-each (향상된 for문) 구문이 가능해지려면 해당 클래스가 Iterable을 구현하고 있어야 합니다.
  2. 역할

    • “이 객체는 반복(순회)이 가능하다”고 명시하며, 반복자를 만들어 줄 수 있는 능력을 가집니다.
    • 대표적으로, List, Set 등 대부분의 컬렉션 클래스가 Iterable을 구현하고 있습니다.

2. Iterator 인터페이스

  1. 정의

    public interface Iterator<E> {
        boolean hasNext();
        E next();
        default void remove() { ... }
        ...
    }
    • Iterator는 실제로 컬렉션 요소를 하나씩 꺼내며 순회할 수 있도록 하는 메서드를 제공합니다.
  2. 주요 메서드

    • hasNext(): 다음 요소가 존재하는지 확인 (존재하면 true, 없으면 false)
    • next(): 다음 요소를 반환하고, 내부 포인터를 한 칸 이동
    • remove(): 현재 순회 중인 요소를 컬렉션에서 제거 (필요에 따라 사용)
  3. 역할

    • Iterable로부터 획득한 반복자(Iterator) 객체를 통해 실제 컬렉션을 순회
    • while (iterator.hasNext()) { ... } 와 같은 방식으로 요소를 하나씩 가져올 수 있음

3. 두 인터페이스의 차이점 정리

구분IterableIterator
역할반복 가능한 객체”임을 명시실제 반복을 수행하는 객체”
주요 메서드Iterator<T> iterator()hasNext(), next(), (optional) remove()
사용 패턴컬렉션이 Iterable을 구현하면,
for-each 구문 사용 가능
컬렉션을 순회할 때,
while (it.hasNext()) { ... } 식으로 사용
예시Collection, List, Set, Map (내부의 keySet 등)HashMapentrySet().iterator()
ArrayList.iterator()
  • Iterable 은 “이 객체는 순회할 수 있다”는 선언과 함께, 구체적인 반복자(Iterator)를 생성하는 iterator() 메서드만 제공합니다.
  • Iterator 는 “어떻게 순회할 것인가”에 대한 구체적인 메서드(hasNext(), next())를 가지며, 실제 순회를 제어합니다.

결론

  • Iterable 은 “반복자(Iterator)를 만들 수 있는” 객체를 표현하고,
  • Iterator 는 “반복을 수행”하는 객체입니다.

따라서 Iterable 을 구현한 컬렉션(예: ArrayList, HashSet 등)은 iterator()를 통해 Iterator 를 반환하고,
개발자는 반환된 IteratorhasNext()next()를 호출하며 데이터를 하나씩 순회할 수 있게 됩니다.

(5) Collection vs Collections

Java에서 CollectionCollections는 이름이 비슷하지만 전혀 다른 개념입니다.


1. Collection 인터페이스

  1. 정의

    • Collection은 자바 컬렉션 프레임워크최상위 인터페이스 중 하나입니다.
    • 여러 요소(elements) 를 한 곳에 모아두는 자료구조를 추상화합니다.
  2. 주요 특징

    • List, Set, Queue 등 다양한 하위 인터페이스를 통해 구체적인 자료구조를 정의합니다.
    • 모든 컬렉션 클래스(ArrayList, HashSet, LinkedList 등)는 Collection이나 그 하위 인터페이스를 구현합니다.
    • Iterator iterator()와 같은 메서드를 통해 요소들을 순회할 수 있습니다.
  3. 예시

    // Collection 인터페이스를 직접 사용하기보다는
    // 이를 구현한 구체 클래스(예: ArrayList)를 활용
    Collection<String> collection = new ArrayList<>();
    collection.add("Apple");
    collection.add("Banana");

2. Collections 클래스

  1. 정의

    • Collectionsjava.util 패키지에 포함된 유틸리티(utility) 클래스입니다.
    • 컬렉션 객체를 조작하기 위한 정적(static) 메서드들을 제공합니다.
  2. 주요 기능

    • 정렬: Collections.sort(list)
    • 검색: Collections.binarySearch(list, key)
    • 최솟값/최댓값: Collections.min(collection), Collections.max(collection)
    • 불변 컬렉션 생성: Collections.unmodifiableList(list), Collections.unmodifiableSet(set)
    • 동기화 래핑: Collections.synchronizedList(list), Collections.synchronizedMap(map)
    • 이 외에 스왑(swap), 반전(reverse), 랜덤 셔플(shuffle) 등 다양한 메서드 포함
  3. 예시

    List<String> list = new ArrayList<>();
    list.add("Apple");
    list.add("Banana");
    list.add("Cherry");
    
    // 정렬
    Collections.sort(list);  // ["Apple", "Banana", "Cherry"]
    
    // 반전
    Collections.reverse(list);  // ["Cherry", "Banana", "Apple"]

3. 차이점 요약

구분CollectionCollections
역할여러 요소를 모은 자료구조를 추상화하는 인터페이스컬렉션 관련 유틸리티 메서드를 모아둔 클래스
형태인터페이스클래스 (final 아님)
주요 기능요소 추가/삭제, 반복(iterator) 등
구현체마다 다름
정렬, 검색, 동기화, 불변 컬렉션 생성 등
모두 정적 메서드로 제공
사용 예List, Set, Queue 등의 근간Collections.sort(…), Collections.shuffle(…)
  • 정리
    • Collection 은 “List, Set, Queue 등 모든 컬렉션 계열의 뿌리가 되는 추상화”이고,
    • Collections 는 “이러한 컬렉션들을 편리하게 조작하기 위한 정적 메서드 모음 클래스”입니다.

2. List 인터페이스

(1) List 인터페이스란?

  • Java Collection Framework에서 제공하는 인터페이스 중 하나로, 순서가 있는 데이터의 집합을 다룹니다.
  • 중복된 요소를 허용하며, 각 요소는 삽입 순서에 따라 인덱스를 가집니다.
  • 주요 메서드:
    • add(E element): 요소 추가
    • get(int index): 특정 인덱스의 요소 반환
    • set(int index, E element): 특정 인덱스의 요소 변경
    • remove(int index): 특정 인덱스의 요소 제거
    • size(): 리스트의 크기 반환
    • contains(Object o): 특정 요소가 포함되어 있는지 확인
    • indexOf(Object o): 특정 요소의 첫 번째 인덱스 반환

(2) List 인터페이스의 주요 구현체

1. ArrayList

  • 내부적으로 배열을 사용하여 요소를 관리합니다.

  • 특징:

    • 인덱스를 기반으로 한 랜덤 접근 속도가 빠름.

    • 연속적인 데이터(증간에 빈공간x)의 리스트

    • 요소의 삽입 및 삭제는 느릴 수 있음(중간 요소가 빈 공간을 없애기 위해 이동해야 할 수도 있기 때문).

    • 데이터 추가, 삭제시 내부에서 동적으로 배열 길이 조절함

    • 내부적으로 Object[] 배열을 이용해 요소를 저장함

    • 크기가 고정되어 있는 배열과 달리 데이터의 크기에 따라 공간을 늘리거나 줄임

      1. 초기 용량

      • new ArrayList<>()로 생성할 경우, 내부적으로 기본 용량(default capacity) 10짜리 배열을 생성합니다.(Java 버전에 따라 조금씩 다를 수 있으나, 일반적으로 10으로 알려져 있습니다.)
      • 만약 생성자에 용량(capacity)을 지정했다면, 해당 용량의 배열을 생성합니다.

        2. 요소 추가(ensureCapacity)
      • add() 메서드 등을 통해 새로운 요소를 추가하려고 할 때, 현재 배열이 가득 차 있지 않다면 그냥 배열에 요소를 저장하고 끝납니다.
      • 현재 배열이 가득 차 있어 추가 공간이 필요한 경우, 내부적으로 ensureCapacity()라는 메서드를 호출하여 새 배열을 준비합니다.

        3. ensureCapacity()
      • 새 용량(newCapacity) 계산 : newCapacity=oldCapacity+(oldCapacity>>1)
      • 새 배열 할당 후 기존 배열 요소 복사 : Arrays.copyOf(...) 등의 메서드를 사용해 새 배열을 생성하고 기존 요소를 전부 복사합니다.
      • 내부 배열 참조 교체 : 복사가 끝나면 내부적으로 참조하던 배열을 새로 생성된 배열로 교체합니다.
    • 배열 공간이 꽉찰 때마다 배열을 복사하는 방식으로 늘리는데, 이때마다 지연

    • 스레드 안전이 필요한 경우, Collections.synchronizedList()를 사용하여 동기화된 리스트로 변환해야 합니다.

  • 사용 예 : 데이터의 삽입/삭제가 빈번하지 않고, 읽기 작업이 많은 경우.

2. LinkedList

  • 내부적으로 이중 연결 리스트(Doubly Linked List)로 요소를 관리합니다. 노드(객체) 끼리의 주소 포인터를 서로 가리키며 링크(참조)함으로써 이어지는 구조
  • 특징:
    • 삽입 및 삭제가 빠름(인덱스 변경 필요 없음).
    • 랜덤 접근 속도는 느림(순차적으로 탐색해야 하기 때문).
      : ArrayList는 내부 배열을 이용하므로, 인덱스를 이용한 랜덤 접근 시 O(1) 시간이 소요되지만, LinkedList는 연결 구조이므로, 특정 인덱스의 요소를 조회하려면 앞에서부터 순차적으로 찾아가야 합니다. 따라서 인덱스 접근은 O(n) 시간이 걸립니다.
    • LinkedList는 Deque 인터페이스도 구현합니다. 따라서 양쪽에서 삽입/삭제가 가능한 큐로서 사용이 가능합니다.
  • 사용 예: 삽입/삭제 작업이 빈번하고, 데이터의 양이 많은 경우.

3. Vector

  • ArrayList와 유사(동적 배열 기반)하지만, 동기화(synchronized) 처리가 되어 있어 멀티스레드 환경에서 안전합니다.
  • 특징:
    • ArrayList보다 느림(동기화로 인해 오버헤드 발생).
    • ArrayList는 내부 배열 용량이 부족해지면 기존 크기의 50%를 증가시키지만, Vector는 기본적으로 내부 배열의용량을 2배로 증가시킴
  • 사용 예: 멀티스레드 환경에서 동기화된 리스트가 필요한 경우.

4. Stack

  • Vector를 상속받아 구현된 클래스이며, LIFO(Last In, First Out) 구조를 따릅니다.
  • 주요 메서드:
    • push(item): 스택의 맨 위(top)에 새로운 요소를 추가한다.
    • pop(): 스택의 맨 위(top)에 있는 요소를 제거하고 반환한다.
    • peek(): 스택의 맨 위(top)에 있는 요소를 반환하지만 제거하지 않는다.
    • isEmpty(): 스택이 비어있는지 확인한다.
  • 사용 예
    • 웹 브라우저 방문기록(뒤로가기)
    • 실행 취소(undo) (되돌리기기능)
    • 역순 문자열 만들기

List 구현체 선택 기준

  • ArrayList: 데이터 읽기가 빈번하고 삽입/삭제가 적은 경우.
  • LinkedList: 데이터 삽입/삭제가 빈번하고 데이터 양이 많은 경우.
  • Vector: 멀티스레드 환경에서 동기화된 리스트가 필요한 경우.
  • Stack: LIFO 구조를 사용하는 경우.

3. Queue

  • FIFO(First In, First Out), 즉 선입선출 방식으로 동작
  • 가장 먼저 들어온 데이터가 가장 먼저 나감(줄 서서 기다리는 큐와 유사).
  • 주요 메서드
    - enqueue(item): 큐의 뒤(rear)에 새로운 요소를 추가한다.
    - dequeue(): 큐의 앞(front)에 있는 요소를 제거하고 반환한다.
    - peek(): 큐의 앞(front)에 있는 요소를 반환하지만 제거하지 않는다.
    - isEmpty(): 큐가 비어있는지 확인한다.
  • 사용 예시 :
    - 인쇄 대기열(프린터 큐): 먼저 들어온 문서부터 차례대로 인쇄한다.
    - 시스템에서의 작업 요청 처리: 요청이 들어온 순서대로 처리(선입선출).
    - 버퍼(Buffer) 처리: pending 중인 작업 중 가장 먼저 처리할 것부터 순차적으로 처리할 수 있음

4. Set

Java의 Set 인터페이스는 “중복을 허용하지 않는” 자료구조를 정의합니다. 리스트(List)가 인덱스를 통해 순서를 보장하고 중복을 허용하는 것과 달리, 집합(Set)은 동일한 요소가 한 번만 존재해야 한다는 특징이 있습니다. 또한, 구현 클래스에 따라 정렬 순서삽입 순서가 유지되는지 여부가 달라집니다.


1. Set 인터페이스의 특징

  1. 중복 요소를 허용하지 않음
    동일한 객체(또는 동등한 객체)가 이미 들어있다면, 새로운 요소 추가 시 무시(덮어쓰기)됩니다.
  2. 순서가 보장되지 않거나, 구현체에 따라 달라짐
    • 일반적인 HashSet요소의 순서를 보장하지 않음
    • LinkedHashSet삽입 순서를 유지
    • TreeSet정렬 순서(오름차순 등)를 유지

2. 주요 구현 클래스

2.1 HashSet

  • 내부적으로 해시 테이블(배열 + 연결 리스트 또는 트리 구조)을 사용하여 요소를 저장.
  • 순서를 보장하지 않고, 중복을 허용하지 않음.
  • 추가, 삭제, 검색 등의 일반적인 연산에서 평균적으로 (O(1)) 시간 복잡도.

2.2 LinkedHashSet

  • HashSet을 상속받아 동작하며, LinkedList 같은 이중 연결 구조를 추가로 이용해 삽입 순서를 유지.
  • 순서를 유지해야 하는 상황에서 사용.
  • 해시 구조를 사용하므로 성능 특성은 HashSet과 거의 유사하나, 연결 구조 관리 오버헤드가 약간 존재.

2.3 TreeSet

  • 이진 탐색 트리(레드-블랙 트리) 기반으로 동작.
  • 요소를 정렬된 상태로 저장(오름차순 등).
  • 검색, 추가, 삭제 연산의 시간 복잡도는 보통 (O(\log n)).
  • 순서를 정하기 위해 Comparable 혹은 Comparator를 통해 정렬 기준을 사용.

3. Set에서 중복 검사

3.1 HashSet에서의 중복 검사

  • 내부적으로 해시 함수(hashCode())와 동등성 검사(equals())를 통해 중복을 판별합니다.
  1. hashCode() 확인
    • 요소를 추가할 때, 객체의 hashCode() 값에 대응하는 해시 버킷(슬롯)에 저장합니다.
    • 같은 hashCode()가 나온다면 충돌(collision) 이 발생할 수 있습니다.
  2. equals() 메서드로 최종 확인
    • 해시 버킷 안에 이미 다른 요소가 있을 경우, equals()로 실제 동등성을 비교하여 같은 객체인지 확인합니다.
    • 만약 equals()true를 반환하면, 이미 존재하는 객체로 간주되어 추가되지 않습니다.

즉, hashCode()가 같고 equals()true이면 “동일 객체”로 판정하여 중복을 허용하지 않습니다.

3.2 TreeSet에서의 중복 검사

  • TreeSet은 해시가 아닌 정렬 트리 구조를 사용하므로, compareTo() (또는 Comparator.compare()) 메서드를 통해 중복 여부를 판단합니다.
  • 삽입 시 기존에 같은 정렬상 위치에 있는 노드가 동등하다고 판정되면(비교 결과가 0이라면), 중복이므로 추가되지 않습니다.

3.3 LinkedHashSet에서의 중복 검사

  • 기본 원리는 HashSet과 동일합니다.
  • 해시 구조를 이용하며, 내부에 연결 리스트를 사용해 요소의 삽입 순서를 기억합니다.

5. Map

Map키(key)와 값(value)의 쌍으로 데이터를 저장하는 자료구조를 정의하는 인터페이스입니다. 키를 통해 값을 빠르게 조회할 수 있고, 키는 중복될 수 없으나, 값은 중복될 수 있습니다. (즉, 한 Map 안에서 동일한 키는 한 번만 존재)


1. Map 인터페이스의 특징

  1. 키-값(key-value) 쌍으로 저장

    • 예) map.put(key, value) 형태로 저장하며, map.get(key)를 통해 조회합니다.
  2. 키는 중복 불가, 값은 중복 가능

    • 동일 키로 put하면, 기존 값이 덮어써짐(overwrite).
    • 값이 똑같아도, 키가 다르면 새 항목으로 인식합니다.
  3. Null 허용 여부

    • 구현체에 따라 다르지만, 일반적으로 HashMapnull 키null 값을 허용합니다.
    • Hashtablenull을 허용하지 않습니다.
  4. 순서 보장 여부

    • 일반적인 HashMap순서를 보장하지 않음.
    • LinkedHashMap삽입 순서를 보장.
    • TreeMap정렬된 순서(오름차순 등)를 보장.
  5. Collection 인터페이스 상속받지 않음

    • 데이터 구조의 개념 차이
      - Collection은 요소(element)의 모음입니다. (예: List, Set, Queue 등)
      - Map은 키(key)와 값(value)의 쌍으로 이루어진 엔트리(entry)의 모음입니다.

2. Map의 주요 구현 클래스

2.1 HashMap

특징

  • 가장 많이 사용되는 구현체.
  • 내부적으로 해시 테이블 구조를 사용해 를 저장하고, 대응되는 을 매핑.
  • 순서 보장 없음.
  • 평균적으로 put/get 연산은 (O(1))의 시간 복잡도.
    but, 해시 충돌 등이 과하게 발생하면 최악의 경우 O(n)로 성능 떨어짐
  • null 키null 값 모두 허용.

HashMap의 내부 동작 방식

1) 해시 함수(hashCode())
  • put 또는 get으로 입력되면, 우선적으로 키 객체의 hashCode()를 호출하여 해시값을 구합니다.
  • 이 해시값을 HashMap 내부의 배열(bucket array)의 인덱스와 매핑하여, 해당 인덱스 위치(버킷)에 키-값 쌍(Entry) 을 저장하게 됩니다.
2) 버킷(bucket) 구조
  • HashMap은 내부적으로 배열 + 연결 구조(Java 8 이후엔 트리 구조 병합)로 이루어진 버킷을 사용합니다.
  • 각 버킷(bucket)은 해당 인덱스에 속하는 Entry(노드) 들의 연결 리스트(혹은 트리)로 구성됩니다.
3) 해시 충돌 처리 (Collision Resolution)
  • 서로 다른 키들이 동일한 해시 인덱스를 가리키는 상황을 해시 충돌이라 합니다.
  • 자바 8부터는 연결 리스트 기반에서 일정 길이 이상의 충돌이 발생하면, 트리(레드-블랙 트리) 로 변경하여 충돌을 최소화하고, 탐색 성능을 향상시킵니다.

(1) 연결 리스트 (Chaining)

  • 버킷의 첫 노드(Entry)에서 next 포인터로 이어지는 연결 리스트 형태.
  • 충돌이 발생하면 리스트가 점점 길어지며, 각 노드를 순차적으로 탐색해야 합니다.

(2) 트리(Tree) 구조

  • Java 8 이상에서 연결 리스트 길이가 일정 수준(기본 8) 이상으로 늘어나면, 해당 버킷을 레드-블랙 트리로 바꿉니다.
  • 트리로 구성된 버킷은 탐색, 삽입, 삭제의 시간 복잡도가 (O(\log n)) 가 됩니다.
  • 충돌이 심하더라도 연결 리스트보다는 탐색 속도가 향상됩니다.

4) equals()로 최종 동등성 비교

  • 해시가 동일(혹은 충돌)한 노드가 여러 개 있으면, equals() 메서드를 통해 실제 동일 키인지를 판별합니다.
  • equals()true를 반환하는 노드를 찾아내면 해당 키로 간주하고, put은 값을 수정하고, get은 값을 반환합니다.

HashMap의 시간 복잡도

1) 평균 시간 복잡도
  • 일반적으로 해시 함수가 고르게 분포하고, 충돌이 적게 발생한다면,
    • 삽입(put), 탐색(get), 삭제(remove) 등이 평균적으로 (O(1)) 에 가깝게 처리됩니다.
  • 이는 대부분의 실제 사용 시나리오에서 매우 빠른 성능을 보이는 이유입니다.
2) 최악 시간 복잡도
  • 충돌이 매우 많이 발생하여, 하나의 버킷에 모든 요소가 몰리는 최악 상황일 경우, 연결 리스트나 트리 구조를 전부 탐색해야 합니다.
    • Java 7 이하: 연결 리스트로만 처리하므로, 노드 (n)개를 전부 순회해야 해서 최악 시간 복잡도가 (O(n)) 이 됩니다.
    • Java 8 이상: 일정 길이를 넘어가면 레드-블랙 트리로 변환하기 때문에, 트리 구조라면 최악 시간 복잡도는 (O(log n)) 로 좋아집니다.
      • 하지만, 매우 극단적인 경우(트리 변환도 제대로 안 되고, 해시 충돌이 심각하게 편향된 경우 등)에는 여전히 (O(n)) 로 볼 수 있습니다.

2.2 LinkedHashMap

  • HashMap을 상속받은 클래스이며, 해시 테이블 외에 이중 연결 리스트를 활용하여 삽입 순서를 유지합니다.
  • 순서가 중요한 경우(예: 캐시 구현) 사용.
  • 해시 구조를 쓰기 때문에, HashMap 대비 살짝 오버헤드가 있지만, 동일한 해시 성능 특성을 가집니다.
  • null 키, null 값 허용.

2.3 TreeMap

  • 내부적으로 이진 검색 트리(레드-블랙 트리)를 사용해 키를 정렬된 상태로 보관.
  • 정렬 순서(기본적으로 오름차순)를 유지하며, Comparator를 사용해 사용자 정의 정렬도 가능.
  • 평균적으로 put/get 연산은 (O(\log n))에 수행.
  • null 키를 허용하지 않습니다(NullPointerException 발생). 값은 null이 가능(하지만 관례상 잘 사용하지 않음).

2.4 Hashtable

  • 옛날(레거시) 클래스로, HashMap과 거의 동일한 해시 테이블 구조.
  • 차이점은 모든 메서드가 synchronized 처리가 되어 있어 멀티스레드 환경에서도 안전하게 사용할 수 있으나, 그만큼 성능이 떨어집니다.
  • null 키, null 값 모두 허용하지 않음.
  • 현재는 일반적으로 잘 사용되지 않으며, 스레드 안전이 필요하면 다른 방법(ConcurrentHashMap 등)을 씁니다.

2.5 ConcurrentHashMap

  • 멀티스레드 환경에서 안전하게 Map을 사용하기 위해 고안된 클래스.
  • 내부에서 세분화된 락(분할 락, segment-lock) 등을 사용하므로, Hashtable 보다 성능이 뛰어남.
  • null 키, null 값 허용하지 않음.

3. Map 사용 시 주의사항

  1. hashCode() & equals() (해시 기반 Map인 HashMap / LinkedHashMap 등)

    • 키 객체의 hashCode(), equals() 오버라이딩이 잘못되어 있으면, 정상적인 중복 판별이 안 될 수 있습니다.
  2. compareTo() or Comparator (TreeMap)

    • 키를 비교하기 위해 정렬 기준이 필요합니다.
    • Comparable 구현 또는 Comparator 제공이 적절하게 이루어져야 합니다.
  3. null 처리

    • 사용하는 구현체가 null 키, null 값을 허용하는지 확인해야 합니다.
    • HashMap/LinkedHashMap 은 허용, TreeMap 은 키에 대해서 불허.
  4. 스레드 안전성

    • 일반적인 HashMap, LinkedHashMap, TreeMap스레드 안전하지 않음.
    • 멀티스레드 환경에서는 ConcurrentHashMap 혹은 Collections.synchronizedMap(new HashMap<>) 활용 고려.

4. 결론

  • Map 인터페이스는 키-값 쌍을 효율적으로 저장하고 탐색하기 위한 자료구조입니다.
  • 주요 구현체:
    • HashMap: 가장 널리 쓰이며, 순서 보장 없음, 평균 접근 O(1).
    • LinkedHashMap: 삽입 순서 유지, 해시 테이블 기반.
    • TreeMap: 정렬된 순서 유지, 이진 검색 트리 기반, 접근 O((\log n)).
    • Hashtable: 레거시 동기화 Map, 잘 쓰이지 않음.
    • ConcurrentHashMap: 멀티스레드 안전 & 좋은 성능.

각 구현체의 특징(순서, 정렬, 스레드 안전 등)null 키/값 허용 여부를 고려하여 상황에 맞게 선택하는 것이 중요합니다.



★스레드

1. Java에서의 스레드

1. Java에서 스레드를 만들기

Java에서 스레드(Thread) 를 생성하는 대표적인 방법은 다음과 같습니다.

1) Thread 클래스 확장(상속)하기

  1. Thread 클래스를 상속받아 run() 메서드를 오버라이딩한다.
  2. 스레드 객체를 생성한 뒤 start() 메서드를 호출하면, 내부적으로 run() 메서드가 실행된다.
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running via Thread subclass.");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start(); // 별도의 스레드에서 run() 실행
    }
}

2) Runnable 인터페이스 구현하기

  1. Runnable 인터페이스의 run() 메서드를 오버라이딩한다.
  2. Thread 생성자에 Runnable 구현 객체를 넘겨준다.
  3. 생성된 스레드에서 start() 메서드를 호출한다.
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread is running via Runnable implementation.");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}

장점: 다른 클래스를 상속받아도 되므로, 클래스 상속 계층에 유연합니다.

=> Thread 클래스는 Runnable 인터페이스를 구현한 것이기 때문에 어느 것을 사용해도 거의 차이가 없다. 대신 Runnable 인터페이스를 구현하면 원하는 기능을 추가할 수도 있다. 이는 장점이 될 수도 있지만, 해당 클래스를 수행할 때 별도의 스레드 객체를 생성해야 한다는 점은 단점이 될 수도있다.

3) Callable 인터페이스 구현하기

  1. Callable<V> 인터페이스의 call() 메서드를 구현한다.
  2. 스레드 풀(ExecutorService)에서 해당 Callable을 실행하면, Future<V> 객체로 결과를 받거나 예외를 처리할 수 있다.
import java.util.concurrent.Callable;

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        // 스레드에서 수행할 로직
        return "Callable result";
    }
}

장점: Callable반환값을 가질 수 있고, 예외 처리도 가능하다는 점에서 Runnable과 다릅니다.


2. 스레드 풀이란?

1) 스레드 풀(Thread Pool)의 개념

  • 스레드 풀미리 일정 개수의 스레드를 생성해 두고, 작업(작업 단위, task)이 들어올 때마다 이 스레드를 재활용하여 실행하는 기법입니다.
  • Java에서는 주로 ExecutorServiceExecutors 유틸 클래스(예: Executors.newFixedThreadPool(int)) 를 사용하여 스레드 풀을 쉽게 구성할 수 있습니다.
ExecutorService executor = Executors.newFixedThreadPool(3); // 스레드 풀 3개

for (int i = 0; i < 10; i++) {
    executor.submit(new MyRunnable()); // Runnable 작업 실행
}

executor.shutdown(); // 작업 종료 후 스레드 풀 종료

2) 왜 스레드 풀을 사용할까?

  1. 스레드 생성 비용 절감

    • 스레드를 자주 생성/소멸하는 것은 많은 비용(시간, 메모리)을 소모합니다.
    • 스레드 풀은 한 번 생성된 스레드를 재사용하므로 오버헤드를 크게 줄일 수 있습니다.
  2. 시스템 자원 관리

    • 스레드를 무제한으로 생성하면, CPU 코어 수보다 많은 스레드가 동시에 실행 대기 상태가 되어 문맥 전환 비용(Context Switch Overhead) 이 커질 수 있습니다.
    • 스레드 풀의 스레드 개수를 적절히 제한하여 자원 사용을 효율적으로 관리할 수 있습니다.
  3. 작업(태스크) 구조화

    • 여러 작업을 큐(Queue) 에 넣고, 풀에 있는 스레드가 하나씩 가져다 처리하도록 구조화할 수 있습니다.
    • 작업의 우선순위, 지연 실행, 주기적 실행 등 다양한 패턴을 구현하기도 수월합니다.
  4. 코드 간결화 및 유지보수

    • ExecutorService, ScheduledExecutorService 등을 사용하면, 스레드 관리(생성/종료) 로직을 직접 작성하지 않아도 됩니다.
    • 코드가 간결해지고 유지보수가 수월해집니다.

결론

  • 스레드 생성 방법
    • Thread 클래스 상속
    • Runnable 인터페이스 구현
    • Callable 인터페이스 구현 후 스레드 풀에서 실행
  • 스레드 풀(Thread Pool)
    • 미리 만들어진 스레드를 재사용하여, 성능, 자원 관리 측면에서 이점을 가져옵니다.
    • Java에서는 ExecutorServiceExecutors 유틸 클래스를 통해 쉽게 구현 가능합니다.

이처럼 Java에서 스레드를 직접 만들 수도 있지만, 실제 프로젝트에서는 대체로 스레드 풀을 통해 작업을 처리하는 방식을 선호합니다. 이는 성능과 자원 활용, 코드 유지보수 측면에서 훨씬 효율적이기 때문입니다.


3. 스프링에서의 스레드

Java 웹 애플리케이션 서버(예: Tomcat, Jetty)나 스프링 프레임워크 환경에서 스레드 풀 크기가 수백 개 이상으로 설정되는 것은 동시성(Concurrency) 이 많이 요구되기 때문입니다. 비록 스레드가 많아질수록 문맥 전환 비용(Context Switching) 이 커지지만, 다수의 요청을 처리해야 하는 서버 환경에서는 이 비용보다 동시 처리로 인한 이점이 더 크게 작용하는 경우가 많습니다. 주요 이유를 살펴보면 다음과 같습니다:


1. I/O 바운드 작업이 대부분

  • 대부분의 웹 애플리케이션은 DB, 외부 API, 파일, 네트워크 등과의 I/O 연산을 포함합니다.
  • 스레드가 I/O를 기다리는 동안 실제 CPU는 놀고 있기 때문에, 한정된 소수 스레드만 두면 응답 대기 시간이 길어질 수 있습니다.
  • 충분한 수의 스레드가 있으면, 한 스레드가 DB나 외부 API를 기다리는 동안, 다른 스레드가 CPU를 활용해 다른 요청을 처리할 수 있습니다.

즉, I/O 대기 시간이 긴(Blocking I/O) 환경에서는 여러 스레드로 동시에 일을 시키는 것이 오히려 전체 처리량(Throughput)을 높입니다.


2. 요청 폭주(트래픽 스파이크)에 대한 대응

  • 실무 서비스에서는 사용자 트래픽이 순간적으로 폭증할 수 있습니다.
  • 스레드 풀의 크기를 작게 유지하면, 트래픽 증가 시 대부분의 요청이 대기하거나 타임아웃으로 실패할 가능성이 커집니다.
  • 적절히 큰 스레드 풀을 두면, 동시에 들어오는 요청 들을 어느 정도 빠르게 배분할 수 있어, 응답 시간을 낮추고 서버의 가용성을 높입니다.

3. CPU 코어 수 대비 ‘실제’ 필요한 스레드 수

  • 일반적으로, CPU 코어 수 이상으로 스레드를 늘리면 문맥 전환 비용이 증가합니다.
  • 하지만 I/O 대기 비중이 큰 웹 애플리케이션의 경우, CPU가 풀로 돌아가는 시간보다 대기 시간이 더 길기 때문에, 코어 수 대비 많은 스레드를 두어도 실제로 동시에 CPU를 점유하는 경우는 제한적입니다.
  • 결과적으로 컨텍스트 스위칭의 오버헤드가 생기긴 하지만, I/O 대기에 의해 생기는 병목을 해소해주는 이점이 더 커집니다.

4. 안정적인 요청 처리와 타임아웃 관리

  • 스레드 수가 너무 적으면, Queue가 금방 차거나 거절(reject) 정책이 발동되어 요청이 거부될 수 있습니다.
  • 스레드가 어느 정도 여유 있게 많으면, 갑작스러운 트래픽 증가에도 일정 수준의 요청을 모두 받아서 어느 정도 처리(or 대기)할 수 있습니다.
  • 이후에도 너무 오래 걸리는 요청은 타임아웃 정책을 통해 정리되므로, 한정된 스레드만 있을 때보다 안정적으로 서비스를 운영 가능합니다.

5. 적절한 선에서의 튜닝 필요

  • 실제 운영 환경에서는 무작정 스레드 풀을 크게 잡을 수는 없습니다. 너무 큰 스레드 풀은:
    • 지나치게 많은 문맥 전환 비용
    • 메모리 사용량 증가 (스택 메모리 등)
    • DB 커넥션 풀 등 외부 자원 고갈
      등이 문제가 될 수 있습니다.
  • 일반적으로는 CPU 코어 수, 서비스 특성(I/O vs CPU 바운드), 예상 트래픽, DB 풀 크기 등을 고려하여 적절한 스레드 풀 크기를 결정하고, 모니터링 결과에 따라 튜닝합니다.

결론

스레드 풀이 크면 컨텍스트 스위칭 비용이 증가하기는 하지만,

  • 웹 애플리케이션은 대부분 I/O 바운드이고,
  • 동시 접속(Concurrency)이 매우 중요하며,
  • 트래픽 스파이크 대응을 위해서,

문맥 전환 비용을 감수하더라도 많은 스레드를 두는 편이 전체적인 응답 지연과 처리율 측면에서 유리한 경우가 많습니다. 결국 애플리케이션 특성과 트래픽 패턴을 파악하고, 자원(메모리, DB 커넥션 등) 한계를 고려하여 스레드 풀 크기를 조정하는 것이 핵심입니다.

profile
백엔드 개발자 SUM입니다.

0개의 댓글