✒️ 0. 들어가기 전
정적 배열과 동적 배열에 대해 다뤄보자.
메모리 관리와 성능의 균형에 관한 이야기이다.
일반적으로 "배열"이라고하면 정적 배열을 의미한다.
그렇기에 보통 "동적 배열"은 명확히 "동적 배열"이라고 이야기한다.
배열은 프로그래밍에서 가장 기본적이면서도 강력한 자료구조 중 하나이다.
앞서 말했듯 크게 정적 배열과 동적 배열로 나눌 수 있는데,
이 두 가지 형태는 각각 고유한 특성과 용도를 가지고 있다.
이 글에서는 정적 배열과 동적 배열의 특징, 장단점, 그리고 실제 사용 사례를 깊이 있게 살펴보겠다
✒️ 1. 정적 배열 (Static Array)
정적 배열은 컴파일 시점에 크기가 결정되는 배열이다.
C++에서는 주로 C 스타일의 배열 선언(int arr[10])이나 std::array를 사용하여 구현한다.
💡 특징
- 고정된 크기: 한 번 선언된 크기는 변경할 수 없다.
- 스택 메모리 사용: 일반적으로 스택에 할당되어 빠른 접근 속도를 제공한다.
- 범위 검사 없음: 인덱스 접근 시 범위를 벗어나도 컴파일러가 검사하지 않는다.
💡 장점
- 빠른 접근 속도: 메모리에 연속적으로 저장되어 있어 캐시 효율성이 높다.
- 간단한 구현: 복잡한 메모리 관리가 필요 없다.
💡 단점
- 크기 변경 불가: 실행 중 크기를 변경할 수 없어 유연성이 떨어진다.
- 메모리 낭비: 필요 이상의 크기로 선언하면 unused 메모리가 발생한다.
💡 사용 예시
cppCopyint staticArray[5] = {1, 2, 3, 4, 5};
std::array<int, 5> stdArray = {1, 2, 3, 4, 5};
(vector와는 달리 메서드가 없다.)
✒️ 2. 동적 배열 (Dynamic Array)
동적 배열은 실행 시간에 크기를 변경할 수 있는 배열이다.
C++에서는 주로 std::vector를 사용하여 구현한다.
💡 특징
- 가변 크기: 실행 중에 크기를 조절할 수 있다.
- 힙 메모리 사용: 동적으로 할당된 메모리를 사용한다.
- 자동 메모리 관리: 벡터 객체가 소멸될 때 자동으로 메모리를 해제한다.
💡 장점
- 유연성: 필요에 따라 크기를 조절할 수 있어 메모리 효율성이 높다.
- 편리한 사용: push_back(), pop_back() 등의 메서드를 제공한다.
- push_back(): 벡터의 끝에 요소를 추가
- pop_back(): 벡터의 마지막 요소를 제거 등의 메서드
💡 단점
- 상대적으로 느린 접근 속도: 포인터를 통한 간접 접근으로 인해 정적 배열보다 약간 느릴 수 있다.
- 메모리 재할당: 크기가 증가할 때 새로운 메모리 할당과 복사가 필요할 수 있다.
💡 사용 예시
cppCopystd::vector dynamicArray = {1, 2, 3};
dynamicArray.push_back(4); // {1, 2, 3, 4}
✒️ 3. 정적 배열과 동적 배열
💡 성능 고려사항
1. 메모리 접근에서 바라보자.
- 정적 배열은 연속된 메모리에 직접 접근하므로 캐시 친화적이다.
2. 이번에는 크기가 변화하는 관점에서 바라보자.
- 동적 배열의 크기 변경 시 발생하는 재할당은 비용이 크다.
3. 초기화 관점에서는?
- 정적 배열은 컴파일 시점에 초기화되어 런타임 오버헤드가 없다.
💡 사용 예시
- 정적 배열: 크기가 고정된 데이터 (예: 요일, 달 이름)
- 동적 배열: 사용자 입력에 따라 변하는 데이터 (예: 동적으로 추가되는 목록)
✒️ 4. Java에서의 동적/정적 배열
C++ 언어의 특성상 동적 / 정적 배열을 알아보는 것에 용이해서 예시를 C++ 로 들었지만,
나는 JAVA/SpringBoot 개발자로써 Java에서도 한 번 알아보자.
💡 동적 배열
Java에서 정적 배열은 고정된 크기를 가진 기본적인 배열 구조이다.
- 선언 방법
javaCopyint[] staticArray = new int[5];
// 또는
int[] staticArray = {1, 2, 3, 4, 5};
당연히 크기가 고정되어 있어 변경 불가한 특징이 있다.
인덱스를 통한 빠른 접근 가능하고, 기본 타입과 객체 모두 저장 가능하다.
💡 정적 배열
Java에서 동적 배열은 주로 ArrayList 클래스를 통해 구현된다.
선언 방법
javaCopyArrayList<Integer> dynamicArray = new ArrayList<>();
크기를 동적으로 조절 가능하다는 특징이 있다.
요소 추가/제거가 용이하며, 객체만 저장 가능하다. (기본 타입은 래퍼 클래스 사용)
- 테이블 연결 필드에서의 활용 예시
public class User {
private int id;
private String name;
private ArrayList<Integer> postIds; // 사용자가 작성한 게시글 ID 리스트
public User() {
this.postIds = new ArrayList<>();
}
public void addPost(int postId) {
this.postIds.add(postId);
}
public ArrayList<Integer> getPostIds() {
return new ArrayList<>(postIds); // 방어적 복사
}
}
📝 면접 예상 질문
Q: 대용량 데이터를 처리할 때 ArrayList와 LinkedList 중 어떤 것을 선택하시겠습니까? 그 이유는?
A: 대용량 데이터의 경우 주로 ArrayList를 선택합니다. 데이터 접근이 빈번할 경우 O(1)의 시간 복잡도로 빠른 접근이 가능하기 때문입니다. 단, 중간 삽입/삭제가 매우 빈번하다면 LinkedList를 고려할 수 있습니다.
Q: 멀티스레드 환경에서 ArrayList를 안전하게 사용하는 방법은?
A: Collections.synchronizedList()를 사용하거나 CopyOnWriteArrayList를 사용할 수 있습니다. 동시성 요구사항에 따라 선택합니다.
Q: ArrayList에서 요소를 자주 검색해야 한다면 어떤 방법을 사용하시겠습니까?
A: 정렬된 데이터라면 Collections.binarySearch()를 사용합니다. 그렇지 않다면 HashMap을 고려할 수 있습니다.
Q: ArrayList와 배열을 함께 사용할 때의 주의점은?
A: ArrayList를 배열로 변환할 때 toArray() 메서드를 올바르게 사용해야 합니다.
또한, 배열을 ArrayList로 변환할 때는 Arrays.asList()의 제한사항을 이해해야 합니다.
-> ArrayList를 배열로 변환 시 list.toArray(new Type[0])를 사용해 타입 안정성을 확보합니다.
배열을 ArrayList로 변환 시 Arrays.asList()는 고정 크기 List를 반환하므로,
완전한 ArrayList를 원한다면 new ArrayList<>(Arrays.asList(array))를 사용합니다.
기본 타입 배열 사용 시 주의가 필요하며, 잦은 변환은 성능 저하를 일으킬 수 있습니다.
또한, 널 요소 처리에 주의해야 합니다.
Q: 객체의 리스트를 다룰 때 메모리 관리를 위해 어떤 점을 고려해야 할까요?
A: 사용이 끝난 객체는 명시적으로 null 처리하여 가비지 컬렉션을 돕고, 필요 없는 참조를 제거하여 메모리 누수를 방지해야 합니다.
Q. 로또번호 7개를 셔플할 때 어떤 자료구조로 구축해야 하며 로직은 어떻게 될까요?
A. (로직을 먼저 떠올리자.)
로또번호 7개 셔플에 필요한 로직은 딱 두가지 참조와 swap입니다. 7개의 수 중 자유롭게 랜덤적으로 2개의 수를 참조하고 그 2개의 수를 swap하는 로직이 필요합니다.
(그리고 플로우)
즉 이는 다음과 같은 플로우를 기반으로 배열이란 자료구조를 사용해야 한다는 답을 갖게 됩니다.
1. 삽입이나 삭제, 탐색이 필요하지 않습니다.
2. 로또번호는 순차적으로 구성되어있기 때문에 이에 관한 자료구조는 배열,
연결리스트가 있습니다.
3. 스택과 큐를 생각할 수 있지만 자유로이 랜덤적으로 2개를 참조해 swap을 해야하고
이 때문에 랜덤접근이 불가능한 자료구조는 배제하게 됩니다. 이 때문에 연결리스트
또한 배제 됩니다.
4. 그래서 참조(random access)에 O(1)인 배열을 사용하는 것으로 결정됩니다.