지난번에 JCF에 대해 간단하게 알아보았다. 이번에는 JCF를 좀 더 자세히 알아보고, 스레드에 대해서도 알아보자.
| Map과 Collection
JCF의 계층 구조를 보면, JCF에서 Collection과 Map으로 나뉘어진 것을 볼 수 있다.
그리고 다른 자료 구조들은 Collection을 상속하고 있는데, Map만 Collection을 상속 받지 않는 것을 알 수 있다. 왜 그럴까?
Java에서 Map이 Collection 인터페이스를 상속 받지 않은 이유는, 두 인터페이스가 서로 다른 데이터 구조와 사용 목적을 표현하기 때문이다.
- Collection은 요소들의 집합으로, 단일한 요소를 다룬다. (Collection 인터페이스는 요소라는 개념을 전제로 설계 했다.)
- 개별 객체를 추가하거나 삭제하거나 탐색하는 데 초점이 맞춰져 있다.
- Map은 키-값 쌍을 다루는 인터페이스이다.
- 따라서, Map은 요소라는 것을 정의하기가 어렵다.
- Collection은 단일 객체를 다루는 반면, Map은 키-값이 함께 있는 복합 객체를 다뤄야 한다.
- 따라서, Map이 Collection을 상속받는다면, 기존 Collection의 메서드 설계가 Map의 특성과 충돌하게 된다.
예시를 들어보자, Collection.remove(Object o)를 Map에서 정의한다고 할 때, Map은 다음과 같은 선택지가 생길 수 있다.
- 키를 삭제해야 하는가?
- 키-값 쌍(Map.Entry)을 삭제해야 하는가?
- 값을 삭제해야 하는가?
위와 같은 선택지는 모호성(요소가 무엇인가?)이 생긴다. 따라서, Java에서는 이러한 모호성을 피하기 위해 Map에서 remove를 key를 기준으로 삭제하게 하였고, 이를 단순화하기 위해 Map과 Collection을 별도로 설계했다.
+) 혼동하지마! Collection Framework와 Collection Interface의 차이
Collection Framework는 Java에서 데이터를 저장하고 조작하는 데 사용되는 표준화된 데이터 구조와 관련 클래스 및 인터페이스의 집합이다.
- 인터페이스(Collection, List, Set, ...), 구현 클래스(ArrayList, HashMap, ...), 알고리즘으로 구성되어있다.
Collection Interface는 Java의 Collection Framework의 핵심 인터페이스 중 하나로, 단일 요소들의 집합을 나타낸다.
- List, Set, Queue 등
+) 혼동하지마! Collection과 Collections의 차이
Collection은 JCF의 최상위 인터페이스 중 하나로, 단일 요소들의 집합을 나타낸다.
- 데이터를 추가, 삭제, 검색, 순회 등의 작업을 위한 공통 동작을 정의한다.
- Collection은 List, Set, Queue 같은 데이터 구조의 상위 인터페이스이다.
반면에, Collections는 유틸리티 클래스이다.
- Collection과 그 하위 구현체(List, Queue, Set 등)를 조작하기 위한 정적 메서드들을 제공한다.
- java.util.Collections 패키지에 포함되어 있으며, 정렬, 검색, 동기화, 변환 등의 작업을 수행할 수 있다.
- Collections 클래스는 인스턴스를 생성하지 않으며, 모든 메서드는 정적(static)이다.
- 컬렉션 객체를 조작하기 위한 다양한 편리한 메서드를 제공합니다.
// ex.
List<Integer> nums = new ArrayList<>();
...
Collections.sort(nums);
| Iterable vs Iterator
예전에, Comparable과 Comparator의 차이에 대해서 알아본 적이 있었다.
- Comparable은 내가 다른 객체와 비교할 수 있는가?
- Comparator는 다른 객체들의 비교 도구
Comparable과 Comparator를 각각 implements를 하고, 재정의 해야하는 함수를 살펴보면 바로 알 수 있다.
// Comparable
public class Student implements Comparable<Student> {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Student other) {
return Integer.compare(this.age, other.age); // 나이 오름차순 정렬
}
...
}
// Comparator
public class Student {
private String name;
private int age;
...
}
...
public class Main {
public static void main(String[] args) {
...
Comparator<Student> ageComparator = new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
return Integer.compare(s1.getAge(), s2.getAge());
}
};
...
}
}
- Comparable은 다른 객체 1개만 받고, 자기 자신과 비교를 한다.
- Comparator는 2개의 객체를 받고, 해당 2개의 객체를 비교한다.
그럼, Iterable과 Iterator의 차이는 뭘까?
Comparable과 Comparator와 비슷하다.
- Iterable은 Collection이 반복 가능하도록 한다. (Collection은 단일 요소의 집합)
- 스스로가 Iterable할 수 있게 하는 것
- JCF의 계층 구조를 살펴보면, Collection 인터페이스를 상속하는 부분을 보면, Collection의 위에 Iterable이 있다.
- 따라서, Map을 제외한 다른 자료 구조(List, Set, ...)들은 Iterable 하다.
- Iterator는 Collection을 순회하면서 접근할 수 있도록 한다.
- 순회할 수 있는 도구이다.
- Iterator는 Iterable의 일부이다.
- Iterable 인터페이스를 구현한 객체에서 iterator() 메서드를 호출하면, Iterator 객체가 반환된다.
List<String> list = Arrays.asList("A", "B", "C");
// Iterable로서 iterator() 호출
Iterator<String> iterator = list.iterator();
// Iterator로 순회 작업 수행
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
| Thread
Thread는 실행 단위이다. Process가 실행 중인 프로그램의 인스턴스라면, Thread는 Process 내에서 실행되는 작업 단위이다.
Java에서 스레드를 만드는 방법에는 뭐가 있을까?
1) Thread 상속 받기
Thread 클래스를 상속 받아 Thread의 run() 메서드를 재정의한다.
public class MyThread extends Thread{
@Override
public void run() {
super.run();
}
}
2) Runnable 인터페이스 구현해서 만들기
자바는 단일 상속만 가능하다. 이미 다른 클래스를 상속 받고 있는데, Thread를 상속 받을 수는 없다.
따라서, 인터페이스를 사용해야 한다. (인터페이스는 여러 개를 구현할 수 있기 때문)
이때, Runnable을 사용해서 run() 메소드를 재정의한다.
public class MyRunnable implements Runnable {
@Override
public void run() {
}
}
3) Callable 인터페이스를 상속 받는다.
Callable 인터페이스를 상속 받아 call() 메서드를 구현한다.
public class Main {
public static void main(String[] args) {
// Callable 구현체 생성
Callable<String> callableTask = new Callable<String>() {
@Override
public String call() throws Exception {
return "Task executed successfully";
}
};
}
}
스레드를 여러 개 만들면 어떻게 될까?
스레드는 프로세스와 달리, 같은 프로세스 내에서는 메모리를 공유한다. 프로세스는 독립적으로 메모리와 자원을 할당 받는다. 따라서, 프로세스 여러 개를 만드는 것 보다는 가볍고 효율적이다.
하지만, 스레드도 결국에는 리소스를 소모하기 때문에 스레드가 무한대로 생성하게 되면 서버가 다운될 수 있다.
| 스레드 풀
스레드 풀은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고, 작업 큐에 들어오는 작업들을 하나씩 스레드가 맡아 처리하는 기법이다. 작업 처리 요청이 급격하게 증가해도, 작업 큐에 대기하다가 여유가 있는 스레드가 처리하는 방식이다.
따라서, 스레드의 전체 개수는 일정하며, 성능 저하도 일어나지 않는다.
네트워크 통신, 파일 읽기/쓰기, 데이터베이스 요청 등은 Blocking I/O 방식으로 동작하게 된다.
- I/O 작업이 완료될 때까지 해당 스레드가 대기 상태에 들어가는 것
- 따라서, 이 동안 스레드는 아무 작업도 하지 않으며, CPU 리소스를 활용하지 못한다.
웹 애플리케이션에서는 빈번하게 네트워크, 파일 입출력과 같은 작업을 하는데, 이는 주로 Blocking I/O 방식으로 동작한다.
따라서, 스레드가 적다면, 일부 스레드가 Blocking 상태에 있을 때 다른 작업을 처리할 스레드가 부족해질 수 있다.
- 애플리케이션의 응답성과 관련이 있어, 응답성을 저하시킨다.
이 때문에, 스프링과 같은 프레임 워크에서는 스레드 풀의 스레드 개수를 수백 개 이상으로 운영한다고 한다.
다만, 스레드 수가 늘어나면 Context Switching 비용이 증가한다.
하지만! Blocking I/O의 대기 시간은 Context Switching 비용보다 더 크다고 한다.
스레드 풀이 커질수록 Context Switching 비용은 증가하지만, 효율적으로 설계된 스레드 풀과 작업 큐로 이를 상쇄할 수 있기 때문에 수백 개의 스레드 개수를 만드는 것이다.
'💻 개발 > Java' 카테고리의 다른 글
동시성 프로그래밍 - 심화 (0) | 2024.12.19 |
---|---|
동시성 프로그래밍 - 기초 (0) | 2024.12.10 |
JCF와 스레드 - JCF 기초 (0) | 2024.12.04 |
Java 모의 면접 후기 (0) | 2024.12.01 |
11/25 - TIL : JPA와 N+1 (0) | 2024.11.25 |