Java 애플리케이션의 성능을 최적화하는 데 있어 컬렉션 프레임워크의 효율적인 사용은 매우 중요합니다.
대용량 데이터를 처리할 때 적절한 컬렉션 선택과 최적화 기법은 애플리케이션의 성능을 크게 향상시킬 수 있습니다.
적절한 컬렉션 타입 선택
컬렉션 타입 | 최적 사용 사례 | 성능 특성 |
---|---|---|
ArrayList | 빈번한 인덱스 접근 필요 시 | 접근: O(1), 삽입/삭제: O(n) |
LinkedList | 빈번한 삽입/삭제 필요 시 | 삽입/삭제: O(1), 접근: O(n) |
HashMap | 키 기반 빠른 검색 필요 시 | 검색/삽입/삭제: 평균 O(1) |
HashSet | 중복 없는 고유 요소 저장 시 | 검색/삽입/삭제: 평균 O(1) |
TreeMap | 정렬된 키-값 쌍 필요 시 | 검색/삽입/삭제: O(log n) |
TreeSet | 정렬된 고유 요소 필요 시 | 검색/삽입/삭제: O(log n) |
초기 용량 설정 최적화
컬렉션의 초기 용량을 적절히 설정하면 동적 확장으로 인한 성능 저하를 방지할 수 있습니다.
1
2
3
4
|
// 초기 용량 설정 예시
ArrayList<String> list = new ArrayList<>(10000); // 예상 요소 수에 맞게 초기화
HashMap<String, Integer> map = new HashMap<>(10000, 0.75f); // 초기 용량과 로드 팩터 설정
|
cs |
반복 처리 최적화
컬렉션 순회 방식에 따라 성능이 크게 달라질 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// 비효율적인 방식
for (int i = 0; i < list.size(); i++) { // 매 반복마다 size() 호출
process(list.get(i));
}
// 최적화된 방식
int size = list.size(); // size() 한 번만 호출
for (int i = 0; i < size; i++) {
process(list.get(i));
}
// 향상된 for문 (가독성 좋음)
for (String item : list) {
process(item);
}
// 스트림 API 활용 (함수형 접근)
list.stream()
.filter(item –> item.length() > 3)
.forEach(this::process);
|
cs |
함수형 프로그래밍과 스트림 활용
Java 8부터 도입된 스트림 API는 컬렉션 처리를 간결하고 효율적으로 만들어 줍니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 명령형 방식
List<String> filtered = new ArrayList<>();
for (String s : strings) {
if (s.length() > 3) {
filtered.add(s.toUpperCase());
}
}
// 스트림 API 활용
List<String> filtered = strings.stream()
.filter(s –> s.length() > 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
// 병렬 스트림 활용 (멀티코어 활용)
List<String> filtered = strings.parallelStream()
.filter(s –> s.length() > 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
|
cs |
메모리 효율성 향상
메모리 사용량을 최적화하여 가비지 컬렉션 부하를 줄일 수 있습니다.
1
2
3
4
5
6
7
8
9
|
// 원시 타입 사용
int[] primitiveArray = new int[1000]; // 더 효율적인 메모리 사용
// 래퍼 클래스 사용
Integer[] wrapperArray = new Integer[1000]; // 추가 오버헤드 발생
// 전문화된 컬렉션 라이브러리 활용 (예: Trove, FastUtil)
TIntArrayList tList = new TIntArrayList(1000); // 원시 타입 전용 컬렉션
|
cs |
객체 풀링 활용
자주 생성되고 삭제되는 객체의 경우, 객체 풀링을 통해 성능을 향상시킬 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
public class ObjectPool<T> {
private List<T> available = new ArrayList<>();
private List<T> inUse = new ArrayList<>();
private Supplier<T> supplier;
public ObjectPool(Supplier<T> supplier, int initialSize) {
this.supplier = supplier;
for (int i = 0; i < initialSize; i++) {
available.add(supplier.get());
}
}
public synchronized T acquire() {
if (available.isEmpty()) {
available.add(supplier.get());
}
T instance = available.remove(available.size() – 1);
inUse.add(instance);
return instance;
}
public synchronized void release(T instance) {
inUse.remove(instance);
available.add(instance);
}
}
// 사용 예시
ObjectPool<StringBuilder> pool = new ObjectPool<>(StringBuilder::new, 10);
StringBuilder sb = pool.acquire();
try {
sb.append(“Hello”).append(” World”);
// 작업 수행
} finally {
pool.release(sb);
}
|
cs |
불변 컬렉션 활용
스레드 안전성이 필요한 경우 불변 컬렉션을 사용하면 동기화 오버헤드 없이 안전하게 데이터를 공유할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
|
// Java 9 이상
List<String> immutableList = List.of(“A”, “B”, “C”);
Map<String, Integer> immutableMap = Map.of(“A”, 1, “B”, 2, “C”, 3);
// Java 8 이하
List<String> immutableList = Collections.unmodifiableList(Arrays.asList(“A”, “B”, “C”));
Map<String, Integer> map = new HashMap<>();
map.put(“A”, 1);
map.put(“B”, 2);
map.put(“C”, 3);
Map<String, Integer> immutableMap = Collections.unmodifiableMap(map);
|
cs |
동시성 컬렉션 활용
멀티스레드 환경에서는 동시성 컬렉션을 사용하여 성능을 향상시킬 수 있습니다.
1
2
3
4
5
6
7
|
// 동기화된 컬렉션 (성능 저하 가능성)
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
// 동시성 컬렉션 (더 나은 성능)
List<String> concurrentList = new CopyOnWriteArrayList<>();
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
|
cs |
실전 예제: 대용량 데이터 처리 최적화
다음은 대용량 사용자 데이터를 처리하는 최적화된 예제입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
public class UserProcessor {
private static final int BATCH_SIZE = 1000;
public List<UserDTO> processLargeUserList(List<User> users) {
return users.parallelStream()
.filter(User::isActive)
.collect(Collectors.groupingBy(user –> user.getId() % BATCH_SIZE))
.values()
.parallelStream()
.flatMap(batch –> processBatch(batch).stream())
.collect(Collectors.toList());
}
private List<UserDTO> processBatch(List<User> batch) {
// 배치 단위로 처리
return batch.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
private UserDTO convertToDTO(User user) {
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setUsername(user.getUsername());
// 필요한 변환 작업 수행
return dto;
}
// 성능 측정 유틸리티 메서드
public void measurePerformance(List<User> users) {
long start = System.currentTimeMillis();
List<UserDTO> result = processLargeUserList(users);
long end = System.currentTimeMillis();
System.out.println(“Processed “ + result.size() + ” users in “ + (end – start) + “ms”);
}
}
|
cs |
고급 최적화 기법: 커스텀 해시 함수 구현
해시 기반 컬렉션(HashMap, HashSet)의 성능은 해시 함수의 품질에 크게 의존합니다.
효율적인 해시 함수를 구현하면 충돌을 최소화하고 성능을 향상시킬 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public class OptimizedKey {
private final String value;
public OptimizedKey(String value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OptimizedKey that = (OptimizedKey) o;
return Objects.equals(value, that.value);
}
@Override
public int hashCode() {
// 효율적인 해시 함수 구현
int hash = 7;
for (int i = 0; i < value.length(); i++) {
hash = 31 * hash + value.charAt(i);
}
return hash;
}
}
|
cs |
Collections 유틸리티 클래스 활용
Java의 Collections 클래스는 컬렉션 작업을 위한 다양한 유틸리티 메서드를 제공합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
import java.util.*;
public class CollectionsUtilExample {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(5);
numbers.add(3);
numbers.add(8);
numbers.add(1);
// 정렬
Collections.sort(numbers);
System.out.println(“Sorted List: “ + numbers);
// 이진 검색
int index = Collections.binarySearch(numbers, 3);
System.out.println(“Index of 3: “ + index);
// 역순 정렬
Collections.reverse(numbers);
System.out.println(“Reversed List: “ + numbers);
// 최소/최대값 찾기
int min = Collections.min(numbers);
int max = Collections.max(numbers);
System.out.println(“Min: “ + min + “, Max: “ + max);
// 동기화된 컬렉션 생성
List<Integer> synchronizedList = Collections.synchronizedList(numbers);
// 불변 컬렉션 생성
List<Integer> unmodifiableList = Collections.unmodifiableList(numbers);
}
}
|
cs |
Java 컬렉션 프레임워크를 최적화하여 사용하면 애플리케이션의 성능을 크게 향상시킬 수 있습니다.
적절한 컬렉션 선택, 초기 용량 설정, 효율적인 반복 처리, 함수형 프로그래밍 활용 등의 기법을 통해 더 효율적이고
확장 가능한 애플리케이션을 개발할 수 있습니다.