1. 프롤로그 - 컴퓨터 메모리 속의 "아파트 단지"
자료구조의 조상님이자, 모든 개발자가 가장 먼저 배우는 배열(Array)입니다. 단순해 보이지만, 이 녀석의 "메모리 연속성(Contiguous Memory)"이라는 특성을 완벽히 이해해야 고성능 애플리케이션을 만들 수 있습니다.
배열은 메모리상의 연속된 땅을 미리 분양받는 것과 같습니다. "나 여기서부터 저기까지 100평 쓸 거야! 아무도 건들지 마!"
2. 조회(Read)의 제왕 - O(1)과 수학적 원리
배열이 사랑받는 유일하고도 강력한 이유는 조회 속도가 깡패라는 점입니다. 데이터가 100만 개가 있어도, 100만 번째 데이터를 찾는 속도는 1번째 데이터를 찾는 속도와 똑같이 즉시(Instant)입니다. 이것이 가능한 이유는 "탐색"을 하지 않고 "계산"을 하기 때문입니다.
주소 계산의 마법 (Pointer Arithmetic)
컴퓨터는 배열의 시작 주소만 알면, 수학 공식 하나로 원하는 위치를 즉시 계산합니다.
공식:
Target Address = Start Address + (Index × Data Size)
예를 들어, int arr[5]가 메모리 주소 1000번지에서 시작한다고 가정합시다. (int는 4바이트)
arr[0]-> 1000 + (0 × 4) = 1000번지arr[1]-> 1000 + (1 × 4) = 1004번지arr[2]-> 1000 + (2 × 4) = 1008번지- ...
arr[999]-> 1000 + (999 × 4) = 4996번지
역무원이 5호차 좌석을 찾을 때 1호차부터 걸어가지 않는 것과 같습니다. "한 칸에 20미터니까 5호차는 100미터 지점이네" 하고 순간 이동하는 것과 같습니다. 이것이 바로 랜덤 액세스(Random Access)의 위력입니다.
3. 삽입/삭제의 지옥 - O(N)의 비극
하지만 빈틈없는 연속성은 치명적인 약점이 됩니다. 중간에 누가 끼어드는 꼴을 용납하지 못합니다.
상황 - 영화관 새치기
영화관 좌석이 꽉 차 있는데, 갑자기 3번째 자리에 친구를 앉히고 싶습니다.
- 3번째 사람부터 100번째 사람까지 전원이 일어나야 합니다.
- 한 칸씩 옆으로 이동(Shift)해야 합니다. (우르르 쾅쾅)
- 빈자리가 생기면 그제야 앉습니다.
데이터가 100만 개라면? 100만 번의 이동 연산이 발생합니다.
- 맨 뒤 추가(
push): 빠름 (O(1)) - 그냥 뒤에 의자 하나 놓으면 됨. - 중간/맨 앞 추가(
insert): 개느림 (O(N)) - 전원 이동. - 삭제(
delete): 마찬가지로 빈자리를 채우기 위해 전원이 당겨 앉아야 함 (O(N)).
그래서 데이터의 추가/삭제가 빈번한 게시판 댓글이나 채팅 로그 같은 곳에는 배열을 쓰면 안 됩니다. (이럴 땐 연결 리스트를 씁니다.)
4. 숨겨진 무기 - 캐시 지역성 (Cache Locality)
실무에서 가장 중요한 고난이도 질문입니다. "이론상으로는 연결 리스트(Linked List)가 삽입/삭제가 O(1)이라 빠른데, 왜 실제로는 배열(Vector)을 많이 쓰나요?"
정답은 CPU 캐시(Cache) 때문입니다. CPU는 메모리(RAM)에서 데이터를 가져올 때, 달랑 하나만 가져오지 않습니다. RAM은 너무 느리기 때문이죠. "어차피 주인님이 옆에 있는 데이터도 찾겠지?"라고 예측하고 주변 한 덩어리(Cache Line, 보통 64바이트)를 미리 다 퍼옵니다(Prefetching).
메모리 시각화
Array (행복한 CPU)
[Data1][Data2][Data3][Data4]...
-> 붙어 있으니까 한 번에 캐시로 로딩됨!
-> Data2 부를 때 이미 캐시에 있음 (Cache Hit)
-> 초고속 처리
Linked List (슬픈 CPU)
[Data1] ----> (저 멀리 산너머) [Data2] ----> (강 건너) [Data3]
-> 흩어져 있음.
-> Data2 부를 때마다 RAM에 다시 다녀와야 함 (Cache Miss)
-> CPU가 멍때리는 시간 발생 (Stall)
이 차이 때문에 실제 벤치마크를 돌려보면 배열이 연결 리스트보다 수십 배에서 수백 배 빠를 수 있습니다. 하드웨어 친화적인 코딩이 무엇인지 보여주는 가장 좋은 예시입니다.
5. 보안 이슈 - 버퍼 오버플로우 (Buffer Overflow)
배열은 해커들이 가장 좋아하는 먹잇감입니다. 특히 C/C++ 처럼 메모리를 직접 다루는 언어에서, 경계 검사(Boundary Check)를 하지 않을 때 발생합니다.
char buffer[8]을 선언했습니다. (8칸짜리 배열)- 메모리 구조상, 이 배열 바로 뒤에는 함수 리턴 주소(Return Address)가 저장되어 있습니다.
- 해커가 8글자가 아니라 100글자를 입력합니다.
- 앞의 8칸은 정상적으로 채워지지만, 넘치는 데이터가 뒤의 리턴 주소를 덮어씌웁니다.
- 해커는 리턴 주소를 "자신의 악성 코드가 있는 주소"로 바꿔치기합니다.
- 함수가 끝나는 순간, 프로그램은 해커의 명령대로 움직이게 됩니다. (System Hacked)
최신 OS는 이를 막기 위해 카나리아(Canary) 값 심기, ASLR 등 온갖 방어 기법을 쓰지만, 근본적인 해결책은 Java, Python, Rust 같이 인덱스 검사를 강제하는 언어를 쓰는 것입니다.
6. 동적 배열(Dynamic Array)의 비밀
"파이썬 리스트나 자바 ArrayList는 크기가 무한대잖아요? 배열 아니라면서요?" 아닙니다. 걔네들도 내부적으로는 고정 크기 배열을 씁니다. 다만 "이사(Migration)"를 잘할 뿐입니다.
ArrayList의 성장 과정 (Growth Strategy)
- 처음엔 크기 10짜리 배열을 만듭니다.
- 데이터가 꽉 차면?
- 크기 20짜리(2배) 새 배열을 몰래 만듭니다.
- 기존 데이터 10개를 새 집으로 복사(Copy)합니다.
- 헌 집을 버립니다.
이 과정은 O(N) 비용이 듭니다. 하지만 아주 가끔 일어나므로, 평균을 내보면(Amortized Analysis) 여전히 O(1)입니다.
- Java: 1.5배씩 늘립니다.
- C++ (std::vector): 2배씩 늘립니다.
- Golang: 2배씩 늘리다가, 커지면 1.25배로 줄입니다. (메모리 아끼려고)
Tip: 여러분이 만약 담을 데이터 개수를 대충 안다면(예: 100만 개), new ArrayList<>(1000000) 처럼 초기 크기를 지정해주세요. 이사 비용을 아껴서 성능을 획기적으로 높일 수 있습니다.
7. 2차원 배열의 행우선 vs 열우선 뜯어보기
2차원 배열 arr[100][100]을 반복문으로 돌 때, 어떤 순서가 빠를까요?
// A방식: 행 우선 (Row-Major)
for(int r=0; r<100; r++)
for(int c=0; c<100; c++) arr[r][c]...
// B방식: 열 우선 (Column-Major)
for(int c=0; c<100; c++)
for(int r=0; r<100; r++) arr[r][c]...
C/C++/Java/C#은 행 우선(Row-Major) 언어입니다. 메모리에 [0,0], [0,1], [0,2]... 순서로 저장됩니다.
그래서 A방식이 캐시 히트율이 높아 훨씬 빠릅니다. B방식은 캐시를 계속 엇나가게(Cache Miss) 만듭니다.
반대로 Fortran, MATLAB, R, Julia 같은 과학 계산용 언어는 열 우선(Column-Major)입니다. 여기서는 B방식이 빠릅니다. 데이터 분석가들이 Python(행 우선)으로 넘어올 때 가장 많이 하는 실수입니다.
8. 메모리 하드웨어의 물리적 진실 한 걸음 더
우리가 흔히 "배열이 빠르다"고 할 때, 그 "빠름"의 근원이 도대체 어디서 오는지 물리 레벨에서 파헤쳐봤습니다. 이 내용을 알면 왜 개발자가 하드웨어를 이해해야 하는지 알게 됩니다.
1) DRAM의 작동 원리 (커패시터의 눈물)
우리가 쓰는 메인 메모리(RAM)는 대부분 DRAM (Dynamic RAM)입니다. DRAM의 각 비트(0 또는 1)는 커패시터(Capacitor)라는 아주 작은물통에 전기를 채우는 방식으로 저장됩니다. 문제는 이 물통에 구멍이 뚫려있다는 것입니다. 가만히 놔두면 전기가 줄줄 샙니다(Leakage). 그래서 컴퓨터는 1초에 수십 번씩 전기를 다시 채워주는 작업을 합니다. 이것을 리프레시(Refresh)라고 합니다. 이 리프레시 작업 때문에 CPU가 메모리에 접근하려고 할 때 "잠깐만! 지금 물 채우는 중이야!"라며 기다려야 하는 상황이 발생합니다. 배열을 쓰면 데이터를 연속적으로 읽기 때문에, 메모리 컨트롤러가 "어차피 계속 읽을 거니까 미리미리 준비해두자"는 최적화를 할 수 있습니다.
2) SRAM과 캐시의 속도 차이
CPU 바로 옆에 붙어있는 캐시 메모리는 SRAM (Static RAM)입니다. 얘는 커패시터(물통)가 아니라 플립플롭(Flip-Flop)이라는 회로를 씁니다. 전기가 새지 않아서 리프레시가 필요 없고, 속도가 빛의 속도에 가깝습니다. 하지만 트랜지스터가 6개나 들어가서 덩치가 크고 비쌉니다. 그래서 용량을 작게 만듭니다.
배열을 사용한다는 것은, 이 비싸고 귀하신 SRAM(캐시)을 100% 활용하겠다는 서약과 같습니다. 반면 연결 리스트를 쓰면, 비싼 SRAM을 놔두고 느려터진 DRAM(메인 메모리)까지 매번 다녀와야 합니다. 이것은 마치 최고급 스포츠카(CPU)를 타고 꽉 막힌 시골길(DRAM 접근)을 달리는 것과 같습니다. 배열을 쓰는 것은 스포츠카를 위해 고속도로(Sequential Access)를 뚫어주는 행위입니다.
3) 페이지 폴트(Page Fault)와 배열
운영체제는 메모리를 페이지(Page, 보통 4KB) 단위로 관리합니다. 배열은 데이터가 옹기종기 모여 있어서, 페이지 하나만 메모리에 올리면 수천 개의 데이터를 처리할 수 있습니다. 하지만 연결 리스트는 노드 하나가 여기 있고, 다음 노드는 저기 멀리 다른 페이지에 있을 수 있습니다. 재수 없으면 노드 하나 읽을 때마다 하드디스크에서 페이지를 새로 로딩해야 하는 페이지 폴트(Page Fault)가 발생합니다. 페이지 폴트는 CPU 입장에서는 거의 영겁의 시간(수 밀리초) 동안 멈춰 있어야 하는 대재앙입니다. 대용량 처리를 할 때 배열이 압도적인 이유가 바로 이 OS 페이지 관리 효율성 때문입니다.
9. 초고성능의 세계 - GPU와 SIMD
배열이 왜 AI(인공지능) 시대의 주인공인지 아시나요? 바로 SIMD (Single Instruction, Multiple Data) 기술 때문입니다.
1) CPU의 한계
일반적인 코드는 이렇게 동작합니다.
a[0] + b[0] 계산 (1 사이클)
a[1] + b[1] 계산 (1 사이클)
...
100번 반복하면 100 사이클이 걸립니다. (Scalar Operation).
2) SIMD의 마법 (벡터 연산)
최신 CPU(Intel AVX, ARM NEON)와 GPU는 이렇게 동작합니다.
"야, a[0]~a[7]까지 8개랑, b[0]~b[7]까지 8개 한꺼번에 가져와."
그리고 "더해!"라는 명령 한 번에 8개의 덧셈을 동시에 끝냅니다. (Vector Operation).
이게 가능하려면 데이터가 메모리에 완벽하게 붙어있어야(Contiguous) 합니다.
중간에 구멍이 있거나(Linked List), 주소가 흩어져 있으면 SIMD 유닛은 작동을 멈춥니다.
3) GPU는 배열 먹는 괴물
NVIDIA GPU가 하는 일이 뭘까요? 화면의 픽셀(배열) 수백만 개를 동시에 계산하는 것입니다. 그래서 GPU 메모리(VRAM)는 배열 처리에 목숨을 걸었습니다. 여러분이 딥러닝을 하거나 고성능 게임을 만들고 싶다면, 객체지향(OOP)이니 뭐니 하는 추상은 잠깐 잊고, 데이터를 어떻게 배열로 쫙 펼칠지(Flatten) 고민해야 합니다. 이것이 Data-Oriented Design (데이터 지향 설계)의 핵심입니다.
10. 철학 - 단순함이 복잡함을 이긴다
최신 언어들이 Set, Map, Stream 등 화려한 자료구조를 제공하지만,
결국 하드웨어 레벨에서 실행되는 것은 배열입니다.
컴퓨터 구조의 역사는 "어떻게 하면 배열을 더 빨리 돌릴까?"를 연구해온 역사와 같습니다.
화려한 추상화 뒤에 숨겨진 기계적 공감을 잊지 마세요.
튜링상을 받은 전설적인 컴퓨터 과학자들은 여전히 비트와 배열을 사랑합니다.
11. 요약 - 배열을 써야 할 때
- 데이터 개수가 정해져 있을 때 (좌석 예매, 픽셀 데이터)
- 순차적으로 데이터를 훑어야 할 때 (이미지 처리, 행렬 연산)
- 조회(Lookup)가 삽입보다 훨씬 많을 때
세상은 복잡한 자료구조를 좋아하지만, 실제로 CPU가 가장 사랑하고 가장 빠른 자료구조는 예나 지금이나 배열입니다. 단순함이 복잡함을 이깁니다.
Array: The Fastest and Stiffest Data Structure (Definitive Guide)
10. Advanced: Array vs Garbage Collector
One reason High-Performance languages (C++, Rust) love Arrays vs Java/Python/JS is Garbage Collection (GC).
- Array of Objects (Java):
Object[]. The array just holds references (pointers). The actual objects are scattered in the Heap.- GC has to mark/sweep every single object individually.
- This causes massive GC Pauses.
- Structure of Arrays (Game Dev): Keep data flat.
int[] hp,float[] x,float[] y.- This is one solid block of memory.
- GC treats it as one object. Scans it instantly.
- Result: Zero GC lags in your game loop.
11. Functional Programming Perspective
In FP (Haskell, Lisp, React Redux), we prefer Immutability.
Arrays are Mutable by nature.
Changing arr[0] = 99 destroys history.
- Persistent Data Structures: Instead of an Array, FP uses Tries or Linked Lists sharing structure.
- JavaScript:
const newArr = [...oldArr, newItem].- Safety: ✅ (No side effects).
- Performance: ❌ (O(N) copy every time).
- Solution: Use libraries like
Immer.jsorImmutable.jswhich use Structural Sharing to mimic Array speed without the copying cost.
12. Modern CPU Branch Prediction
Arrays allow CPUs to guess correctly.
When iterating for (int i=0; i<1000; i++), the Branch Predictor says "He will likely loop again."
This makes the pipeline efficient.
Linked Lists with while(node.next) are harder to predict if nodes are scattered.
The CPU pipeline stalls, waiting for memory.
1. Prologue: The Apartment Complex of Memory
Imagine a Luxury Apartment Complex. Each unit is identical in size. The unit numbers are sequential (101, 102, 103...). This is an Array.
It reserves a Contiguous Block of memory. "I claim memory from address 0x100 to 0x200! Hands off!" Because the land is reserved in one piece, it has unique strengths and weaknesses.
2. King of Read: O(1) with Math
Why is Array access instant (O(1))?
Why is getting arr[10000] just as fast as arr[0]?
Because it uses Math, not searching.
The Formula: Pointer Arithmetic
The CPU doesn't scan. It calculates the memory address directly using this formula:
Target Address = Start Address + (Index * Data Size)
If arr starts at memory address 1000 and holds 4-byte integers:
arr[0]: 1000 + (0 * 4) = 1000arr[1]: 1000 + (1 * 4) = 1004arr[2]: 1000 + (2 * 4) = 1008- ...
arr[500]: 1000 + (500 * 4) = 3000
The CPU teleports to address 3000. This is Random Access.
Calculations take constant time O(1), regardless of the index size.
3. Hell of Insert/Delete: O(N)
Arrays hate intruders. They are like a tight line of soldiers. There is no empty space between them.
The "Movie Theater" Scenario
Imagine a row of 100 people sitting in a theater. You want to sit in seat #3. But it's occupied. And you insist on sitting there.
- Person at #100 must move to #101.
- Person at #99 must move to #100.
- ...
- Person at #3 must move to #4.
- Finally, you sit at #3.
This is Shifting.
To insert one item at the beginning (index 0), N items must move.
Cost: O(N).
This is why Arrays are terrible for queues or lists with frequent middle insertions.
4. Hidden Weapon: CPU Cache Locality
This is the #1 reason why Arrays beat Linked Lists in high-performance computing (Game Engines, AI, High Frequency Trading).
How RAM Works (The Lie)
We think "CPU reads one integer from RAM".
False.
RAM is slow. CPU is fast.
When CPU asks for address 1000, the memory controller says:
"He's asking for 1000. He'll probably ask for 1004, 1008 soon. Let's send the whole chunk!"
It moves a Cache Line (usually 64 bytes) at once. This implies 16 integers are loaded into the L1 Cache instantly.
Locality Visualization
Array (Contiguous Memory)
[0][1][2][3][4][5][6][7]...
- CPU requests
arr[0]. - Hardware Prefetcher loads
arr[0]...arr[15]into L1 Cache. - CPU requests
arr[1]. CACHE HIT! (Instant). - CPU requests
arr[2]. CACHE HIT! (Instant). - Result: 15/16 accesses are essentially free.
Linked List (Scattered Memory)
[Node1] --> (Random Address 0x5000) --> [Node2] --> (Random Address 0x9000)
- CPU requests Node 1.
- CPU requests Node 2. It's at 0x5000. Not in cache.
- CACHE MISS. CPU stalls for 100 cycles waiting for RAM.
- Result: Every step is slow.
In benchmarks, iterating a large Array is 10x-50x faster than a Linked List, purely due to Cache Locality.
5. Secret Power: SIMD (Vectorization)
Modern CPUs (Intel AVX, ARM NEON) can process multiple data points in a single clock cycle. This is SIMD (Single Instruction, Multiple Data).
- Scalar (Normal):
a[0] + b[0], thena[1] + b[1]... (8 cycles for 8 items) - Vector (SIMD): Load
a[0]...a[7]andb[0]...b[7], add them all at once. (1 cycle)
SIMD only works efficiently with Arrays (contiguous memory). It cannot optimize Linked Lists effectively.
This is why NumPy in Python is fast. It uses C-arrays and SIMD under the hood.
6. Security Risk: Buffer Overflow
The most famous hacking vulnerability in history. Languages like C/C++ do not check boundaries by default for performance.
The Attack
- Code allocates
char name[8](8 bytes). - Next to
namein memory is the Return Address (where function goes after finishing). - Hacker inputs a name with 100 characters ('A' * 100).
- The first 8 'A's fill the buffer.
- The next bytes overwrite the Return Address.
- The hacker replaces the Return Address with the address of their malicious code (Shellcode).
- Function returns -> Jumps to Hacker's code -> System Hacked.
Defense: Always check length. Use safer languages (Rust, Java, Python) that throw IndexOutOfBoundException.
7. Dynamic Arrays (How Python list works)
"Wait, Python lists can grow infinitely. Arrays are fixed size. How?"
Python's list, Java's ArrayList, C++'s std::vector are Dynamic Arrays.
They use a strategy called Geometric Resizing.
Generally:
- Allocate capacity: 4.
- User pushes 5th item. Overflow!
- Allocate new array with capacity: 8 (Double it).
- COPY all 4 items to new array.
- Delete old array.
- Insert 5th item.
Performance Analysis
- Most pushes: O(1).
- Resize push: O(N) (Copying).
- Amortized Time: O(1).
- Because resizing happens rarely (exponentially less frequent), the average cost per insertion is still constant.
Tip: In Java/C++, if you know you need 1,000,000 items, initialize with new ArrayList<>(1000000). This prevents the expensive resizing steps (4->8->16...->1000000).
7.5. Behind the Engine (Javascript V8)
In JavaScript, Arrays are strange.
const arr = [] is technically an Object-like structure.
BUT, the V8 engine (Chrome/Node.js) optimizes them.
Packed vs Holey Arrays
- Packed (Fast):
[1, 2, 3]. No holes. V8 stores this as a C-arraydoubles. Super fast. - Holey (Slow):
[1, , 3]. A hole in the middle. V8 downgrades this to a Hash Map lookup. 100x slower.
Tip: Never do arr[9999] = 1 on an empty array. You create 9999 holes. The engine gives up on optimization.
Always uses push() or initialize with new Array(size).fill(0).
Smi vs Double vs Object
V8 tracks element types.
PACKED_SMI_ELEMENTS: All small integers. (Fastest)PACKED_DOUBLE_ELEMENTS: Floats.PACKED_ELEMENTS: Objects/Strings.
If you add a float 1.5 to an integer array, V8 secretly converts the entire array to Doubles.
If you add a String, it de-optimizes to generic pointers.
Lesson: Keep your arrays monomorphic (same type).
8. Row-Major vs Column-Major (2D Arrays)
When you have a matrix int matrix[100][100], how is it flattened into 1D RAM?
Row-Major (C, C++, Java, Python, C#)
Row by row.
[0,0], [0,1] ... [0,99], [1,0] ...
Implication:
Looping for r in rows: for c in cols is FAST (Sequential access).
Looping for c in cols: for r in rows is SLOW (Cache miss every time).
Column-Major (Fortran, MATLAB, R, Julia)
Column by column.
[0,0], [1,0] ... [99,0], [0,1] ...
If you are a Data Scientist moving code from MATLAB to Python, be careful. Your loops might need to be swapped for performance.